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; /// /// 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. /// internal interface IVideoClipProvider { Slot TextureOutput { get; } InputSlot ColorInput { get; } InputSlot BlendModeInput { get; } /// Called by a [VideoClipPlayer] each frame for every clip it manages, so an unmanaged clip can hint. void MarkManaged(); } [Guid("04c1a6dc-3042-48a8-81d2-0a5a162016dc")] internal sealed class VideoClip : Instance, IStatusProvider, IVideoClipProvider, IContentTimeClip, IDescriptiveFilename { [Output(Guid = "eb954aeb-535b-4b22-ac49-858f71bdaac4", DirtyFlagTrigger = DirtyFlagTrigger.Animated)] public readonly Slot Texture = new(); [Output(Guid = "30357595-0893-47F8-8BCA-22DD77275768", DirtyFlagTrigger = DirtyFlagTrigger.Always)] public readonly TimeClipSlot 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 IVideoClipProvider.TextureOutput => Texture; InputSlot IVideoClipProvider.ColorInput => Color; InputSlot IVideoClipProvider.BlendModeInput => BlendMode; // Drives the timeline clip label (filename, or "RenamedName (file)") instead of the op's symbol name. InputSlot 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 = new(); [Input(Guid = "31721e18-556b-452b-a8aa-18dbd44af74d")] public readonly InputSlot 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 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 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 Color = new(Vector4.One); [Input(Guid = "27f02af3-06f3-4cb2-8731-2e8777634275", MappedType = typeof(SharedEnums.BlendModes))] public readonly InputSlot 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 OptimizeFor = new(); }