本地优化
This commit is contained in:
@@ -26,4 +26,7 @@
|
|||||||
|
|
||||||
<!-- 如果需要录制视频到本地,可能还需要存储权限 -->
|
<!-- 如果需要录制视频到本地,可能还需要存储权限 -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<!-- 允许自身音频被本应用捕获,默认true -->
|
||||||
|
<application android:allowAudioPlaybackCapture="true" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -122,6 +122,134 @@ NavMeshSettings:
|
|||||||
debug:
|
debug:
|
||||||
m_Flags: 0
|
m_Flags: 0
|
||||||
m_NavMeshData: {fileID: 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
|
--- !u!224 &322971333 stripped
|
||||||
RectTransform:
|
RectTransform:
|
||||||
m_CorrespondingSourceObject: {fileID: 2618240917987615803, guid: e0d7d0986111c73499b1c0e092bc092a,
|
m_CorrespondingSourceObject: {fileID: 2618240917987615803, guid: e0d7d0986111c73499b1c0e092bc092a,
|
||||||
@@ -579,6 +707,12 @@ MonoBehaviour:
|
|||||||
m_MipBias: 0
|
m_MipBias: 0
|
||||||
m_VarianceClampScale: 0.9
|
m_VarianceClampScale: 0.9
|
||||||
m_ContrastAdaptiveSharpening: 0
|
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
|
--- !u!1 &1915034400
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -644,7 +778,8 @@ RectTransform:
|
|||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children: []
|
m_Children:
|
||||||
|
- {fileID: 314561961}
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||||
@@ -664,20 +799,20 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 8bcc82901c3f48a88d3408251afa3365, type: 3}
|
m_Script: {fileID: 11500000, guid: 8bcc82901c3f48a88d3408251afa3365, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
m_TextureSize: {x: 1920, y: 1200}
|
m_TextureSize: {x: 2560, y: 1440}
|
||||||
m_Source: 3
|
m_Source: 3
|
||||||
m_Camera: {fileID: 0}
|
m_Camera: {fileID: 0}
|
||||||
m_Texture: {fileID: 8400000, guid: dc38ccb1cb998bf4e8b83a3bbf7b46ad, type: 2}
|
m_Texture: {fileID: 0}
|
||||||
m_WebCamDeviceIndex: 1
|
m_WebCamDeviceIndex: 0
|
||||||
m_Depth: 16
|
m_Depth: 16
|
||||||
m_AntiAliasing: 1
|
m_AntiAliasing: 1
|
||||||
m_Codec:
|
m_Codec:
|
||||||
m_MimeType: video/H264
|
m_MimeType: video/VP9
|
||||||
m_SdpFmtpLine: implementation_name=NvCodec;level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=420033
|
m_SdpFmtpLine: implementation_name=Internal;profile-id=0
|
||||||
m_FrameRate: 30
|
m_FrameRate: 60
|
||||||
m_Bitrate:
|
m_Bitrate:
|
||||||
min: 0
|
min: 4000
|
||||||
max: 1000
|
max: 8000
|
||||||
m_ScaleFactor: 1
|
m_ScaleFactor: 1
|
||||||
m_AutoRequestUserAuthorization: 1
|
m_AutoRequestUserAuthorization: 1
|
||||||
--- !u!114 &1915034405
|
--- !u!114 &1915034405
|
||||||
@@ -731,7 +866,7 @@ MonoBehaviour:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
m_Source: 2
|
m_Source: 2
|
||||||
m_AudioListener: {fileID: 0}
|
m_AudioListener: {fileID: 1595310059}
|
||||||
m_AudioSource: {fileID: 0}
|
m_AudioSource: {fileID: 0}
|
||||||
m_MicrophoneDeviceIndex: 0
|
m_MicrophoneDeviceIndex: 0
|
||||||
m_AutoRequestUserAuthorization: 1
|
m_AutoRequestUserAuthorization: 1
|
||||||
@@ -741,8 +876,8 @@ MonoBehaviour:
|
|||||||
m_ChannelCount: 0
|
m_ChannelCount: 0
|
||||||
m_SampleRate: 0
|
m_SampleRate: 0
|
||||||
m_Bitrate:
|
m_Bitrate:
|
||||||
min: 8
|
min: 247
|
||||||
max: 208
|
max: 1000
|
||||||
m_Loopback: 0
|
m_Loopback: 0
|
||||||
--- !u!1660057539 &9223372036854775807
|
--- !u!1660057539 &9223372036854775807
|
||||||
SceneRoots:
|
SceneRoots:
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public interface IGlobalConfigSystem : ISystem
|
|||||||
|
|
||||||
public string GetUserId();
|
public string GetUserId();
|
||||||
public void SetUserId(string userId);
|
public void SetUserId(string userId);
|
||||||
|
|
||||||
|
public string GetParticipantId();
|
||||||
|
public void SetParticipantId(string participantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
|
public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
|
||||||
@@ -65,6 +68,8 @@ public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private int _connectionTimeType;
|
private int _connectionTimeType;
|
||||||
|
|
||||||
|
private string _participantId;
|
||||||
|
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource _cts;
|
||||||
|
|
||||||
|
|
||||||
@@ -211,6 +216,16 @@ public class GlobalConfigSystem : AbstractSystem, IGlobalConfigSystem
|
|||||||
_connectionAvatar = connectionTexture;
|
_connectionAvatar = connectionTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetParticipantId()
|
||||||
|
{
|
||||||
|
return _participantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetParticipantId(string participantId)
|
||||||
|
{
|
||||||
|
_participantId = participantId;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnInit()
|
protected override void OnInit()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ namespace Script
|
|||||||
await WebRTCUtil.DownloadAndSetAvatar(obj[i].avatar,
|
await WebRTCUtil.DownloadAndSetAvatar(obj[i].avatar,
|
||||||
entry.transform.Find("image").GetComponent<Image>());
|
entry.transform.Find("image").GetComponent<Image>());
|
||||||
_userMap.TryAdd(obj[i], entry);
|
_userMap.TryAdd(obj[i], entry);
|
||||||
|
|
||||||
|
if (obj[i].role == "host")
|
||||||
|
{
|
||||||
|
this.GetSystem<IGlobalConfigSystem>().SetParticipantId(obj[i].participantId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新会议聊天面板人数
|
// 更新会议聊天面板人数
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RenderStreaming;
|
using RenderStreaming;
|
||||||
|
using Script.Recorder;
|
||||||
using Script.Util;
|
using Script.Util;
|
||||||
using Stary.Evo;
|
using Stary.Evo;
|
||||||
using Stary.Evo.UIFarme;
|
using Stary.Evo.UIFarme;
|
||||||
@@ -58,7 +59,7 @@ namespace Script
|
|||||||
_audioStreamSender = GameObject.Find("RenderStreaming").GetComponent<AudioStreamSender>();
|
_audioStreamSender = GameObject.Find("RenderStreaming").GetComponent<AudioStreamSender>();
|
||||||
_videoStreamSender = GameObject.Find("RenderStreaming").GetComponent<VideoStreamSender>();
|
_videoStreamSender = GameObject.Find("RenderStreaming").GetComponent<VideoStreamSender>();
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
_recorder = new EditorGameViewRecorder();
|
_recorder = new ServerMixedRecorder();
|
||||||
#elif UNITY_ANDROID
|
#elif UNITY_ANDROID
|
||||||
_recorder = new XrealMixedRecorder();
|
_recorder = new XrealMixedRecorder();
|
||||||
#else
|
#else
|
||||||
|
|||||||
260
Assets/Script/Recorder/EditorServerRecorder.cs
Normal file
260
Assets/Script/Recorder/EditorServerRecorder.cs
Normal file
@@ -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<string> OnStoppedRecordingVideoAction { get; set; }
|
||||||
|
|
||||||
|
private string recordingId;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, RTCPeerConnection> 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>();
|
||||||
|
messageChannel.OnRecordingPeerRequestReceived += OnRecordingPeerRequestReceived;
|
||||||
|
messageChannel.OnRecordingAnswerReceived += OnRecordingAnswerReceived;
|
||||||
|
messageChannel.OnRecordingCandidateReceived += OnRecordingCandidateReceived;
|
||||||
|
messageChannel.OnRecordingStoppedReceived += OnRecordingStoppedReceived;
|
||||||
|
|
||||||
|
var data = new
|
||||||
|
{
|
||||||
|
connectionId = this.GetSystem<IGlobalConfigSystem>().GetConnectionId(),
|
||||||
|
layout = "grid",
|
||||||
|
format = "webm",
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await WebRequestSystem.Post(
|
||||||
|
this.GetSystem<IGlobalConfigSystem>().IP + "/api/recording-sessions",
|
||||||
|
JsonConvert.SerializeObject(data));
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
RecordingSession session = JsonConvert.DeserializeObject<RecordingSession>(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<WebcamToRenderTexture>();
|
||||||
|
videoTrack = new VideoStreamTrack(sourceRenderTexture.renderTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
microphoneAudioSource =
|
||||||
|
GameObject.Find("RenderStreaming/microphoneAudioSource").GetComponent<AudioSource>();
|
||||||
|
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<IGlobalConfigSystem>().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<IGlobalConfigSystem>().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<IGlobalConfigSystem>().GetConnectionId();
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "on-message",
|
||||||
|
["data"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["connectionId"] = connectionId,
|
||||||
|
["message"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = type,
|
||||||
|
["data"] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SignalingMessageHelper.SendMessage(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void StopRecording()
|
||||||
|
{
|
||||||
|
await WebRequestSystem.Delete(this.GetSystem<IGlobalConfigSystem>().IP,
|
||||||
|
$"/api/recording-sessions/{recordingId}");
|
||||||
|
var messageChannel = GameObject.FindObjectOfType<MessageChannel>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Assets/Script/Recorder/EditorServerRecorder.cs.meta
Normal file
3
Assets/Script/Recorder/EditorServerRecorder.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3583d180c5d94451bea6ead323d96021
|
||||||
|
timeCreated: 1780402391
|
||||||
381
Assets/Script/Recorder/ServerMixedRecorder.cs
Normal file
381
Assets/Script/Recorder/ServerMixedRecorder.cs
Normal file
@@ -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<string> OnStoppedRecordingVideoAction { get; set; }
|
||||||
|
|
||||||
|
private string recordingId;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, RTCPeerConnection> 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>();
|
||||||
|
messageChannel.OnRecordingPeerRequestReceived += OnRecordingPeerRequestReceived;
|
||||||
|
messageChannel.OnRecordingAnswerReceived += OnRecordingAnswerReceived;
|
||||||
|
messageChannel.OnRecordingCandidateReceived += OnRecordingCandidateReceived;
|
||||||
|
messageChannel.OnRecordingStoppedReceived += OnRecordingStoppedReceived;
|
||||||
|
|
||||||
|
var data = new
|
||||||
|
{
|
||||||
|
connectionId = this.GetSystem<IGlobalConfigSystem>().GetConnectionId(),
|
||||||
|
layout = "grid",
|
||||||
|
format = "webm",
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await WebRequestSystem.Post(
|
||||||
|
this.GetSystem<IGlobalConfigSystem>().IP + "/api/recording-sessions",
|
||||||
|
JsonConvert.SerializeObject(data));
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
RecordingSession session = JsonConvert.DeserializeObject<RecordingSession>(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<VideoStreamSender>();
|
||||||
|
videoTrack = videoStreamSender.Track as VideoStreamTrack;
|
||||||
|
|
||||||
|
|
||||||
|
AudioStreamSender audioStreamSender = RenderStreaming.GetComponent<AudioStreamSender>();
|
||||||
|
audioTrack = audioStreamSender.Track as AudioStreamTrack;
|
||||||
|
// sourceRenderTexture = this.GetSystem<IRenderStreamingSystem>().GetRenderStreamingTexture();
|
||||||
|
// if (sourceRenderTexture != null)
|
||||||
|
// {
|
||||||
|
// videoTrack = new VideoStreamTrack(sourceRenderTexture);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// microphoneAudioSource =
|
||||||
|
// GameObject.Find("RenderStreaming/microphoneAudioSource").GetComponent<AudioSource>();
|
||||||
|
// 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<IGlobalConfigSystem>().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<IGlobalConfigSystem>().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<IGlobalConfigSystem>().GetConnectionId();
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = "on-message",
|
||||||
|
["data"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["connectionId"] = connectionId,
|
||||||
|
["message"] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["type"] = type,
|
||||||
|
["data"] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SignalingMessageHelper.SendMessage(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void StopRecording()
|
||||||
|
{
|
||||||
|
await WebRequestSystem.Delete(this.GetSystem<IGlobalConfigSystem>().IP,
|
||||||
|
$"/api/recording-sessions/{recordingId}");
|
||||||
|
var messageChannel = GameObject.FindObjectOfType<MessageChannel>();
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string connectionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string layout { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string format { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string createdAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string startedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string updatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class Agent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string recordingId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string connectionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string mediaMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string createdAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string updatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class RecordingSession
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string success { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public Session session { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public Agent agent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string notified { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string peerRequestNotified { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Assets/Script/Recorder/ServerMixedRecorder.cs.meta
Normal file
3
Assets/Script/Recorder/ServerMixedRecorder.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d9d49eabfbbd4b4d8b4b88f082441596
|
||||||
|
timeCreated: 1780371636
|
||||||
47
Assets/Script/Recorder/WebcamToRenderTexture.cs
Normal file
47
Assets/Script/Recorder/WebcamToRenderTexture.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Assets/Script/Recorder/WebcamToRenderTexture.cs.meta
Normal file
3
Assets/Script/Recorder/WebcamToRenderTexture.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 63353e4a29ff45c9803dae86c9590d72
|
||||||
|
timeCreated: 1780403919
|
||||||
@@ -14,7 +14,7 @@ using CameraType = Unity.XR.XREAL.CameraType;
|
|||||||
using GalleryDataProvider = Unity.XR.XREAL.MockGalleryDataProvider;
|
using GalleryDataProvider = Unity.XR.XREAL.MockGalleryDataProvider;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public class XrealMixedRecorder : IVideoRecorder, IController
|
public class XrealMixedRecorder : IVideoRecorder, IController , IDisposable
|
||||||
{
|
{
|
||||||
public enum ResolutionLevel
|
public enum ResolutionLevel
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ public class XrealMixedRecorder : IVideoRecorder, IController
|
|||||||
|
|
||||||
public ResolutionLevel resolutionLevel = ResolutionLevel.High;
|
public ResolutionLevel resolutionLevel = ResolutionLevel.High;
|
||||||
public BlendMode blendMode = BlendMode.Blend;
|
public BlendMode blendMode = BlendMode.Blend;
|
||||||
public AudioState audioState = AudioState.MicAudio;
|
public AudioState audioState = AudioState.ApplicationAndMicAudio;
|
||||||
public CaptureSide captureside = CaptureSide.Single;
|
public CaptureSide captureside = CaptureSide.Single;
|
||||||
public bool useGreenBackGround = false;
|
public bool useGreenBackGround = false;
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ public class XrealMixedRecorder : IVideoRecorder, IController
|
|||||||
}
|
}
|
||||||
|
|
||||||
Debug.Log("Stop Video Capture!");
|
Debug.Log("Stop Video Capture!");
|
||||||
_videoCapture.StopRecordingAsync(OnStoppedRecordingVideo);
|
_videoCapture.StopRecordingAsync(OnStoppedVideoCaptureMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Executes the 'stopped recording video' action. </summary>
|
/// <summary> Executes the 'stopped recording video' action. </summary>
|
||||||
@@ -103,6 +103,11 @@ public class XrealMixedRecorder : IVideoRecorder, IController
|
|||||||
/// <param name="result"> The result.</param>
|
/// <param name="result"> The result.</param>
|
||||||
private async void OnStoppedVideoCaptureMode(XREALVideoCapture.VideoCaptureResult result)
|
private async void OnStoppedVideoCaptureMode(XREALVideoCapture.VideoCaptureResult result)
|
||||||
{
|
{
|
||||||
|
if (!result.success)
|
||||||
|
{
|
||||||
|
Debug.Log("Stopped Recording Video Faild!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Debug.Log("Stopped Video Capture Mode!");
|
Debug.Log("Stopped Video Capture Mode!");
|
||||||
|
|
||||||
var encoder = _videoCapture.GetContext().GetEncoder() as VideoEncoder;
|
var encoder = _videoCapture.GetContext().GetEncoder() as VideoEncoder;
|
||||||
@@ -114,8 +119,6 @@ public class XrealMixedRecorder : IVideoRecorder, IController
|
|||||||
await DelayInsertVideoToGallery(path, filename, "Record");
|
await DelayInsertVideoToGallery(path, filename, "Record");
|
||||||
OnStoppedRecordingVideoAction?.Invoke(path);
|
OnStoppedRecordingVideoAction?.Invoke(path);
|
||||||
// Release video capture resource.
|
// Release video capture resource.
|
||||||
_videoCapture.Dispose();
|
|
||||||
_videoCapture = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> 延迟将视频插入相册,确保视频文件已完全写入 </summary>
|
/// <summary> 延迟将视频插入相册,确保视频文件已完全写入 </summary>
|
||||||
@@ -235,4 +238,19 @@ public class XrealMixedRecorder : IVideoRecorder, IController
|
|||||||
{
|
{
|
||||||
return MainArchitecture.Interface;
|
return MainArchitecture.Interface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_videoCapture.StopVideoModeAsync((result) =>
|
||||||
|
{
|
||||||
|
if (!result.success)
|
||||||
|
{
|
||||||
|
Debug.Log("Stopped Video Capture Mode faild!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_videoCapture?.Dispose();
|
||||||
|
_videoCapture = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ namespace Script
|
|||||||
{
|
{
|
||||||
void SetUp();
|
void SetUp();
|
||||||
void HangUp();
|
void HangUp();
|
||||||
|
|
||||||
|
RenderTexture GetRenderStreamingTexture();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RenderStreamingSystem : AbstractSystem, IRenderStreamingSystem
|
public class RenderStreamingSystem : AbstractSystem, IRenderStreamingSystem
|
||||||
@@ -232,6 +234,15 @@ namespace Script
|
|||||||
Debug.Log($"[MultiParticipantHost] Participant UI removed: {participantId}");
|
Debug.Log($"[MultiParticipantHost] Participant UI removed: {participantId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RenderTexture GetRenderStreamingTexture()
|
||||||
|
{
|
||||||
|
if (_yuvToRenderTexture.localRenderTexture == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("RenderTexture 未初始化");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _yuvToRenderTexture.localRenderTexture;
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -348,16 +348,13 @@ namespace Stary.Evo
|
|||||||
/// <param name="url">获取Token值的服务URL地址(很重要)</param>
|
/// <param name="url">获取Token值的服务URL地址(很重要)</param>
|
||||||
/// <param name="postData">传入请求的参数,此处参数为JOSN格式</param>
|
/// <param name="postData">传入请求的参数,此处参数为JOSN格式</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task<ResultMessageEntity> Post(string url, string postData)
|
public static async Task<string> Post(string url, string postData)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await GetCertificateData();
|
||||||
using var webRequest = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST);
|
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();
|
webRequest.downloadHandler = new DownloadHandlerBuffer();
|
||||||
var postBytes = Encoding.UTF8.GetBytes(postData);
|
var postBytes = Encoding.UTF8.GetBytes(postData);
|
||||||
webRequest.uploadHandler = new UploadHandlerRaw(postBytes);
|
webRequest.uploadHandler = new UploadHandlerRaw(postBytes);
|
||||||
@@ -367,6 +364,7 @@ namespace Stary.Evo
|
|||||||
webRequest.disposeDownloadHandlerOnDispose = true;
|
webRequest.disposeDownloadHandlerOnDispose = true;
|
||||||
webRequest.disposeCertificateHandlerOnDispose = true;
|
webRequest.disposeCertificateHandlerOnDispose = true;
|
||||||
webRequest.timeout = 30;
|
webRequest.timeout = 30;
|
||||||
|
webRequest.certificateHandler = new SelfSignedCertHandler(certificateData);
|
||||||
await webRequest.SendWebRequest();
|
await webRequest.SendWebRequest();
|
||||||
webRequest.uploadHandler?.Dispose();
|
webRequest.uploadHandler?.Dispose();
|
||||||
// 更新错误检查方式
|
// 更新错误检查方式
|
||||||
@@ -374,28 +372,16 @@ namespace Stary.Evo
|
|||||||
webRequest.result == UnityWebRequest.Result.ProtocolError)
|
webRequest.result == UnityWebRequest.Result.ProtocolError)
|
||||||
{
|
{
|
||||||
Debug.LogError(webRequest.error);
|
Debug.LogError(webRequest.error);
|
||||||
return new ResultMessageEntity
|
return webRequest.error;
|
||||||
{
|
|
||||||
code = 5001,
|
|
||||||
message = webRequest.error
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultMessageEntity =
|
return webRequest.downloadHandler.text;
|
||||||
JsonConvert.DeserializeObject<ResultMessageEntity>(webRequest.downloadHandler.text);
|
|
||||||
if (resultMessageEntity.code != 200) Debug.LogError(resultMessageEntity.message);
|
|
||||||
|
|
||||||
return resultMessageEntity;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Debug.LogError("UnityEvo:WebRequestSystem.Post" + e.Message);
|
Debug.LogError("UnityEvo:WebRequestSystem.Post" + e.Message);
|
||||||
|
|
||||||
return new ResultMessageEntity
|
return e.Message;
|
||||||
{
|
|
||||||
code = 5001,
|
|
||||||
message = e.Message
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,10 +394,11 @@ namespace Stary.Evo
|
|||||||
/// <param name="url">请求数据的URL地址</param>
|
/// <param name="url">请求数据的URL地址</param>
|
||||||
/// <param name="path">请求数据的路径</param>
|
/// <param name="path">请求数据的路径</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async Task<ResultMessageEntity> Delete(string url, string path)
|
public static async Task<string> Delete(string url, string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await GetCertificateData();
|
||||||
// 修复URL拼接
|
// 修复URL拼接
|
||||||
var fullUrl = url.TrimEnd('/') + "/" + path.TrimStart('/');
|
var fullUrl = url.TrimEnd('/') + "/" + path.TrimStart('/');
|
||||||
using var webRequest = new UnityWebRequest(fullUrl, UnityWebRequest.kHttpVerbDELETE);
|
using var webRequest = new UnityWebRequest(fullUrl, UnityWebRequest.kHttpVerbDELETE);
|
||||||
@@ -420,6 +407,7 @@ namespace Stary.Evo
|
|||||||
webRequest.SetRequestHeader("Authorization", authorization); // 修正请求头名称规范
|
webRequest.SetRequestHeader("Authorization", authorization); // 修正请求头名称规范
|
||||||
|
|
||||||
webRequest.timeout = 20;
|
webRequest.timeout = 20;
|
||||||
|
webRequest.certificateHandler = new SelfSignedCertHandler(certificateData);
|
||||||
await webRequest.SendWebRequest();
|
await webRequest.SendWebRequest();
|
||||||
|
|
||||||
// 增强错误处理
|
// 增强错误处理
|
||||||
@@ -431,36 +419,20 @@ namespace Stary.Evo
|
|||||||
$"Response: {webRequest.downloadHandler.text}";
|
$"Response: {webRequest.downloadHandler.text}";
|
||||||
|
|
||||||
Debug.LogError(errorMsg);
|
Debug.LogError(errorMsg);
|
||||||
return new ResultMessageEntity
|
return errorMsg;
|
||||||
{
|
|
||||||
code = 5001,
|
|
||||||
message = errorMsg
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复空响应处理
|
// 修复空响应处理
|
||||||
var responseText = webRequest.downloadHandler.text;
|
var responseText = webRequest.downloadHandler.text;
|
||||||
if (string.IsNullOrEmpty(responseText))
|
|
||||||
return new ResultMessageEntity
|
|
||||||
{
|
|
||||||
code = 200,
|
|
||||||
message = "删除成功"
|
|
||||||
};
|
|
||||||
|
|
||||||
var resultMessageEntity =
|
|
||||||
JsonConvert.DeserializeObject<ResultMessageEntity>(webRequest.downloadHandler.text);
|
|
||||||
if (resultMessageEntity.code != 200) Debug.LogError(resultMessageEntity.message);
|
|
||||||
|
|
||||||
return resultMessageEntity;
|
|
||||||
|
return responseText;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Debug.LogError("UnityEvo:WebRequestSystem.Get" + e.Message);
|
Debug.LogError("UnityEvo:WebRequestSystem.Get" + e.Message);
|
||||||
return new ResultMessageEntity
|
return e.Message;
|
||||||
{
|
|
||||||
code = 5001,
|
|
||||||
message = e.Message
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,6 +540,5 @@ namespace Stary.Evo
|
|||||||
else
|
else
|
||||||
Debug.Log($"上传成功: {request.downloadHandler.text}");
|
Debug.Log($"上传成功: {request.downloadHandler.text}");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,12 @@ namespace Unity.RenderStreaming
|
|||||||
public event Action<string, ChatData> OnChatMessageReceived;
|
public event Action<string, ChatData> OnChatMessageReceived;
|
||||||
|
|
||||||
|
|
||||||
|
public event Action<RecordingRequest> OnRecordingPeerRequestReceived;
|
||||||
|
public event Action<RecordingAnswer> OnRecordingAnswerReceived;
|
||||||
|
public event Action<RecordingCandidate> OnRecordingCandidateReceived;
|
||||||
|
public event Action<string> OnRecordingStoppedReceived;
|
||||||
|
|
||||||
|
|
||||||
public override void OnMessage(string message)
|
public override void OnMessage(string message)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -59,6 +65,27 @@ namespace Unity.RenderStreaming
|
|||||||
var mediaState = json.ToObject<MediaStateChange>();
|
var mediaState = json.ToObject<MediaStateChange>();
|
||||||
OnMediaStateChangeReceived?.Invoke(ConnectionId, mediaState);
|
OnMediaStateChangeReceived?.Invoke(ConnectionId, mediaState);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MessageTypes.RecordingPeerRequest:
|
||||||
|
json = record.data as JObject;
|
||||||
|
var recordingPeerRequest = json.ToObject<RecordingRequest>();
|
||||||
|
OnRecordingPeerRequestReceived?.Invoke(recordingPeerRequest);
|
||||||
|
break;
|
||||||
|
case MessageTypes.RecordingAnswer:
|
||||||
|
json = record.data as JObject;
|
||||||
|
var recordingAnswer = json.ToObject<RecordingAnswer>();
|
||||||
|
OnRecordingAnswerReceived?.Invoke(recordingAnswer);
|
||||||
|
break;
|
||||||
|
case MessageTypes.RecordingCandidate:
|
||||||
|
json = record.data as JObject;
|
||||||
|
var recordingCandidate = json.ToObject<RecordingCandidate>();
|
||||||
|
OnRecordingCandidateReceived?.Invoke(recordingCandidate);
|
||||||
|
break;
|
||||||
|
case MessageTypes.RecordingStopped:
|
||||||
|
json = record.data as JObject;
|
||||||
|
var recordingStopped = json.ToObject<RecordingStopped>();
|
||||||
|
OnRecordingStoppedReceived?.Invoke(recordingStopped.recordingId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
messageHistory.Add(record);
|
messageHistory.Add(record);
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ namespace Unity.RenderStreaming
|
|||||||
public const string UserInfo = "user-info";
|
public const string UserInfo = "user-info";
|
||||||
public const string MediaStateChange = "media-state-changed";
|
public const string MediaStateChange = "media-state-changed";
|
||||||
public const string ParticipantsSync = "participants-sync";
|
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]
|
[Serializable]
|
||||||
@@ -54,4 +59,43 @@ namespace Unity.RenderStreaming
|
|||||||
public int width;
|
public int width;
|
||||||
public int height;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using RenderStreaming;
|
using RenderStreaming;
|
||||||
using Stary.Evo;
|
using Stary.Evo;
|
||||||
|
using Unity.RenderStreaming;
|
||||||
using Unity.XR.XREAL;
|
using Unity.XR.XREAL;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
@@ -19,12 +20,14 @@ namespace Script
|
|||||||
//private RawImage _localVideoImage;
|
//private RawImage _localVideoImage;
|
||||||
|
|
||||||
//private RawImage _textureImage;
|
//private RawImage _textureImage;
|
||||||
|
private VideoStreamSender videoStreamSender;
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
//_localVideoImage= GameObject.Find("CanvasMain/RawImage").GetComponent<RawImage>();
|
//_localVideoImage= GameObject.Find("CanvasMain/RawImage").GetComponent<RawImage>();
|
||||||
//_textureImage= GameObject.Find("CanvasMain/RawImage1").GetComponent<RawImage>();
|
//_textureImage= GameObject.Find("CanvasMain/RawImage1").GetComponent<RawImage>();
|
||||||
localVideoMaterial = Resources.Load<Material>("LocalRenderMaterial");
|
localVideoMaterial = Resources.Load<Material>("LocalRenderMaterial");
|
||||||
m_RGBCameraTexture = XREALRGBCameraTexture.CreateSingleton();
|
m_RGBCameraTexture = XREALRGBCameraTexture.CreateSingleton();
|
||||||
|
videoStreamSender=this.GetComponent<VideoStreamSender>();
|
||||||
Play();
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +55,8 @@ namespace Script
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// 获取Y纹理的尺寸作为目标RenderTexture的尺寸
|
// 获取Y纹理的尺寸作为目标RenderTexture的尺寸
|
||||||
int width = yuvTextures[0].width;
|
var width = videoStreamSender.width;
|
||||||
int height = yuvTextures[0].height;
|
var height = videoStreamSender.height;
|
||||||
// if (_localVideoImage==null || _localVideoImage.rectTransform.sizeDelta.x != width ||
|
// if (_localVideoImage==null || _localVideoImage.rectTransform.sizeDelta.x != width ||
|
||||||
// _localVideoImage.rectTransform.sizeDelta.y != height)
|
// _localVideoImage.rectTransform.sizeDelta.y != height)
|
||||||
// {
|
// {
|
||||||
@@ -68,7 +71,7 @@ namespace Script
|
|||||||
localRenderTexture.Release();
|
localRenderTexture.Release();
|
||||||
|
|
||||||
// 创建新的RenderTexture
|
// 创建新的RenderTexture
|
||||||
localRenderTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
|
localRenderTexture = new RenderTexture((int)width, (int)height, 0, RenderTextureFormat.ARGB32);
|
||||||
localRenderTexture.enableRandomWrite = true;
|
localRenderTexture.enableRandomWrite = true;
|
||||||
localRenderTexture.Create();
|
localRenderTexture.Create();
|
||||||
//_textureImage.texture = localRenderTexture;
|
//_textureImage.texture = localRenderTexture;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Unity.WebRTC;
|
using Unity.WebRTC;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using WebSocketSharp;
|
using WebSocketSharp;
|
||||||
@@ -439,7 +440,7 @@ namespace Unity.RenderStreaming.Signaling
|
|||||||
}
|
}
|
||||||
else if (routedMessage.type == "on-message")
|
else if (routedMessage.type == "on-message")
|
||||||
{
|
{
|
||||||
var message = JsonUtility.FromJson<SignalingMessage>(content);
|
var message = JsonConvert.DeserializeObject<SignalingMessage>(content);
|
||||||
var messageData = new OnMessageData
|
var messageData = new OnMessageData
|
||||||
{
|
{
|
||||||
connectionId = routedMessage.from,
|
connectionId = routedMessage.from,
|
||||||
|
|||||||
Reference in New Issue
Block a user