/*----------------------------------------------------------------
// 基于 Leviathan 的 RawImage 视频解码扩展:ChromaKey 参数可序列化、编辑器首帧预览
//----------------------------------------------------------------*/
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
#endif
///
/// UI RawImage 视频解码(ZZVideo):持久化 ChromaKey、编辑器预览首帧
///
/// 编辑器下使用 (仅 UNITY_EDITOR 编译),以便 触发自动预览;运行时构建无此特性,与默认 MonoBehaviour 一致。
#if UNITY_EDITOR
[ExecuteAlways]
#endif
[RequireComponent(typeof(RawImage))]
public class ZZVideoRawImageDecoder : LeviathanVideoDecoder
{
[SerializeField]
[Tooltip("仅在 AlphaType 为 ChromaKey 时使用")]
private ZZVideoChromaKeyData _chromaKeyData = new ZZVideoChromaKeyData();
/// 播放速度(1 为正常)。继承自基类序列化字段,可在 Inspector 与代码中修改。
public float PlaybackSpeed
{
get => base.PlaySpeed;
set => SetPlaySpeed(value);
}
/// 代码侧设置播放速度(与 / 基类 等价)。
public void SetPlaybackSpeed(float speed) => SetPlaySpeed(speed);
/// 获取当前播放速度倍率。
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();
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);
///
/// 使用当前 bytesFile / AlphaType 开始播放(Leviathan 基类 Play() 为 internal,跨程序集需由此入口调用)
///
public void PlayVideoFromSerialized()
{
if (!Application.isPlaying) return;
if (bytesFile == null) return;
PlayVideo(bytesFile, _alphaType, true);
}
#if UNITY_EDITOR
private bool _editorAutoPreviewFlushRegistered;
private bool _editorValidateFlushRegistered;
/// 编辑模式下不跑基类 Update/LateUpdate,避免 ExecuteAlways 下每帧进解码逻辑;运行时照常。
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();
}
///
/// 合并到下一编辑器帧执行一次首帧预览,避免 OnEnable/OnValidate/Inspector 重复解码。
///
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;
///
/// 编辑模式下解码并显示首帧(单线程)。自动调度走 false 时会按资源与时间去重,避免闪烁。
///
/// 为 true 时跳过去重(Inspector「强制刷新首帧预览」)。
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.01~16。"));
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