381 lines
12 KiB
C#
381 lines
12 KiB
C#
|
|
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; }
|
|||
|
|
}
|
|||
|
|
}
|