2026-05-27 00:05:32 +08:00
|
|
|
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
|
2026-06-01 00:23:11 +08:00
|
|
|
using GalleryDataProvider = Unity.XR.XREAL.NativeGalleryDataProvider;
|
2026-05-27 00:05:32 +08:00
|
|
|
#else
|
|
|
|
|
using GalleryDataProvider = Unity.XR.XREAL.MockGalleryDataProvider;
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-06-03 22:05:03 +08:00
|
|
|
public class XrealMixedRecorder : IVideoRecorder, IController , IDisposable
|
2026-05-27 00:05:32 +08:00
|
|
|
{
|
|
|
|
|
public enum ResolutionLevel
|
|
|
|
|
{
|
|
|
|
|
High,
|
|
|
|
|
Middle,
|
|
|
|
|
Low
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ResolutionLevel resolutionLevel = ResolutionLevel.High;
|
|
|
|
|
public BlendMode blendMode = BlendMode.Blend;
|
2026-06-03 22:05:03 +08:00
|
|
|
public AudioState audioState = AudioState.ApplicationAndMicAudio;
|
2026-05-27 00:05:32 +08:00
|
|
|
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;
|
|
|
|
|
|
2026-06-02 11:39:30 +08:00
|
|
|
|
2026-05-27 00:05:32 +08:00
|
|
|
/// <summary> Save the video to Application.persistentDataPath. </summary>
|
|
|
|
|
/// <value> The full pathname of the video save file. </value>
|
2026-06-02 11:39:30 +08:00
|
|
|
public string VideoSavePath
|
2026-05-27 00:05:32 +08:00
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2026-06-02 11:39:30 +08:00
|
|
|
string timeStamp = Time.time.ToString().Replace(".", "").Replace(":", "");
|
|
|
|
|
string filename = string.Format("Xreal_Record_{0}.mp4", timeStamp);
|
|
|
|
|
return Path.Combine(Application.persistentDataPath, filename);
|
2026-05-27 00:05:32 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-02 11:39:30 +08:00
|
|
|
|
2026-05-27 00:05:32 +08:00
|
|
|
|
|
|
|
|
public void StartRecording()
|
|
|
|
|
{
|
|
|
|
|
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!");
|
2026-06-03 22:05:03 +08:00
|
|
|
_videoCapture.StopRecordingAsync(OnStoppedVideoCaptureMode);
|
2026-05-27 00:05:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
{
|
2026-06-03 22:05:03 +08:00
|
|
|
if (!result.success)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log("Stopped Recording Video Faild!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-27 00:05:32 +08:00
|
|
|
Debug.Log("Stopped Video Capture Mode!");
|
|
|
|
|
|
|
|
|
|
var encoder = _videoCapture.GetContext().GetEncoder() as VideoEncoder;
|
|
|
|
|
var path = encoder.EncodeConfig.outPutPath;
|
2026-06-02 11:39:30 +08:00
|
|
|
Debug.Log("Stopped Video Capture Mode!--path"+path);
|
2026-05-27 00:05:32 +08:00
|
|
|
var filename = string.Format("Xreal_Shot_Video_{0}.mp4",
|
|
|
|
|
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString());
|
2026-06-02 11:39:30 +08:00
|
|
|
Debug.Log("Stopped Video Capture Mode!--path"+filename);
|
2026-05-27 00:05:32 +08:00
|
|
|
await DelayInsertVideoToGallery(path, filename, "Record");
|
|
|
|
|
OnStoppedRecordingVideoAction?.Invoke(path);
|
|
|
|
|
// Release video capture resource.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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!");
|
|
|
|
|
|
2026-06-02 11:39:30 +08:00
|
|
|
_videoCapture.StartRecordingAsync(VideoSavePath, (a) =>
|
|
|
|
|
{
|
|
|
|
|
OnStartedRecordingVideo?.Invoke();
|
|
|
|
|
if (!a.success)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log("Started Recording Video Faild!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Debug.Log("Started Recording Video!");
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-27 00:05:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IArchitecture GetArchitecture()
|
|
|
|
|
{
|
|
|
|
|
return MainArchitecture.Interface;
|
|
|
|
|
}
|
2026-06-03 22:05:03 +08:00
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_videoCapture.StopVideoModeAsync((result) =>
|
|
|
|
|
{
|
|
|
|
|
if (!result.success)
|
|
|
|
|
{
|
|
|
|
|
Debug.Log("Stopped Video Capture Mode faild!");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_videoCapture?.Dispose();
|
|
|
|
|
_videoCapture = null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
2026-05-27 00:05:32 +08:00
|
|
|
}
|