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; } } }