/*---------------------------------------------------------------- // 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 class LeviathanVideoDecoder : LeviathanVideoDecoderBase { /// 注入的共用材质;非空时 RawImage 使用该实例并由此路写 Y/U/V。 private Material _sharedMaterialOverride; private LeviathanVideoSharedMaterialController _sharedMaterialControllerOwner; 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 OnDestroy() { var sharedOwner = _sharedMaterialControllerOwner; _sharedMaterialControllerOwner = null; _sharedMaterialOverride = null; if (sharedOwner != null) sharedOwner.NotifyDecoderReleased(this); base.OnDestroy(); } /// /// 绑定到共用材质控制器:本解码器的 RawImage 将使用 ,由本路在刷新时写入 Y/U/V。 /// public void BindToSharedMaterialController(LeviathanVideoSharedMaterialController owner, Material sharedMaterial) { _sharedMaterialControllerOwner = owner; _sharedMaterialOverride = sharedMaterial; } /// /// 解除共用材质绑定,恢复使用本组件自带的按 Alpha 类型缓存材质(若仍在播放会立即重绑贴图)。 /// public void UnbindSharedMaterialController() { var sharedOwner = _sharedMaterialControllerOwner; _sharedMaterialControllerOwner = null; _sharedMaterialOverride = null; if (sharedOwner != null) sharedOwner.NotifyDecoderReleased(this); var rawImage = GetComponent(); if (rawImage == null) return; if (IsPlaying && _textures != null && _textures.Length > 0 && _textures[0] != null) { rawImage.texture = _textures[0]; UpdateImageMaterial(rawImage); } else { rawImage.material = null; } } /// /// 在绑定共用材质后,用当前纹理重新写一遍 RawImage 与材质上的贴图槽(切换激活路或 Play 之后可调用)。 /// public void RefreshSharedMaterialDisplay() { var rawImage = GetComponent(); if (rawImage == null || _sharedMaterialOverride == null) return; if (_textures == null || _textures.Length < 3 || _textures[0] == null) return; rawImage.texture = _textures[0]; UpdateImageMaterial(rawImage); } /// /// 子类在材质创建并绑定 Y/U/V 纹理后可覆写(例如写入序列化 ChromaKey 参数)。 /// /// 已赋给 RawImage 的材质实例 protected virtual void OnRawImageMaterialInstanceReady(Material mat) { } /// /// 启用渲染目标 /// 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 override void OnTexturesUploadedToGpu() { var rawImage = GetComponent(); if (rawImage == null || _textures == null || _textures.Length < 3) return; rawImage.texture = _textures[0]; var mat = _sharedMaterialOverride != null ? _sharedMaterialOverride : rawImage.material; if (mat != null) { mat.SetTexture(UTexIdx, _textures[1]); mat.SetTexture(VTexIdx, _textures[2]); } rawImage.SetVerticesDirty(); rawImage.SetMaterialDirty(); } /// /// 按视频有效宽高与 Alpha 类型更新 RawImage 尺寸与 UV。 /// protected void UpdateImageSize(RawImage rawImage) { if (!disableAutoSize) { if (VideoValidWidth > 0 && VideoHeight > 0) { rawImage.SetNativeSize(); rawImage.rectTransform.sizeDelta = _alphaType switch { PlayAlphaType.RightSide => new Vector2(VideoValidWidth * 0.5f, VideoHeight), PlayAlphaType.BottomSide => new Vector2(VideoValidWidth, VideoHeight * 0.5f), _ => new Vector2(VideoValidWidth, VideoHeight) }; } } var vw = VideoWidth > 0 ? VideoWidth : 1; rawImage.uvRect = new Rect(0, 0, _alphaType == PlayAlphaType.RightSide ? 1 : (float)VideoValidWidth / vw, 1); } /// /// 按 Alpha 类型创建材质并绑定 U/V 纹理;若已绑定共用材质则只更新该实例上的参数与贴图。 /// protected void UpdateImageMaterial(RawImage rawImage) { if (_textures == null || _textures.Length < 3) return; Material mat; if (_sharedMaterialOverride != null) { mat = _sharedMaterialOverride; } else { // 软件解码器使用YUV纹理,需要YUV420P着色器 switch (_alphaType) { case PlayAlphaType.RightSide: mat = new Material(Shader.Find("LeviathanVideo/YUV420P_Alpha_Right")); mat.SetFloat(ValidWidthRatioIdx, VideoWidth > 0 ? (float)VideoValidWidth / VideoWidth : 1f); break; case PlayAlphaType.BottomSide: mat = new Material(Shader.Find("LeviathanVideo/YUV420P_Alpha_Bottom")); break; case PlayAlphaType.ChromaKey: mat = new Material(Shader.Find("LeviathanVideo/YUV420P_ChromaKey")); break; case PlayAlphaType.None: default: mat = new Material(Shader.Find("LeviathanVideo/YUV420P")); break; } } if (_sharedMaterialOverride != null && _alphaType == PlayAlphaType.RightSide) mat.SetFloat(ValidWidthRatioIdx, VideoWidth > 0 ? (float)VideoValidWidth / VideoWidth : 1f); mat.SetTexture(UTexIdx, _textures[1]); mat.SetTexture(VTexIdx, _textures[2]); rawImage.material = mat; OnRawImageMaterialInstanceReady(mat); } #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