479 lines
16 KiB
C#
479 lines
16 KiB
C#
/*----------------------------------------------------------------
|
||
// 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
|