// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; using System; using System.Collections.Generic; using System.IO; using JetBrains.Annotations; using osu.Framework.Graphics.Animations; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Graphics.Shaders; using osuTK; namespace osu.Framework.Graphics.Video { /// /// Represents a composite that displays a video played back from a stream or a file. /// public class Video : AnimationClockComposite { /// /// Whether this video is in a buffering state, waiting on decoder or underlying stream. /// public bool Buffering { get; private set; } /// /// True if the video should loop after finishing its playback, false otherwise. /// public override bool Loop { get => base.Loop; set { if (decoder != null) decoder.Looping = value; base.Loop = value; } } /// /// Whether the video decoding process has faulted. /// public bool IsFaulted => decoder?.IsFaulted ?? false; /// /// The current state of the decoding process. /// public VideoDecoder.DecoderState State => decoder?.State ?? VideoDecoder.DecoderState.Ready; internal double CurrentFrameTime => lastFrame?.Time ?? 0; internal int AvailableFrames => availableFrames.Count; private VideoDecoder decoder; private readonly Stream stream; private readonly Queue availableFrames = new Queue(); private DecodedFrame lastFrame; /// /// The total number of frames processed by this instance. /// public int FramesProcessed { get; private set; } /// /// The length in milliseconds that the decoder can be out of sync before a seek is automatically performed. /// private const float lenience_before_seek = 2500; private bool isDisposed; internal VideoSprite Sprite; /// /// YUV->RGB conversion matrix based on the video colorspace /// public Matrix3 ConversionMatrix => decoder.GetConversionMatrix(); /// /// Creates a new . /// /// The video file. /// Whether the current clock time should be assumed as the 0th video frame. public Video(string filename, bool startAtCurrentTime = true) : this(File.OpenRead(filename), startAtCurrentTime) { } public override Drawable CreateContent() => Sprite = new VideoSprite(this) { RelativeSizeAxes = Axes.Both }; /// /// Creates a new . /// /// The video file stream. /// Whether the current clock time should be assumed as the 0th video frame. public Video([NotNull] Stream stream, bool startAtCurrentTime = true) : base(startAtCurrentTime) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); } [BackgroundDependencyLoader] private void load(GameHost gameHost, ShaderManager shaders) { decoder = gameHost.CreateVideoDecoder(stream); decoder.Looping = Loop; decoder.StartDecoding(); Duration = decoder.Duration; } protected override void Update() { base.Update(); if (decoder.State == VideoDecoder.DecoderState.EndOfStream) { // if at the end of the stream but our playback enters a valid time region again, a seek operation is required to get the decoder back on track. if (PlaybackPosition < decoder.Duration) seekIntoSync(); } var peekFrame = availableFrames.Count > 0 ? availableFrames.Peek() : null; bool outOfSync = false; if (peekFrame != null) { outOfSync = Math.Abs(PlaybackPosition - peekFrame.Time) > lenience_before_seek; if (Loop) { // handle looping bounds (as we could be in the roll-over process between loops). outOfSync &= Math.Abs(PlaybackPosition - decoder.Duration - peekFrame.Time) > lenience_before_seek && Math.Abs(PlaybackPosition + decoder.Duration - peekFrame.Time) > lenience_before_seek; } } // we are too far ahead or too far behind if (outOfSync && decoder.CanSeek) { Logger.Log($"Video too far out of sync ({peekFrame.Time}), seeking to {PlaybackPosition}"); seekIntoSync(); } var frameTime = CurrentFrameTime; while (availableFrames.Count > 0 && checkNextFrameValid(availableFrames.Peek())) { if (lastFrame != null) decoder.ReturnFrames(new[] { lastFrame }); lastFrame = availableFrames.Dequeue(); var tex = lastFrame.Texture; // Check if the new frame has been uploaded so we don't display an old frame if ((tex?.TextureGL as VideoTexture)?.UploadComplete ?? false) { Sprite.Texture = tex; UpdateSizing(); } } if (availableFrames.Count == 0) { foreach (var f in decoder.GetDecodedFrames()) availableFrames.Enqueue(f); } Buffering = decoder.IsRunning && availableFrames.Count == 0; if (frameTime != CurrentFrameTime) FramesProcessed++; void seekIntoSync() { decoder.Seek(PlaybackPosition); decoder.ReturnFrames(availableFrames); availableFrames.Clear(); } } private bool checkNextFrameValid(DecodedFrame frame) { // in the case of looping, we may start a seek back to the beginning but still receive some lingering frames from the end of the last loop. these should be allowed to continue playing. if (Loop && Math.Abs((frame.Time - Duration) - PlaybackPosition) < lenience_before_seek) return true; return frame.Time <= PlaybackPosition && Math.Abs(frame.Time - PlaybackPosition) < lenience_before_seek; } protected override void Dispose(bool isDisposing) { if (isDisposed) return; base.Dispose(isDisposing); isDisposed = true; decoder?.Dispose(); foreach (var f in availableFrames) f.Texture.Dispose(); } protected override float GetFillAspectRatio() => Sprite.FillAspectRatio; protected override Vector2 GetCurrentDisplaySize() => new Vector2(Sprite.Texture?.DisplayWidth ?? 0, Sprite.Texture?.DisplayHeight ?? 0); } }