Files
2026-07-13 13:13:17 +08:00

156 lines
6.9 KiB
C#

using T3.Core.Animation;
using T3.Core.Operator.Interfaces;
using T3.Core.Resource.Assets;
using T3.Core.Utils;
using T3.Core.Video;
using T3.VideoServices;
namespace Lib.io.video;
/// <summary>
/// Implemented by [VideoClip] so the [VideoClipPlayer] compositor ([_ProcessVideoClips]) can read each clip's
/// per-clip compositing params. The clip's TimeClip / LayerIndex come from its TimeClipSlot output instead.
/// </summary>
internal interface IVideoClipProvider
{
Slot<Texture2D> TextureOutput { get; }
InputSlot<Vector4> ColorInput { get; }
InputSlot<int> BlendModeInput { get; }
/// <summary>Called by a [VideoClipPlayer] each frame for every clip it manages, so an unmanaged clip can hint.</summary>
void MarkManaged();
}
[Guid("04c1a6dc-3042-48a8-81d2-0a5a162016dc")]
internal sealed class VideoClip : Instance<VideoClip>, IStatusProvider, IVideoClipProvider, IContentTimeClip, IDescriptiveFilename
{
[Output(Guid = "eb954aeb-535b-4b22-ac49-858f71bdaac4", DirtyFlagTrigger = DirtyFlagTrigger.Animated)]
public readonly Slot<Texture2D> Texture = new();
[Output(Guid = "30357595-0893-47F8-8BCA-22DD77275768", DirtyFlagTrigger = DirtyFlagTrigger.Always)]
public readonly TimeClipSlot<Command> TimeSlot = new();
public VideoClip()
{
Texture.UpdateAction += Update;
TimeSlot.UpdateAction += Update;
}
private void Update(EvaluationContext context)
{
Command.GetValue(context);
var relativePath = Path.GetValue(context);
if (!AssetRegistry.TryResolveAddress(relativePath, this, out var absolutePath, out _))
{
_statusMessage = "Can't find video " + relativePath;
Texture.DirtyFlag.Clear();
return;
}
// Map the timeline position into the clip's source time, applying the per-clip playback rate.
var timeRange = TimeSlot.TimeClip.TimeRange;
var sourceRange = TimeSlot.TimeClip.SourceRange;
var barsInSeconds = context.LocalTime - timeRange.Start;
if (timeRange.End != timeRange.Start)
{
var rate = (sourceRange.End - sourceRange.Start) / (timeRange.End - timeRange.Start);
barsInSeconds *= rate;
}
barsInSeconds += sourceRange.Start;
var sourceTimeInSecs = context.Playback.SecondsFromBars(barsInSeconds);
// Clamp to the clip's source range so times outside the clip resolve to its first/last frame; the
// controller additionally clamps to the video's real duration.
var sourceStart = context.Playback.SecondsFromBars(sourceRange.Start);
var sourceEnd = context.Playback.SecondsFromBars(sourceRange.End);
var clampedTime = Math.Clamp(sourceTimeInSecs, Math.Min(sourceStart, sourceEnd), Math.Max(sourceStart, sourceEnd));
var result = VideoPlaybackEngine.Instance.RequestFrame(_streamId, absolutePath, clampedTime,
loop: false, context.Playback.IsRenderingToFile,
(VideoPlaybackOptimization)OptimizeFor.GetValue(context));
_statusMessage = result.ErrorMessage;
Texture.Value = result.Texture;
// During export, make the renderer wait for this clip's exact frame — but only while it's actually
// active. The player also pulls clips ahead of their cut to pre-warm the decoder, and those must not
// stall export waiting for a frame that isn't on screen yet.
if (context.Playback.IsRenderingToFile && context.LocalTime >= timeRange.Start && context.LocalTime < timeRange.End)
Playback.OpNotReady |= !result.IsReady;
Texture.DirtyFlag.Clear();
}
protected override void Dispose(bool isDisposing)
{
if (!isDisposing)
return;
VideoPlaybackEngine.Instance.ReleaseStream(_streamId);
}
public IStatusProvider.StatusLevel GetStatusLevel()
{
if (!string.IsNullOrEmpty(_statusMessage))
return IStatusProvider.StatusLevel.Error;
return IsManagedByAPlayer ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Notice;
}
public string GetStatusMessage()
{
if (!string.IsNullOrEmpty(_statusMessage))
return _statusMessage;
return IsManagedByAPlayer ? null : "Not drawn by any [VideoClipPlayer] — wire it into one or enable the player's AutoCollect.";
}
Slot<Texture2D> IVideoClipProvider.TextureOutput => Texture;
InputSlot<Vector4> IVideoClipProvider.ColorInput => Color;
InputSlot<int> IVideoClipProvider.BlendModeInput => BlendMode;
// Drives the timeline clip label (filename, or "RenamedName (file)") instead of the op's symbol name.
InputSlot<string> IDescriptiveFilename.SourcePathSlot => Path;
// A [VideoClipPlayer] stamps the current frame on every clip it manages — wired or auto-collected, and even
// while the clip sits between its in/out points. A clip no player has stamped for a couple of frames is
// drawn by nobody, so its status hints at that instead of silently showing nothing.
void IVideoClipProvider.MarkManaged() => _lastManagedFrame = Playback.FrameCount;
private bool IsManagedByAPlayer => Playback.FrameCount - _lastManagedFrame <= ManagedFrameSlack;
private const int ManagedFrameSlack = 2;
private readonly Guid _streamId = Guid.NewGuid();
private string _statusMessage;
private int _lastManagedFrame;
// Input parameters
[Input(Guid = "10c311ee-6426-463a-a1fe-cfac6de04224")]
public readonly InputSlot<Command> Command = new();
[Input(Guid = "31721e18-556b-452b-a8aa-18dbd44af74d")]
public readonly InputSlot<string> Path = new();
// Audio is silent in this milestone (BASS routing is backlog); kept for graph compatibility.
[Input(Guid = "28f27625-37fe-409a-b6c1-d4eabf6c1eb8")]
public readonly InputSlot<float> Volume = new();
// No longer used: FFmpeg gives direct PTS control, so there is no resync threshold.
[Input(Guid = "5EB10090-AE6A-4AE7-9FBD-5BD9FFD13B1B")]
public readonly InputSlot<float> ResyncThreshold = new();
// Per-clip compositing params, read by [VideoClipPlayer] when stacking active clips. Color is tint + alpha
// (alpha = opacity); it rides context.ForegroundColor into the composite.
[Input(Guid = "7fb1d490-6c9b-4380-b88c-800d44c16475")]
public readonly InputSlot<Vector4> Color = new(Vector4.One);
[Input(Guid = "27f02af3-06f3-4cb2-8731-2e8777634275", MappedType = typeof(SharedEnums.BlendModes))]
public readonly InputSlot<int> BlendMode = new();
// Fast Seeking (default) decodes in software with a RAM cache for snappy scrub-back and smooth HD playback;
// Playback Performance decodes zero-copy on the GPU for the smoothest large/4K playback.
[Input(Guid = "f7e8f5f1-3333-409f-92da-573b1f32c0d6", MappedType = typeof(VideoPlaybackOptimization))]
public readonly InputSlot<int> OptimizeFor = new();
}