156 lines
6.9 KiB
C#
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();
|
|
}
|