A game framework written with osu! in mind.
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}