/*---------------------------------------------------------------- // Copyright (C) 2025 Beijing All rights reserved. // // Author: huachangmiao // Create Date: 2025/04/11 // Module Describe: UI RawImage 视频渲染组件 //----------------------------------------------------------------*/ using UnityEngine; using UnityEngine.UI; #if UNITY_EDITOR using UnityEditor; #endif /// /// UI RawImage 视频渲染组件 /// 继承自 LeviathanVideoDecoderBase,用于在 UI RawImage 上显示视频 /// /// [RequireComponent(typeof(RawImage))] public unsafe class LeviathanVideoDecoder : LeviathanVideoDecoderBase { private static readonly int ChromaKeyColorId = Shader.PropertyToID("_KeyColor"); private static readonly int ChromaKeyCutoffId = Shader.PropertyToID("_ColorCutoff"); private static readonly int ChromaKeyColorFeatheringId = Shader.PropertyToID("_ColorFeathering"); private static readonly int ChromaKeyMaskFeatheringId = Shader.PropertyToID("_MaskFeathering"); private static readonly int ChromaKeySharpeningId = Shader.PropertyToID("_Sharpening"); private Material ChromaKeyMaterial { get { if (_alphaType != PlayAlphaType.ChromaKey) return null; var rawImage = GetComponent(); if (rawImage == null || rawImage.material == null) return null; return rawImage.material.HasProperty(ChromaKeyColorId) ? rawImage.material : null; } } public void SetChromaKeyColor(Color color) { var mat = ChromaKeyMaterial; if (mat != null) mat.SetColor(ChromaKeyColorId, color); } public Color GetChromaKeyColor() { var mat = ChromaKeyMaterial; return mat != null ? mat.GetColor(ChromaKeyColorId) : Color.green; } public void SetChromaKeyCutoff(float value) { var mat = ChromaKeyMaterial; if (mat != null) mat.SetFloat(ChromaKeyCutoffId, Mathf.Clamp01(value)); } public float GetChromaKeyCutoff() { var mat = ChromaKeyMaterial; return mat != null ? mat.GetFloat(ChromaKeyCutoffId) : 0.2f; } public void SetChromaKeyColorFeathering(float value) { var mat = ChromaKeyMaterial; if (mat != null) mat.SetFloat(ChromaKeyColorFeatheringId, Mathf.Clamp01(value)); } public float GetChromaKeyColorFeathering() { var mat = ChromaKeyMaterial; return mat != null ? mat.GetFloat(ChromaKeyColorFeatheringId) : 0.33f; } public void SetChromaKeyMaskFeathering(float value) { var mat = ChromaKeyMaterial; if (mat != null) mat.SetFloat(ChromaKeyMaskFeatheringId, Mathf.Clamp01(value)); } public float GetChromaKeyMaskFeathering() { var mat = ChromaKeyMaterial; return mat != null ? mat.GetFloat(ChromaKeyMaskFeatheringId) : 1f; } public void SetChromaKeySharpening(float value) { var mat = ChromaKeyMaterial; if (mat != null) mat.SetFloat(ChromaKeySharpeningId, Mathf.Clamp01(value)); } public float GetChromaKeySharpening() { var mat = ChromaKeyMaterial; return mat != null ? mat.GetFloat(ChromaKeySharpeningId) : 0.5f; } public void SetChromaKeyParams(Color keyColor, float cutoff, float colorFeathering, float maskFeathering, float sharpening) { var mat = ChromaKeyMaterial; if (mat == null) return; mat.SetColor(ChromaKeyColorId, keyColor); mat.SetFloat(ChromaKeyCutoffId, Mathf.Clamp01(cutoff)); mat.SetFloat(ChromaKeyColorFeatheringId, Mathf.Clamp01(colorFeathering)); mat.SetFloat(ChromaKeyMaskFeatheringId, Mathf.Clamp01(maskFeathering)); mat.SetFloat(ChromaKeySharpeningId, Mathf.Clamp01(sharpening)); } private new static void Log(string message) { LeviathanLogConfig.Log("VideoDecoder", message); } private new static void LogWarning(string message) { LeviathanLogConfig.LogWarning("VideoDecoder", message); } private new static void LogError(string message) { LeviathanLogConfig.LogError("VideoDecoder", message); } /// /// 启用渲染目标 /// protected override void OnEnableRenderTarget() { RawImage rawImage = GetComponent(); if (rawImage) { rawImage.enabled = true; } } /// /// 禁用渲染目标 /// protected override void OnDisableRenderTarget() { RawImage rawImage = GetComponent(); if (rawImage) { rawImage.enabled = false; } } /// /// 更新渲染目标尺寸 /// protected override void OnUpdateRenderTargetSize() { RawImage rawImage = GetComponent(); if (rawImage) { UpdateImageSize(rawImage); } } /// /// 初始化材质 /// protected override void OnInitMaterial() { RawImage rawImage = GetComponent(); if (rawImage) { rawImage.texture = _textures[0]; UpdateImageMaterial(rawImage); } } /// /// 更新材质 /// protected override void OnUpdateMaterial() { RawImage rawImage = GetComponent(); if (rawImage) { UpdateImageMaterial(rawImage); } } protected void UpdateImageSize(RawImage rawImage) { if (!disableAutoSize) { var videoFrame = VideoFrame; if (videoFrame != null && videoFrame->width > 0) { rawImage.SetNativeSize(); rawImage.rectTransform.sizeDelta = _alphaType switch { PlayAlphaType.RightSide => new Vector2(videoFrame->width * 0.5f, videoFrame->height), PlayAlphaType.BottomSide => new Vector2(videoFrame->width, videoFrame->height * 0.5f), _ => new Vector2(videoFrame->width, videoFrame->height) }; } } rawImage.uvRect = new Rect(0, 0, _alphaType == PlayAlphaType.RightSide ? 1 : (float)_videoValidWidth / _videoWidth, 1); } protected void UpdateImageMaterial(RawImage rawImage) { // 软件解码器使用YUV纹理,需要YUV420P着色器 Material newMaterial; switch (_alphaType) { case PlayAlphaType.RightSide: newMaterial = new Material(Shader.Find("LeviathanVideo/YUV420P_Alpha_Right")); newMaterial.SetFloat(ValidWidthRatioIdx, (float)_videoValidWidth / _videoWidth); break; case PlayAlphaType.BottomSide: newMaterial = new Material(Shader.Find("LeviathanVideo/YUV420P_Alpha_Bottom")); break; case PlayAlphaType.ChromaKey: newMaterial = new Material(Shader.Find("LeviathanVideo/YUV420P_ChromaKey")); break; case PlayAlphaType.None: default: newMaterial = new Material(Shader.Find("LeviathanVideo/YUV420P")); break; } newMaterial.SetTexture(UTexIdx, _textures[1]); newMaterial.SetTexture(VTexIdx, _textures[2]); rawImage.material = newMaterial; } #if UNITY_EDITOR private void OnValidate() { // 使用 delayCall 避免在 OnValidate 中调用 SendMessage 相关操作 var currentBytesFile = bytesFile; var lastBytesFile = _lastBytesFile; UnityEditor.EditorApplication.delayCall += () => { if (this == null) return; SetAlphaType(_alphaType); // 检测 bytesFile 是否发生变化 if (currentBytesFile != lastBytesFile) { _lastBytesFile = currentBytesFile; if (IsPlaying && currentBytesFile != null) { Log($"编辑器中检测到 bytesFile 变化,自动播放新视频"); Stop(); Play(); } } }; } #endif } #if UNITY_EDITOR [CustomEditor(typeof(LeviathanVideoDecoder))] public class LeviathanVideoDecoderEditor : Editor { // 格式化时间:秒转换为 HH:MM:SS.mmm 格式 private static string FormatTimeSeconds(double totalSeconds) { int hours = (int)(totalSeconds / 3600); int minutes = (int)((totalSeconds % 3600) / 60); int seconds = (int)(totalSeconds % 60); int milliseconds = (int)((totalSeconds % 1) * 1000); return $"{hours:D2}:{minutes:D2}:{seconds:D2}.{milliseconds:D3}"; } // 格式化时间:微秒转换为 HH:MM:SS.mmm 格式 private static string FormatTimeMicroseconds(long microseconds) { return FormatTimeSeconds(microseconds / 1000000.0); } public override void OnInspectorGUI() { DrawDefaultInspector(); var mono = (LeviathanVideoDecoder)target; // Display video dimensions as read-only fields EditorGUI.BeginDisabledGroup(true); EditorGUILayout.TextField("帧数", $"{mono.PlayFrameIndex} / {mono.FrameCount}"); // FrameInterval 单位是秒,Duration 单位是微秒 EditorGUILayout.TextField("播放时间", FormatTimeSeconds(mono.PlayFrameIndex * mono.FrameInterval)); EditorGUILayout.TextField("总时间", FormatTimeMicroseconds(mono.Duration)); EditorGUILayout.TextField("FPS", mono.FrameInterval > 0 ? $"{1.0 / mono.FrameInterval:F2}" : "0"); EditorGUILayout.TextField("纹理尺寸", $"{mono.VideoWidth} × {mono.VideoHeight}"); EditorGUILayout.TextField("视频有效尺寸", $"{mono.VideoValidWidth} × {mono.VideoHeight}"); EditorGUI.EndDisabledGroup(); { bool oldValue = mono.IsPlaying; // 绘制checkbox bool newValue = EditorGUILayout.Toggle("isPlaying", oldValue); // 如果用户切换了checkbox if (oldValue != newValue) { if (newValue) { mono.Play(); } else { mono.Stop(); } EditorUtility.SetDirty(target); } } { bool oldValue = mono.IsPause; // 绘制checkbox bool newValue = EditorGUILayout.Toggle("isPause", oldValue); // 如果用户切换了checkbox if (oldValue != newValue) { if (newValue) { mono.Pause(); } else { mono.Resume(); } EditorUtility.SetDirty(target); } } if (mono.AlphaType == LeviathanVideoDecoderBase.PlayAlphaType.ChromaKey) { DrawChromaKeyParameters(mono); } this.Repaint(); } private void DrawChromaKeyParameters(LeviathanVideoDecoder mono) { var rawImage = mono.GetComponent(); if (rawImage == null || rawImage.material == null) return; if (!rawImage.material.HasProperty(Shader.PropertyToID("_KeyColor"))) return; EditorGUILayout.Space(); EditorGUILayout.LabelField("ChromaKey 参数", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); Color keyColor = EditorGUILayout.ColorField("KeyColor", mono.GetChromaKeyColor()); float cutoff = EditorGUILayout.Slider("Cutoff", mono.GetChromaKeyCutoff(), 0f, 1f); float colorFeathering = EditorGUILayout.Slider("ColorFeathering", mono.GetChromaKeyColorFeathering(), 0f, 1f); float maskFeathering = EditorGUILayout.Slider("MaskFeathering", mono.GetChromaKeyMaskFeathering(), 0f, 1f); float sharpening = EditorGUILayout.Slider("Sharpening", mono.GetChromaKeySharpening(), 0f, 1f); if (EditorGUI.EndChangeCheck()) { mono.SetChromaKeyParams(keyColor, cutoff, colorFeathering, maskFeathering, sharpening); } } } #endif