This commit is contained in:
2026-05-27 00:05:32 +08:00
parent 33128ea686
commit e53f400530
12 changed files with 464 additions and 1 deletions

View File

@@ -3588,7 +3588,7 @@ MonoBehaviour:
onValueChanged:
m_PersistentCalls:
m_Calls: []
m_IsOn: 1
m_IsOn: 0
--- !u!1 &3813444533319316158
GameObject:
m_ObjectHideFlags: 0

View File

@@ -40,6 +40,9 @@ namespace Script
private AudioStreamSender _audioStreamSender;
private VideoStreamSender _videoStreamSender;
private IVideoRecorder _recorder;
public void Initialize(GameObject panelGo, MainPanel mainPanel)
{
PanelGo = panelGo;
@@ -54,8 +57,18 @@ namespace Script
_hangUpTog = PanelGo.transform.Find("menuBar/hangUp").GetComponent<Button>();
_audioStreamSender = GameObject.Find("RenderStreaming").GetComponent<AudioStreamSender>();
_videoStreamSender = GameObject.Find("RenderStreaming").GetComponent<VideoStreamSender>();
#if UNITY_EDITOR
_recorder = new EditorGameViewRecorder();
#elif UNITY_ANDROID
_recorder = new XrealMixedRecorder();
#else
Debug.LogWarning("当前平台没有可用录制器");
#endif
_recorder.OnStartedRecordingVideo = OnStartedRecordingVideo;
_recorder.OnStoppedRecordingVideoAction = OnStoppedRecordingVideoAction;
}
public void OnEnter()
{
LoadUsers();
@@ -119,8 +132,38 @@ namespace Script
private void OnRecordTogValueChanged(bool value)
{
if (value)
_recorder.StartRecording();
else
_recorder.StopRecording();
}
private void OnStartedRecordingVideo()
{
}
private void OnStoppedRecordingVideoAction(string path)
{
var participants = new List<MainPanel.UsersItem>();
MainPanel.UsersItem host = null;
foreach (var value in _meetingList.Values)
if (value.item.role == "host")
{
host = value.item;
break;
}
else
{
participants.Add(value.item);
}
WebRequestSystem.Upload(path, this.GetSystem<IGlobalConfigSystem>().IP,
this.GetSystem<IGlobalConfigSystem>().GetConnectionId(),
this.GetSystem<IGlobalConfigSystem>().GetUserId(), host, participants);
}
private void OnHangUpTogValueChanged()
{
this.GetSystem<IRenderStreamingSystem>().HangUp();

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0d0e1b8f02d02434c9f0d3e805a6ba04
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,99 @@
#if UNITY_EDITOR
using System;
using System.IO;
using RenderStreaming;
using Stary.Evo;
using UnityEngine;
using UnityEditor.Recorder;
using UnityEditor.Recorder.Input;
public class EditorGameViewRecorder : IVideoRecorder, IController
{
private RecorderController recorderController;
public bool IsRecording => recorderController != null && recorderController.IsRecording();
/// <summary> Save the video to Application.persistentDataPath. </summary>
/// <value> The full pathname of the video save file. </value>
private string VideoSaveExtension => Path.Combine(Application.persistentDataPath, "Recording");
private string VideoSavePath
{
get
{
var timeStamp = DateTime.Now.ToString("yyyy-MM-ddTHH-mm-ss");
var filename =
$"meeting-recording-{this.GetSystem<IGlobalConfigSystem>().GetConnectionId()}-{timeStamp}";
return Path.Combine(Application.persistentDataPath, VideoSaveExtension, filename);
}
}
private string _videoSavePath;
public Action OnStartedRecordingVideo { get; set; }
public Action<string> OnStoppedRecordingVideoAction { get; set; }
public void StartRecording()
{
if (IsRecording)
return;
_videoSavePath = VideoSavePath;
var controllerSettings = ScriptableObject.CreateInstance<RecorderControllerSettings>();
recorderController = new RecorderController(controllerSettings);
var movieSettings = ScriptableObject.CreateInstance<MovieRecorderSettings>();
movieSettings.name = "Game View MP4 Recorder";
movieSettings.Enabled = true;
movieSettings.OutputFormat = MovieRecorderSettings.VideoRecorderOutputFormat.MP4;
movieSettings.OutputFile = _videoSavePath;
movieSettings.CaptureAudio = false;
movieSettings.ImageInputSettings = new GameViewInputSettings
{
OutputWidth = 1920,
OutputHeight = 1080
};
controllerSettings.AddRecorderSettings(movieSettings);
controllerSettings.SetRecordModeToManual();
controllerSettings.FrameRate = 30.0f;
controllerSettings.CapFrameRate = true;
recorderController.PrepareRecording();
var started = recorderController.StartRecording();
if (!started)
{
Debug.LogError("Editor Recorder 启动失败,请确认已进入 Play Mode并安装 com.unity.recorder");
recorderController = null;
return;
}
Debug.Log($"Editor 开始录制: {_videoSavePath}");
OnStartedRecordingVideo?.Invoke();
}
public void StopRecording()
{
if (!IsRecording)
{
Debug.LogError("当前没有正在进行的 Editor 录制");
return;
}
recorderController.StopRecording();
if (!File.Exists(_videoSavePath))
OnStoppedRecordingVideoAction?.Invoke(_videoSavePath + ".mp4");
else
Debug.LogError($"Editor MP4 文件不存在,可能还在写入或输出路径变化: {_videoSavePath}");
recorderController = null;
}
public IArchitecture GetArchitecture()
{
return MainArchitecture.Interface;
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f7a8ef900894205803013925c8fbe95
timeCreated: 1779783425

View File

@@ -0,0 +1,10 @@
using System;
public interface IVideoRecorder
{
bool IsRecording { get; }
Action OnStartedRecordingVideo { get; set; }
Action<string> OnStoppedRecordingVideoAction { get; set; }
void StartRecording();
void StopRecording();
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7807c2de252777049950ce1055c8f3a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using RenderStreaming;
using Stary.Evo;
using UnityEngine;
using Unity.XR.XREAL;
using CameraType = Unity.XR.XREAL.CameraType;
#if UNITY_ANDROID && !UNITY_EDITOR
using GalleryDataProvider = NativeGalleryDataProvider;
#else
using GalleryDataProvider = Unity.XR.XREAL.MockGalleryDataProvider;
#endif
public class XrealMixedRecorder : IVideoRecorder, IController
{
public enum ResolutionLevel
{
High,
Middle,
Low
}
public ResolutionLevel resolutionLevel = ResolutionLevel.High;
public BlendMode blendMode = BlendMode.Blend;
public AudioState audioState = AudioState.ApplicationAndMicAudio;
public CaptureSide captureside = CaptureSide.Single;
public bool useGreenBackGround = false;
private XREALVideoCapture _videoCapture;
public bool IsRecording => _videoCapture != null && _videoCapture.IsRecording;
public Action OnStartedRecordingVideo { get; set; }
public Action<string> OnStoppedRecordingVideoAction { get; set; }
private GalleryDataProvider _galleryDataTool;
/// <summary> Save the video to Application.persistentDataPath. </summary>
/// <value> The full pathname of the video save file. </value>
private string VideoSaveExtension => Path.Combine(Application.persistentDataPath, "Recording");
private string VideoSavePath
{
get
{
var timeStamp = Time.time.ToString().Replace(".", "").Replace(":", "");
var filename =
$"{this.GetSystem<IGlobalConfigSystem>().GetConnectionId()}{timeStamp}.mp4";
return Path.Combine(Application.persistentDataPath, VideoSaveExtension, filename);
}
}
private string _videoSavePath;
public void StartRecording()
{
_videoSavePath = VideoSavePath;
if (_videoCapture == null)
CreateVideoCapture(StartVideoCapture);
else if (_videoCapture.IsRecording)
StopVideoCapture();
else
StartVideoCapture();
}
public void StopRecording()
{
if (_videoCapture == null || !_videoCapture.IsRecording)
{
Debug.LogError("当前没有正在进行的 XREAL 录制");
return;
}
StopVideoCapture();
}
/// <summary> Stops video capture. </summary>
public void StopVideoCapture()
{
if (_videoCapture == null || !_videoCapture.IsRecording)
{
Debug.LogWarning("Can not stop video capture!");
return;
}
Debug.Log("Stop Video Capture!");
_videoCapture.StopRecordingAsync(OnStoppedRecordingVideo);
}
/// <summary> Executes the 'stopped recording video' action. </summary>
/// <param name="result"> The result.</param>
private void OnStoppedRecordingVideo(XREALVideoCapture.VideoCaptureResult result)
{
if (!result.success)
{
Debug.Log("Stopped Recording Video Faild!");
return;
}
Debug.Log("Stopped Recording Video!");
_videoCapture.StopVideoModeAsync(OnStoppedVideoCaptureMode);
}
/// <summary> Executes the 'stopped video capture mode' action. </summary>
/// <param name="result"> The result.</param>
private async void OnStoppedVideoCaptureMode(XREALVideoCapture.VideoCaptureResult result)
{
Debug.Log("Stopped Video Capture Mode!");
var encoder = _videoCapture.GetContext().GetEncoder() as VideoEncoder;
var path = encoder.EncodeConfig.outPutPath;
var filename = string.Format("Xreal_Shot_Video_{0}.mp4",
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
await DelayInsertVideoToGallery(path, filename, "Record");
OnStoppedRecordingVideoAction?.Invoke(path);
// Release video capture resource.
_videoCapture.Dispose();
_videoCapture = null;
}
/// <summary> 延迟将视频插入相册,确保视频文件已完全写入 </summary>
/// <param name="originFilePath"> 视频文件路径 </param>
/// <param name="displayName"> 显示名称 </param>
/// <param name="folderName"> 文件夹名称 </param>
private async UniTask DelayInsertVideoToGallery(string originFilePath, string displayName, string folderName)
{
await UniTask.DelayFrame(100);
InsertVideoToGallery(originFilePath, displayName, folderName);
}
/// <summary> 将视频插入到系统相册中 </summary>
/// <param name="originFilePath"> 视频文件的原始路径 </param>
/// <param name="displayName"> 视频在相册中显示的名称 </param>
/// <param name="folderName"> 相册文件夹名称 </param>
public void InsertVideoToGallery(string originFilePath, string displayName, string folderName)
{
Debug.LogFormat("InsertVideoToGallery: {0}, {1} => {2}", displayName, originFilePath, folderName);
if (_galleryDataTool == null) _galleryDataTool = new GalleryDataProvider();
_galleryDataTool.InsertVideo(originFilePath, displayName, folderName);
}
private void CreateVideoCapture(Action callback)
{
XREALVideoCaptureUtility.CreateAsync(false, delegate(XREALVideoCapture videoCapture)
{
Debug.Log("Created VideoCapture Instance!");
if (videoCapture != null)
{
_videoCapture = videoCapture;
callback?.Invoke();
}
else
{
Debug.LogError("Failed to create VideoCapture Instance!");
}
});
}
public void StartVideoCapture()
{
if (_videoCapture.IsRecording)
{
Debug.LogWarning("Can not start video capture!");
return;
}
var cameraParameters = new CameraParameters();
var cameraResolution = GetResolutionByLevel(resolutionLevel);
cameraParameters.cameraType = CameraType.RGB;
cameraParameters.hologramOpacity = 0.0f;
cameraParameters.frameRate = NativeConstants.RECORD_FPS_DEFAULT;
cameraParameters.cameraResolutionWidth = cameraResolution.width;
cameraParameters.cameraResolutionHeight = cameraResolution.height;
cameraParameters.pixelFormat = CapturePixelFormat.PNG;
cameraParameters.blendMode = blendMode;
// Set audio state, audio record needs the permission of "android.permission.RECORD_AUDIO",
// Add it to your "AndroidManifest.xml" file in "Assets/Plugin".
cameraParameters.audioState = audioState;
cameraParameters.captureSide = captureside;
cameraParameters.backgroundColor = useGreenBackGround ? Color.green : Color.black;
_videoCapture.StartVideoModeAsync(cameraParameters, OnStartedVideoCaptureMode, true);
}
private Resolution GetResolutionByLevel(ResolutionLevel level)
{
var resolutions =
XREALVideoCaptureUtility.SupportedResolutions.OrderByDescending((res) => res.width * res.height);
var resolution = new Resolution();
switch (level)
{
case ResolutionLevel.High:
resolution = resolutions.ElementAt(0);
break;
case ResolutionLevel.Middle:
resolution = resolutions.ElementAt(1);
break;
case ResolutionLevel.Low:
resolution = resolutions.ElementAt(2);
break;
default:
break;
}
return resolution;
}
private void OnStartedVideoCaptureMode(XREALVideoCapture.VideoCaptureResult result)
{
if (!result.success)
{
Debug.Log("Started Video Capture Mode faild!");
return;
}
Debug.Log("Started Video Capture Mode!");
_videoCapture.StartRecordingAsync(_videoSavePath, (a) => OnStartedRecordingVideo?.Invoke());
}
public IArchitecture GetArchitecture()
{
return MainArchitecture.Interface;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7a4221a28a1840a3aae5572edfa217be
timeCreated: 1779783401

View File

@@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Script;
using UnityEngine;
using UnityEngine.Networking;
@@ -511,5 +512,37 @@ namespace Stary.Evo
};
}
}
public static async UniTask Upload(string filePath, string serverBaseUrl, string meetingId, string userId,
MainPanel.UsersItem host,
List<MainPanel.UsersItem> participants)
{
var bytes = File.ReadAllBytes(filePath);
var fileName = Path.GetFileName(filePath);
var ext = Path.GetExtension(fileName).ToLowerInvariant();
var mimeType = ext == ".webm" ? "video/webm" : "video/mp4";
var form = new List<IMultipartFormSection>
{
new MultipartFormDataSection("meetingId", meetingId),
new MultipartFormDataSection("userId", userId),
new MultipartFormDataSection("filename", fileName),
new MultipartFormFileSection("recording", bytes, fileName, mimeType),
new MultipartFormDataSection("host", JsonConvert.SerializeObject(host)),
new MultipartFormDataSection("participants", JsonConvert.SerializeObject(participants))
};
using var request = UnityWebRequest.Post($"{serverBaseUrl}/api/recordings", form);
request.timeout = 300;
request.certificateHandler = new SelfSignedCertHandler(certificateData);
await request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
Debug.LogError($"上传失败: {request.error}, body={request.downloadHandler.text}");
else
Debug.Log($"上传成功: {request.downloadHandler.text}");
}
}
}

View File

@@ -5,6 +5,7 @@
"com.unity.ide.rider": "3.0.40",
"com.unity.inputsystem": "1.19.0",
"com.unity.nuget.newtonsoft-json": "3.2.2",
"com.unity.recorder": "4.0.3",
"com.unity.render-pipelines.universal": "17.3.0",
"com.unity.test-framework": "1.1.33",
"com.unity.ugui": "2.0.0",

View File

@@ -91,6 +91,15 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.recorder": {
"version": "4.0.3",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.timeline": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.render-pipelines.core": {
"version": "14.0.12",
"depth": 1,
@@ -167,6 +176,18 @@
},
"url": "https://packages.unity.com"
},
"com.unity.timeline": {
"version": "1.7.7",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.director": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.particlesystem": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.ugui": {
"version": "2.0.0",
"depth": 0,