Files
ft/Client/LocalPackages/LeviathanVideo/Runtime/LeviathanVideoDecoder.cs
2026-06-29 21:18:33 +08:00

479 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*----------------------------------------------------------------
// 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
/// <summary>
/// UI RawImage 视频渲染组件
/// 继承自 LeviathanVideoDecoderBase用于在 UI RawImage 上显示视频
/// </summary>
[RequireComponent(typeof(RawImage))]
public class LeviathanVideoDecoder : LeviathanVideoDecoderBase
{
/// <summary>由 <see cref="LeviathanVideoSharedMaterialController"/> 注入的共用材质;非空时 RawImage 使用该实例并由此路写 Y/U/V。</summary>
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<RawImage>();
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();
}
/// <summary>
/// 绑定到共用材质控制器:本解码器的 RawImage 将使用 <paramref name="sharedMaterial"/>,由本路在刷新时写入 Y/U/V。
/// </summary>
public void BindToSharedMaterialController(LeviathanVideoSharedMaterialController owner, Material sharedMaterial)
{
_sharedMaterialControllerOwner = owner;
_sharedMaterialOverride = sharedMaterial;
}
/// <summary>
/// 解除共用材质绑定,恢复使用本组件自带的按 Alpha 类型缓存材质(若仍在播放会立即重绑贴图)。
/// </summary>
public void UnbindSharedMaterialController()
{
var sharedOwner = _sharedMaterialControllerOwner;
_sharedMaterialControllerOwner = null;
_sharedMaterialOverride = null;
if (sharedOwner != null)
sharedOwner.NotifyDecoderReleased(this);
var rawImage = GetComponent<RawImage>();
if (rawImage == null)
return;
if (IsPlaying && _textures != null && _textures.Length > 0 && _textures[0] != null)
{
rawImage.texture = _textures[0];
UpdateImageMaterial(rawImage);
}
else
{
rawImage.material = null;
}
}
/// <summary>
/// 在绑定共用材质后,用当前纹理重新写一遍 RawImage 与材质上的贴图槽(切换激活路或 Play 之后可调用)。
/// </summary>
public void RefreshSharedMaterialDisplay()
{
var rawImage = GetComponent<RawImage>();
if (rawImage == null || _sharedMaterialOverride == null)
return;
if (_textures == null || _textures.Length < 3 || _textures[0] == null)
return;
rawImage.texture = _textures[0];
UpdateImageMaterial(rawImage);
}
/// <summary>
/// 子类在材质创建并绑定 Y/U/V 纹理后可覆写(例如写入序列化 ChromaKey 参数)。
/// </summary>
/// <param name="mat">已赋给 RawImage 的材质实例</param>
protected virtual void OnRawImageMaterialInstanceReady(Material mat)
{
}
/// <summary>
/// 启用渲染目标
/// </summary>
protected override void OnEnableRenderTarget()
{
RawImage rawImage = GetComponent<RawImage>();
if (rawImage)
{
rawImage.enabled = true;
}
}
/// <summary>
/// 禁用渲染目标
/// </summary>
protected override void OnDisableRenderTarget()
{
RawImage rawImage = GetComponent<RawImage>();
if (rawImage)
{
rawImage.enabled = false;
}
}
/// <summary>
/// 更新渲染目标尺寸
/// </summary>
protected override void OnUpdateRenderTargetSize()
{
RawImage rawImage = GetComponent<RawImage>();
if (rawImage)
{
UpdateImageSize(rawImage);
}
}
/// <summary>
/// 初始化材质
/// </summary>
protected override void OnInitMaterial()
{
RawImage rawImage = GetComponent<RawImage>();
if (rawImage)
{
rawImage.texture = _textures[0];
UpdateImageMaterial(rawImage);
}
}
/// <summary>
/// 更新材质
/// </summary>
protected override void OnUpdateMaterial()
{
RawImage rawImage = GetComponent<RawImage>();
if (rawImage)
{
UpdateImageMaterial(rawImage);
}
}
protected override void OnTexturesUploadedToGpu()
{
var rawImage = GetComponent<RawImage>();
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();
}
/// <summary>
/// 按视频有效宽高与 Alpha 类型更新 RawImage 尺寸与 UV。
/// </summary>
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);
}
/// <summary>
/// 按 Alpha 类型创建材质并绑定 U/V 纹理;若已绑定共用材质则只更新该实例上的参数与贴图。
/// </summary>
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<RawImage>();
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