10 private readonly LibVLC _vlc;
11 private readonly MediaPlayer _player;
12 private readonly
object _control =
new();
13 private readonly
object _frames =
new();
14 private readonly VideoMetadataState _metadata =
new();
15 private readonly MediaPlayer.LibVLCVideoLockCb _lockCb;
16 private readonly MediaPlayer.LibVLCVideoUnlockCb _unlockCb;
17 private readonly MediaPlayer.LibVLCVideoDisplayCb _displayCb;
18 private readonly MediaPlayer.LibVLCVideoFormatCb _formatCb;
19 private readonly MediaPlayer.LibVLCVideoCleanupCb _cleanupCb;
20 private VideoDecodeBuffer? _decodeBuffer;
21 private Media? _media;
22 private StreamMediaInput? _input;
23 private Stream? _ownedStream;
24 private CancellationTokenSource? _parseCancellation;
25 private Task<MediaParsedStatus>? _parseTask;
26 private long _generation;
27 private long _playbackRevision;
28 private bool _disposed, _acceptFrames, _playRequested;
29 private volatile bool _loop;
30 private (
int width,
int height) _naturalSize;
31 private Exception? _lastError;
34 public bool Loop {
get => _loop;
set => _loop = value; }
36 public bool IsPlaying {
get { lock (_control)
return !_disposed && _player.IsPlaying; } }
46 public TimeSpan
Position {
get { lock (_control)
return _disposed ? TimeSpan.Zero : TimeSpan.FromMilliseconds(Math.Max(0, _player.Time)); } }
48 public (
int width,
int height)
NaturalSize {
get { lock (_frames)
return _naturalSize; } }
50 public Exception?
LastError => Volatile.Read(ref _lastError);
58 public event EventHandler?
Ended;
60 public event EventHandler<VideoStateChangedEventArgs>?
StateChanged;
62 public event EventHandler<VideoFrameReadyEventArgs>?
FrameReady;
68 public VlcVideoPlayer(
string[]? vlcArgs =
null,
int initialWidth = 1280,
int initialHeight = 720)
70 if (initialWidth <= 0)
throw new ArgumentOutOfRangeException(nameof(initialWidth));
71 if (initialHeight <= 0)
throw new ArgumentOutOfRangeException(nameof(initialHeight));
72 if (OperatingSystem.IsBrowser() || !BitConverter.IsLittleEndian)
73 throw new PlatformNotSupportedException(
"Gondwana.Video requires a little-endian native desktop LibVLC 3 runtime.");
78 _vlc =
new LibVLC(vlcArgs ?? []);
79 try { _player =
new MediaPlayer(_vlc); }
80 catch { _vlc.Dispose();
throw; }
82 catch (Exception ex) when (ex is DllNotFoundException or BadImageFormatException or TypeInitializationException or VLCException)
84 throw new InvalidOperationException(
"Gondwana.Video could not load native LibVLC 3. Add VideoLAN.LibVLC.Windows to the Windows app, or VideoLAN.LibVLC.Mac to a compatible macOS app; on Linux install libvlc-dev and VLC codec plugins. Match the process architecture and deploy the plugins directory. See Gondwana.Video/README.md. For a custom runtime call LibVLCSharp.Shared.Core.Initialize(path) before creating the player.", ex);
87 _unlockCb = (_, _, _) => { };
88 _displayCb = DisplayFrame;
89 _formatCb = SetupFormat;
90 _cleanupCb = CleanupFormat;
91 _player.SetVideoCallbacks(_lockCb, _unlockCb, _displayCb);
92 _player.SetVideoFormatCallbacks(_formatCb, _cleanupCb);
93 _player.Playing += OnStarted;
94 _player.Paused += OnPaused;
95 _player.Stopped += OnStopped;
96 _player.EndReached += OnEnded;
97 _player.EncounteredError += OnError;
103 ArgumentNullException.ThrowIfNull(source);
104 if (!source.IsAbsoluteUri) source =
new Uri(Path.GetFullPath(source.OriginalString));
108 if (!ReplaceMedia(() =>
new Media(_vlc, source), out
long generation))
return;
110 _parseCancellation =
new CancellationTokenSource();
111 _parseTask = media.Parse(source.IsFile ? MediaParseOptions.ParseLocal : MediaParseOptions.ParseNetwork,
112 cancellationToken: _parseCancellation.Token);
115 _ = ObserveParseAsync(_parseTask, media, generation);
120 public void Open(Stream source,
bool leaveOpen =
false)
122 ArgumentNullException.ThrowIfNull(source);
123 if (!source.CanRead)
throw new ArgumentException(
"Video stream must be readable.", nameof(source));
127 var input =
new StreamMediaInput(source);
130 if (!ReplaceMedia(() =>
new Media(_vlc, input), out _))
136 _ownedStream = leaveOpen ? null : source;
138 catch { input.Dispose();
throw; }
144 private bool ReplaceMedia(Func<Media> create, out
long generation)
146 lock (_frames) _acceptFrames =
false;
148 long replacementGeneration = _generation;
150 lock (_frames) _naturalSize = (0, 0);
152 Volatile.Write(ref _lastError,
null);
154 if (_disposed || replacementGeneration != _generation)
160 _player.Media = _media;
161 generation = _generation = _metadata.Begin();
163 if (_disposed || generation != _generation)
return false;
164 lock (_frames) _acceptFrames =
true;
168 private async Task ObserveParseAsync(Task<MediaParsedStatus> parse, Media media,
long generation)
170 MediaParsedStatus status;
171 try { status = await parse.ConfigureAwait(
false); }
172 catch (OperationCanceledException) {
return; }
173 catch (Exception ex) { Volatile.Write(ref _lastError, ex); status = MediaParsedStatus.Failed; }
175 QueueForSource(generation, () => PublishMetadata(media, status == MediaParsedStatus.Done));
178 private void PublishMetadata(Media media,
bool ready)
182 TimeSpan.FromMilliseconds(Math.Max(0, media.Duration)))
184 if (_metadata.Complete(_generation, metadata))
185 StateChanged?.Invoke(
this,
new(ready ?
"MetadataReady" :
"MetadataFailed"));
188 private void QueueForSource(
long generation, Action action) => ThreadPool.QueueUserWorkItem(_ =>
192 if (_disposed || generation != _generation)
return;
194 catch (Exception ex) { Volatile.Write(ref _lastError, ex); }
198 private void OnStarted(
object? sender, EventArgs e)
200 long generation = Volatile.Read(ref _generation);
201 QueueForSource(generation, () =>
203 if (_input is not
null && !
IsMetadataReady) PublishMetadata(_media!,
true);
204 Started?.Invoke(
this, EventArgs.Empty);
207 private void OnPaused(
object? sender, EventArgs e) => QueueForSource(Volatile.Read(ref _generation), () =>
Paused?.Invoke(
this, EventArgs.Empty));
208 private void OnStopped(
object? sender, EventArgs e) => QueueForSource(Volatile.Read(ref _generation), () =>
Stopped?.Invoke(
this, EventArgs.Empty));
209 private void OnError(
object? sender, EventArgs e) => QueueForSource(Volatile.Read(ref _generation), () =>
211 Volatile.Write(ref _lastError, new InvalidOperationException(
"LibVLC could not play the active source. Check its format, access, and installed codec plugins."));
212 StateChanged?.Invoke(this, new(
"Error"));
214 private void OnEnded(
object? sender, EventArgs e)
216 long revision = Volatile.Read(ref _playbackRevision);
217 QueueForSource(Volatile.Read(ref _generation), () =>
219 long generation = _generation;
220 Ended?.Invoke(this, EventArgs.Empty);
221 if (!_disposed && _playRequested && generation == _generation && revision == _playbackRevision && Loop)
231 private uint SetupFormat(ref IntPtr opaque, IntPtr chroma, ref uint width, ref uint height, ref uint pitches, ref uint lines)
235 var buffer =
new VideoDecodeBuffer(checked((
int)width), checked((
int)height));
238 _decodeBuffer?.Dispose();
239 _decodeBuffer = buffer;
243 Marshal.Copy(
new byte[] { (byte)
'R', (
byte)
'V', (byte)
'3', (
byte)
'2' }, 0, chroma, 4);
244 pitches = (uint)buffer.Stride;
245 lines = (uint)buffer.Lines;
246 lock (_frames) _naturalSize = (buffer.Width, buffer.Height);
249 catch (Exception ex) { Volatile.Write(ref _lastError, ex);
return 0; }
251 private IntPtr LockFrame(IntPtr opaque, IntPtr planes)
257 if (_decodeBuffer is not { } buffer || planes == IntPtr.Zero)
return IntPtr.Zero;
258 Marshal.WriteIntPtr(planes, buffer.Pixels);
262 private void DisplayFrame(IntPtr opaque, IntPtr picture)
268 if (!_acceptFrames || _decodeBuffer is not { } buffer)
return;
269 FrameReady?.Invoke(
this,
new(buffer.Pixels, buffer.Width, buffer.Height, buffer.Stride, 0));
272 catch (Exception ex) { Volatile.Write(ref _lastError, ex); }
274 private void CleanupFormat(ref IntPtr opaque)
281 _decodeBuffer?.Dispose();
282 _decodeBuffer =
null;
286 public void Play() { lock (_control) { ThrowIfDisposed(); ++_playbackRevision; _playRequested =
true; _player.Play(); } }
288 public void Pause() { lock (_control) { ThrowIfDisposed(); ++_playbackRevision; _playRequested =
false; _player.SetPause(
true); } }
290 public void Stop() { lock (_control) { ThrowIfDisposed(); ++_playbackRevision; _playRequested =
false; _player.Stop(); } }
292 public void Seek(TimeSpan position) { lock (_control) { ThrowIfDisposed(); _player.Time = Math.Max(0, (
long)position.TotalMilliseconds); } }
296 if (!
double.IsFinite(rate) || rate <= 0)
throw new ArgumentOutOfRangeException(nameof(rate));
297 lock (_control) { ThrowIfDisposed(); _player.SetRate((
float)rate); }
299 private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed,
this);
300 private void ReleaseMedia()
302 _playRequested =
false;
304 lock (_frames) { _decodeBuffer?.Dispose(); _decodeBuffer =
null; }
305 _parseCancellation?.Cancel();
306 if (_parseTask is not
null)
308 try { _parseTask.GetAwaiter().GetResult(); }
309 catch (OperationCanceledException) { }
310 catch (Exception ex) { Volatile.Write(ref _lastError, ex); }
313 _parseCancellation?.Dispose();
314 _parseCancellation =
null;
315 _player.Media =
null;
316 _media?.Dispose(); _media =
null;
317 _input?.Dispose(); _input =
null;
318 _ownedStream?.Dispose(); _ownedStream =
null;
325 if (_disposed)
return;
328 lock (_frames) _acceptFrames =
false;
331 _player.Playing -= OnStarted;
332 _player.Paused -= OnPaused;
333 _player.Stopped -= OnStopped;
334 _player.EndReached -= OnEnded;
335 _player.EncounteredError -= OnError;