A game framework written with osu! in mind.
at master 213 lines 7.6 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using osu.Framework.Allocation; 5using System; 6using System.Collections.Generic; 7using System.IO; 8using JetBrains.Annotations; 9using osu.Framework.Graphics.Animations; 10using osu.Framework.Logging; 11using osu.Framework.Platform; 12using osu.Framework.Graphics.Shaders; 13using osuTK; 14 15namespace osu.Framework.Graphics.Video 16{ 17 /// <summary> 18 /// Represents a composite that displays a video played back from a stream or a file. 19 /// </summary> 20 public class Video : AnimationClockComposite 21 { 22 /// <summary> 23 /// Whether this video is in a buffering state, waiting on decoder or underlying stream. 24 /// </summary> 25 public bool Buffering { get; private set; } 26 27 /// <summary> 28 /// True if the video should loop after finishing its playback, false otherwise. 29 /// </summary> 30 public override bool Loop 31 { 32 get => base.Loop; 33 set 34 { 35 if (decoder != null) 36 decoder.Looping = value; 37 38 base.Loop = value; 39 } 40 } 41 42 /// <summary> 43 /// Whether the video decoding process has faulted. 44 /// </summary> 45 public bool IsFaulted => decoder?.IsFaulted ?? false; 46 47 /// <summary> 48 /// The current state of the decoding process. 49 /// </summary> 50 public VideoDecoder.DecoderState State => decoder?.State ?? VideoDecoder.DecoderState.Ready; 51 52 internal double CurrentFrameTime => lastFrame?.Time ?? 0; 53 54 internal int AvailableFrames => availableFrames.Count; 55 56 private VideoDecoder decoder; 57 58 private readonly Stream stream; 59 60 private readonly Queue<DecodedFrame> availableFrames = new Queue<DecodedFrame>(); 61 62 private DecodedFrame lastFrame; 63 64 /// <summary> 65 /// The total number of frames processed by this instance. 66 /// </summary> 67 public int FramesProcessed { get; private set; } 68 69 /// <summary> 70 /// The length in milliseconds that the decoder can be out of sync before a seek is automatically performed. 71 /// </summary> 72 private const float lenience_before_seek = 2500; 73 74 private bool isDisposed; 75 76 internal VideoSprite Sprite; 77 78 /// <summary> 79 /// YUV->RGB conversion matrix based on the video colorspace 80 /// </summary> 81 public Matrix3 ConversionMatrix => decoder.GetConversionMatrix(); 82 83 /// <summary> 84 /// Creates a new <see cref="Video"/>. 85 /// </summary> 86 /// <param name="filename">The video file.</param> 87 /// <param name="startAtCurrentTime">Whether the current clock time should be assumed as the 0th video frame.</param> 88 public Video(string filename, bool startAtCurrentTime = true) 89 : this(File.OpenRead(filename), startAtCurrentTime) 90 { 91 } 92 93 public override Drawable CreateContent() => Sprite = new VideoSprite(this) { RelativeSizeAxes = Axes.Both }; 94 95 /// <summary> 96 /// Creates a new <see cref="Video"/>. 97 /// </summary> 98 /// <param name="stream">The video file stream.</param> 99 /// <param name="startAtCurrentTime">Whether the current clock time should be assumed as the 0th video frame.</param> 100 public Video([NotNull] Stream stream, bool startAtCurrentTime = true) 101 : base(startAtCurrentTime) 102 { 103 this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); 104 } 105 106 [BackgroundDependencyLoader] 107 private void load(GameHost gameHost, ShaderManager shaders) 108 { 109 decoder = gameHost.CreateVideoDecoder(stream); 110 decoder.Looping = Loop; 111 decoder.StartDecoding(); 112 113 Duration = decoder.Duration; 114 } 115 116 protected override void Update() 117 { 118 base.Update(); 119 120 if (decoder.State == VideoDecoder.DecoderState.EndOfStream) 121 { 122 // 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. 123 if (PlaybackPosition < decoder.Duration) 124 seekIntoSync(); 125 } 126 127 var peekFrame = availableFrames.Count > 0 ? availableFrames.Peek() : null; 128 bool outOfSync = false; 129 130 if (peekFrame != null) 131 { 132 outOfSync = Math.Abs(PlaybackPosition - peekFrame.Time) > lenience_before_seek; 133 134 if (Loop) 135 { 136 // handle looping bounds (as we could be in the roll-over process between loops). 137 outOfSync &= Math.Abs(PlaybackPosition - decoder.Duration - peekFrame.Time) > lenience_before_seek && 138 Math.Abs(PlaybackPosition + decoder.Duration - peekFrame.Time) > lenience_before_seek; 139 } 140 } 141 142 // we are too far ahead or too far behind 143 if (outOfSync && decoder.CanSeek) 144 { 145 Logger.Log($"Video too far out of sync ({peekFrame.Time}), seeking to {PlaybackPosition}"); 146 seekIntoSync(); 147 } 148 149 var frameTime = CurrentFrameTime; 150 151 while (availableFrames.Count > 0 && checkNextFrameValid(availableFrames.Peek())) 152 { 153 if (lastFrame != null) decoder.ReturnFrames(new[] { lastFrame }); 154 lastFrame = availableFrames.Dequeue(); 155 156 var tex = lastFrame.Texture; 157 158 // Check if the new frame has been uploaded so we don't display an old frame 159 if ((tex?.TextureGL as VideoTexture)?.UploadComplete ?? false) 160 { 161 Sprite.Texture = tex; 162 UpdateSizing(); 163 } 164 } 165 166 if (availableFrames.Count == 0) 167 { 168 foreach (var f in decoder.GetDecodedFrames()) 169 availableFrames.Enqueue(f); 170 } 171 172 Buffering = decoder.IsRunning && availableFrames.Count == 0; 173 174 if (frameTime != CurrentFrameTime) 175 FramesProcessed++; 176 177 void seekIntoSync() 178 { 179 decoder.Seek(PlaybackPosition); 180 decoder.ReturnFrames(availableFrames); 181 availableFrames.Clear(); 182 } 183 } 184 185 private bool checkNextFrameValid(DecodedFrame frame) 186 { 187 // 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. 188 if (Loop && Math.Abs((frame.Time - Duration) - PlaybackPosition) < lenience_before_seek) 189 return true; 190 191 return frame.Time <= PlaybackPosition && Math.Abs(frame.Time - PlaybackPosition) < lenience_before_seek; 192 } 193 194 protected override void Dispose(bool isDisposing) 195 { 196 if (isDisposed) 197 return; 198 199 base.Dispose(isDisposing); 200 201 isDisposed = true; 202 decoder?.Dispose(); 203 204 foreach (var f in availableFrames) 205 f.Texture.Dispose(); 206 } 207 208 protected override float GetFillAspectRatio() => Sprite.FillAspectRatio; 209 210 protected override Vector2 GetCurrentDisplaySize() => 211 new Vector2(Sprite.Texture?.DisplayWidth ?? 0, Sprite.Texture?.DisplayHeight ?? 0); 212 } 213}