Files
ft/Client/Assets/Scripts/ZZVideo/ZZVideoRawImageDecoder.cs
2026-06-29 21:18:33 +08:00

414 lines
15 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.
/*----------------------------------------------------------------
// 基于 Leviathan 的 RawImage 视频解码扩展ChromaKey 参数可序列化、编辑器首帧预览
//----------------------------------------------------------------*/
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
/// <summary>
/// UI RawImage 视频解码ZZVideo持久化 ChromaKey、编辑器预览首帧
/// </summary>
/// <remarks>编辑器下使用 <see cref="ExecuteAlways"/>(仅 UNITY_EDITOR 编译),以便 <see cref="OnEnable"/> 触发自动预览;运行时构建无此特性,与默认 MonoBehaviour 一致。</remarks>
#if UNITY_EDITOR
[ExecuteAlways]
#endif
[RequireComponent(typeof(RawImage))]
public class ZZVideoRawImageDecoder : LeviathanVideoDecoder
{
[SerializeField]
[Tooltip("仅在 AlphaType 为 ChromaKey 时使用")]
private ZZVideoChromaKeyData _chromaKeyData = new ZZVideoChromaKeyData();
/// <summary>播放速度1 为正常)。继承自基类序列化字段,可在 Inspector 与代码中修改。</summary>
public float PlaybackSpeed
{
get => base.PlaySpeed;
set => SetPlaySpeed(value);
}
/// <summary>代码侧设置播放速度(与 <see cref="PlaybackSpeed"/> / 基类 <see cref="LeviathanVideoDecoderBase.SetPlaySpeed"/> 等价)。</summary>
public void SetPlaybackSpeed(float speed) => SetPlaySpeed(speed);
/// <summary>获取当前播放速度倍率。</summary>
public float GetPlaybackSpeed() => base.PlaySpeed;
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 new void SetChromaKeyColor(Color color)
{
_chromaKeyData.keyColor = color;
PushChromaKeyToMaterial();
}
public new Color GetChromaKeyColor() => _chromaKeyData.keyColor;
public new void SetChromaKeyCutoff(float value)
{
_chromaKeyData.cutoff = Mathf.Clamp01(value);
PushChromaKeyToMaterial();
}
public new float GetChromaKeyCutoff() => _chromaKeyData.cutoff;
public new void SetChromaKeyColorFeathering(float value)
{
_chromaKeyData.colorFeathering = Mathf.Clamp01(value);
PushChromaKeyToMaterial();
}
public new float GetChromaKeyColorFeathering() => _chromaKeyData.colorFeathering;
public new void SetChromaKeyMaskFeathering(float value)
{
_chromaKeyData.maskFeathering = Mathf.Clamp01(value);
PushChromaKeyToMaterial();
}
public new float GetChromaKeyMaskFeathering() => _chromaKeyData.maskFeathering;
public new void SetChromaKeySharpening(float value)
{
_chromaKeyData.sharpening = Mathf.Clamp01(value);
PushChromaKeyToMaterial();
}
public new float GetChromaKeySharpening() => _chromaKeyData.sharpening;
public new void SetChromaKeyParams(Color keyColor, float cutoff, float colorFeathering, float maskFeathering, float sharpening)
{
_chromaKeyData.keyColor = keyColor;
_chromaKeyData.cutoff = Mathf.Clamp01(cutoff);
_chromaKeyData.colorFeathering = Mathf.Clamp01(colorFeathering);
_chromaKeyData.maskFeathering = Mathf.Clamp01(maskFeathering);
_chromaKeyData.sharpening = Mathf.Clamp01(sharpening);
PushChromaKeyToMaterial();
}
public void PushChromaKeyToMaterial()
{
var mat = ChromaKeyMaterial;
if (mat == null) return;
mat.SetColor(ChromaKeyColorId, _chromaKeyData.keyColor);
mat.SetFloat(ChromaKeyCutoffId, Mathf.Clamp01(_chromaKeyData.cutoff));
mat.SetFloat(ChromaKeyColorFeatheringId, Mathf.Clamp01(_chromaKeyData.colorFeathering));
mat.SetFloat(ChromaKeyMaskFeatheringId, Mathf.Clamp01(_chromaKeyData.maskFeathering));
mat.SetFloat(ChromaKeySharpeningId, Mathf.Clamp01(_chromaKeyData.sharpening));
}
void ApplyChromaKeyToNewMaterial(Material mat)
{
if (mat == null || !mat.HasProperty(ChromaKeyColorId)) return;
mat.SetColor(ChromaKeyColorId, _chromaKeyData.keyColor);
mat.SetFloat(ChromaKeyCutoffId, Mathf.Clamp01(_chromaKeyData.cutoff));
mat.SetFloat(ChromaKeyColorFeatheringId, Mathf.Clamp01(_chromaKeyData.colorFeathering));
mat.SetFloat(ChromaKeyMaskFeatheringId, Mathf.Clamp01(_chromaKeyData.maskFeathering));
mat.SetFloat(ChromaKeySharpeningId, Mathf.Clamp01(_chromaKeyData.sharpening));
}
protected override void OnRawImageMaterialInstanceReady(Material mat)
{
ApplyChromaKeyToNewMaterial(mat);
}
private new static void Log(string message) => LeviathanLogConfig.Log("ZZVideoRawImageDecoder", message);
private new static void LogWarning(string message) => LeviathanLogConfig.LogWarning("ZZVideoRawImageDecoder", message);
private new static void LogError(string message) => LeviathanLogConfig.LogError("ZZVideoRawImageDecoder", message);
/// <summary>
/// 使用当前 bytesFile / AlphaType 开始播放Leviathan 基类 <c>Play()</c> 为 internal跨程序集需由此入口调用
/// </summary>
public void PlayVideoFromSerialized()
{
if (!Application.isPlaying) return;
if (bytesFile == null) return;
PlayVideo(bytesFile, _alphaType, true);
}
#if UNITY_EDITOR
private bool _editorAutoPreviewFlushRegistered;
private bool _editorValidateFlushRegistered;
/// <summary>编辑模式下不跑基类 Update/LateUpdate避免 ExecuteAlways 下每帧进解码逻辑;运行时照常。</summary>
protected override void Update()
{
if (!Application.isPlaying)
return;
base.Update();
}
protected override void LateUpdate()
{
if (!Application.isPlaying)
return;
base.LateUpdate();
}
protected override void OnEnable()
{
base.OnEnable();
if (Application.isPlaying)
return;
RequestEditorAutoPreviewDeferred();
}
protected override void OnDisable()
{
if (Application.isPlaying)
return;
if (_editorValidateFlushRegistered)
{
EditorApplication.delayCall -= EditorValidateFlush;
_editorValidateFlushRegistered = false;
}
if (_editorAutoPreviewFlushRegistered)
{
EditorApplication.delayCall -= EditorAutoPreviewFlush;
_editorAutoPreviewFlushRegistered = false;
}
if (_isPlaying)
InternalStop();
}
/// <summary>
/// 合并到下一编辑器帧执行一次首帧预览,避免 OnEnable/OnValidate/Inspector 重复解码。
/// </summary>
private void RequestEditorAutoPreviewDeferred()
{
if (Application.isPlaying)
return;
if (!enabled || !gameObject.activeInHierarchy)
return;
if (bytesFile == null)
return;
if (_editorAutoPreviewFlushRegistered)
return;
_editorAutoPreviewFlushRegistered = true;
EditorApplication.delayCall += EditorAutoPreviewFlush;
}
private void EditorAutoPreviewFlush()
{
_editorAutoPreviewFlushRegistered = false;
if (!this)
return;
if (Application.isPlaying)
return;
if (!enabled || !gameObject.activeInHierarchy)
return;
if (bytesFile == null)
return;
EditorPreviewFirstFrame(force: false);
}
private void OnValidate()
{
if (Application.isPlaying)
return;
if (_editorValidateFlushRegistered)
return;
_editorValidateFlushRegistered = true;
EditorApplication.delayCall += EditorValidateFlush;
}
private void EditorValidateFlush()
{
_editorValidateFlushRegistered = false;
if (!this)
return;
if (Application.isPlaying)
return;
var currentBytesFile = bytesFile;
var lastBytesFile = _lastBytesFile;
SetAlphaType(_alphaType);
if (_alphaType == PlayAlphaType.ChromaKey)
PushChromaKeyToMaterial();
if (currentBytesFile != lastBytesFile)
{
_lastBytesFile = currentBytesFile;
if (IsPlaying && currentBytesFile != null)
{
Log("编辑器中检测到 bytesFile 变化,自动播放新视频");
if (Application.isPlaying)
{
Stop();
PlayVideoFromSerialized();
}
else
{
InternalStop();
RequestEditorAutoPreviewDeferred();
}
}
else if (currentBytesFile != null && enabled && gameObject.activeInHierarchy)
{
RequestEditorAutoPreviewDeferred();
}
}
}
[System.NonSerialized] private int _editorLastPreviewBytesInstanceId;
[System.NonSerialized] private int _editorLastPreviewAlphaTypeInt;
[System.NonSerialized] private double _editorLastPreviewEditorTime;
private const double EditorAutoPreviewMinIntervalSeconds = 0.35;
/// <summary>
/// 编辑模式下解码并显示首帧(单线程)。自动调度走 <paramref name="force"/> false 时会按资源与时间去重,避免闪烁。
/// </summary>
/// <param name="force">为 true 时跳过去重Inspector「强制刷新首帧预览」。</param>
public void EditorPreviewFirstFrame(bool force = false)
{
if (bytesFile == null)
{
LogWarning("EditorPreviewFirstFrame: 未指定 bytesFile");
return;
}
if (!force)
{
double now = EditorApplication.timeSinceStartup;
int bid = bytesFile.GetInstanceID();
int at = (int)_alphaType;
if (bid == _editorLastPreviewBytesInstanceId && at == _editorLastPreviewAlphaTypeInt &&
now - _editorLastPreviewEditorTime < EditorAutoPreviewMinIntervalSeconds)
return;
}
if (_isPlaying)
InternalStop();
int ret = PlayVideo(bytesFile, _alphaType, multithreadedDecode: false, beginFrameIndex: 1, syncDecodeFirstFrame: true, fastDecode: true);
if (ret != 0)
{
LogError($"EditorPreviewFirstFrame 解码失败,错误码: {ret}");
return;
}
EditorPauseDecoderIgnoringPlayMode();
PushChromaKeyToMaterial();
_editorLastPreviewBytesInstanceId = bytesFile.GetInstanceID();
_editorLastPreviewAlphaTypeInt = (int)_alphaType;
_editorLastPreviewEditorTime = EditorApplication.timeSinceStartup;
}
#endif
}
#if UNITY_EDITOR
[CustomEditor(typeof(ZZVideoRawImageDecoder))]
public class ZZVideoRawImageDecoderEditor : Editor
{
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}";
}
private static string FormatTimeMicroseconds(long microseconds) => FormatTimeSeconds(microseconds / 1000000.0);
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawPropertiesExcluding(serializedObject, "m_Script", "_chromaKeyData", "_playSpeed");
var mono = (ZZVideoRawImageDecoder)target;
EditorGUILayout.PropertyField(serializedObject.FindProperty("_chromaKeyData"), new GUIContent("ChromaKey 参数"), true);
EditorGUILayout.Space();
{
var speedProp = serializedObject.FindProperty("_playSpeed");
if (speedProp != null)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel(new GUIContent("播放速度", "1 为正常;与 Time.deltaTime 相乘控制进帧快慢。范围 0.0116。"));
EditorGUI.BeginChangeCheck();
float v = speedProp.floatValue;
v = GUILayout.HorizontalSlider(v, 0.25f, 4f);
v = EditorGUILayout.DelayedFloatField(v, GUILayout.Width(52));
EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck())
speedProp.floatValue = Mathf.Clamp(v, 0.01f, 16f);
}
}
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(mono.bytesFile == null))
{
if (GUILayout.Button("强制刷新首帧预览"))
{
Undo.RecordObject(mono, "ZZVideo 刷新首帧预览");
mono.EditorPreviewFirstFrame(force: true);
EditorUtility.SetDirty(mono);
}
}
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextField("帧数", $"{mono.PlayFrameIndex} / {mono.FrameCount}");
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;
bool newValue = EditorGUILayout.Toggle("isPlaying", oldValue);
if (oldValue != newValue)
{
if (newValue)
mono.PlayVideoFromSerialized();
else
mono.Stop();
EditorUtility.SetDirty(target);
}
}
{
bool oldValue = mono.IsPause;
bool newValue = EditorGUILayout.Toggle("isPause", oldValue);
if (oldValue != newValue)
{
if (newValue)
mono.Pause();
else
mono.Resume();
EditorUtility.SetDirty(target);
}
}
if (serializedObject.ApplyModifiedProperties())
{
mono.PushChromaKeyToMaterial();
EditorUtility.SetDirty(mono);
}
Repaint();
}
}
#endif