Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions MediaManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class MediaManager : IDisposable {
MediaObject _npcSound = null;

public event EventHandler<MediaError> OnErrorReceived;
public event EventHandler<string> OnDiagnosticReceived;
public event EventHandler OnCleanupTime;
private IMediaGameObject _mainPlayer = null;
private IMediaGameObject _camera;
Expand Down Expand Up @@ -109,11 +110,13 @@ public async void PlayAudioStream(IMediaGameObject playerObject, WaveStream audi
try {
if (playerObject != null) {
bool playbackQueued = false;
if (_nativeGameAudio.ContainsKey(playerObject.Name)) {
var mediaObject = _nativeGameAudio[playerObject.Name];
if (_nativeGameAudio.TryGetValue(playerObject.Name, out var existingMediaObject)) {
if (!queuePlayback) {
mediaObject.Stop();
} else if (mediaObject.PlaybackState == PlaybackState.Playing) {
// Stop the previous NPC media object before replacing the
// dictionary entry so we do not lose the only handle capable
// of halting stale dialogue playback.
existingMediaObject.Stop();
} else if (existingMediaObject.PlaybackState == PlaybackState.Playing) {
if (!_nativeAudioQueue.ContainsKey(playerObject.Name)) {
_nativeAudioQueue.TryAdd(playerObject.Name, new Queue<WaveStream>());
}
Expand All @@ -129,11 +132,11 @@ public async void PlayAudioStream(IMediaGameObject playerObject, WaveStream audi
} catch { }
};
EventHandler<string> removalFunction = delegate {
mediaObject.PlaybackStopped -= function;
mediaObject.Invalidated = true;
existingMediaObject.PlaybackStopped -= function;
existingMediaObject.Invalidated = true;
};
mediaObject.PlaybackStopped += function;
mediaObject.PlaybackStopped += removalFunction;
existingMediaObject.PlaybackStopped += function;
existingMediaObject.PlaybackStopped += removalFunction;
playbackQueued = true;
}
}
Expand Down Expand Up @@ -181,6 +184,10 @@ private void MediaManager_OnErrorReceived(object? sender, MediaError e) {
OnErrorReceived?.Invoke(this, new MediaError() { Exception = e.Exception });
}

internal void TraceDiagnostic(string message) {
OnDiagnosticReceived?.Invoke(this, message);
}

public async void PlayStream(IMediaGameObject playerObject, string audioPath, int delay = 0) {
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Run(() => {
Expand Down Expand Up @@ -270,13 +277,20 @@ public void StopAudio(IMediaGameObject playerObject) {

}
try {
if (_nativeGameAudio.ContainsKey(playerObject.Name)) {
if (_nativeGameAudio.TryGetValue(playerObject.Name, out var nativeGameAudio)) {
_nativeAudioQueue.Clear();
_nativeGameAudio[playerObject.Name].Invalidated = true;
_nativeGameAudio[playerObject.Name].Stop();
TraceDiagnostic($"NPC audio stop requested name='{playerObject.Name}' state={nativeGameAudio.PlaybackState} invalidated={nativeGameAudio.Invalidated}");
nativeGameAudio.Invalidated = true;
nativeGameAudio.Stop();
} else {
// This is intentionally logged for NPC audio only: if skipped dialogue
// still overlaps, this tells maintainers whether the manager had a live
// media object to stop when the dialogue state was cleared.
TraceDiagnostic($"NPC audio stop requested but no active native audio was found name='{playerObject.Name}' activeNativeAudio={_nativeGameAudio.Count}");
}
} catch {

} catch (Exception e) {
TraceDiagnostic($"NPC audio stop failed name='{playerObject.Name}' type={e.GetType().Name} message='{e.Message}'");
OnErrorReceived?.Invoke(this, new MediaError() { Exception = e });
}
}
}
Expand Down
146 changes: 107 additions & 39 deletions MediaObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class MediaObject : IDisposable {
private bool _vlcWasAbleToStart;
private float _volumeOffset;
private bool _disposed;
private readonly object _playbackLifecycleLock = new object();

public MediaObject(MediaManager parent, IMediaGameObject playerObject, IMediaGameObject camera,
SoundType soundType, string soundPath, string libVLCPath, bool spatialAllowed) {
Expand Down Expand Up @@ -145,31 +146,37 @@ private void MountLoopCheck() {
});
}
private void DonePlayingCheck() {
var player = _player;
var wavePlayer = _wavePlayer;
if (player == null || wavePlayer == null) {
return;
}

Stopwatch stopwatch = new Stopwatch();
Task.Run(async () => {
Task.Run(() => {
try {
Thread.Sleep(300);
long lastPosition = _player?.Position ?? 0;
while (true) {
var player = _player;
if (player == null) {
break;
}
while (!Invalidated && !_disposed && ReferenceEquals(_player, player) && ReferenceEquals(_wavePlayer, wavePlayer)) {
if (player.Position > player.Length * 0.985f) {
if (!stopwatch.IsRunning) {
stopwatch.Start();
}
if (stopwatch.ElapsedMilliseconds > 100) {
Thread.Sleep(100);
_wavePlayer?.Stop();
// Stop() can run when dialogue advances before natural playback completion.
// In that case this poll should exit quietly instead of touching disposed audio state.
if (!Invalidated && !_disposed && ReferenceEquals(_wavePlayer, wavePlayer)) {
wavePlayer.Stop();
}
break;
}
} else {
stopwatch.Reset();
}
lastPosition = player.Position;
Thread.Sleep(100);
}
} catch (ObjectDisposedException) {
// Stop()/Dispose() owns cleanup when playback is interrupted, such as skipping NPC dialogue.
} catch (Exception e) { OnErrorReceived?.Invoke(this, new MediaError() { Exception = e }); }
});
}
Expand Down Expand Up @@ -251,44 +258,110 @@ public void EndLooping() {
try {
_loopStream.EnableLooping = false;
_loopStream?.Dispose();
_loopStream = null;
} catch {

}
}
}
public void Stop() {
Volume = 0;
EndLooping();
if (_wavePlayer != null) {
StopAndDisposePlayback(false);
}
public void LoopEarly() {
_loopStream?.LoopEarly();
}

private void StopAndDisposePlayback(bool disposeVideoResources) {
bool shouldNotifyStopped = false;
bool isNpc;
string objectName;
IWavePlayer wavePlayer;
WaveStream player;
MediaPlayer vlcPlayer;
LibVLC vlc;

lock (_playbackLifecycleLock) {
isNpc = _soundType == SoundType.NPC;
objectName = _playerObject?.Name;
var playbackState = PlaybackState;

try { Volume = 0; } catch { }
EndLooping();

wavePlayer = _wavePlayer;
player = _player;
vlcPlayer = _vlcPlayer;
vlc = libVLC;
shouldNotifyStopped = wavePlayer == null;

if (isNpc) {
_parent?.TraceDiagnostic($"NPC media stop entered name='{objectName}' hasWavePlayer={wavePlayer != null} hasStream={player != null} hasVlcPlayer={vlcPlayer != null} state={playbackState} invalidated={Invalidated} disposed={_disposed}");
}

// Capture the handles before clearing fields. The local handles are
// now responsible for shutdown, so dictionary replacement or cleanup
// cannot drop the last object capable of stopping stale playback.
Invalidated = true;

if (ReferenceEquals(_wavePlayer, wavePlayer)) {
_wavePlayer = null;
}
if (ReferenceEquals(_player, player)) {
_player = null;
}
if (disposeVideoResources && ReferenceEquals(_vlcPlayer, vlcPlayer)) {
_vlcPlayer = null;
}
if (disposeVideoResources && ReferenceEquals(libVLC, vlc)) {
libVLC = null;
}

try { Volume = 0; } catch { }
}

if (wavePlayer != null) {
try {
if (_wavePlayer != null) {
try {
_wavePlayer?.Stop();
_wavePlayer?.Dispose();
} catch {
PlaybackStopped?.Invoke(this, "OK");
}
wavePlayer.Stop();
wavePlayer.Dispose();
if (isNpc) {
_parent?.TraceDiagnostic($"NPC media wave stop completed name='{objectName}'");
}
} catch (Exception e) {
if (isNpc) {
_parent?.TraceDiagnostic($"NPC media wave stop failed name='{objectName}' type={e.GetType().Name} message='{e.Message}'");
}
OnErrorReceived?.Invoke(this, new MediaError() { Exception = e });
PlaybackStopped?.Invoke(this, "OK");
shouldNotifyStopped = true;
}
_wavePlayer = null;
} else {
PlaybackStopped?.Invoke(this, "OK");
}
if (_vlcPlayer != null) {

if (vlcPlayer != null) {
try {
_vlcPlayer?.Stop();
} catch (Exception e) { OnErrorReceived?.Invoke(this, new MediaError() { Exception = e }); }
vlcPlayer.Stop();
if (disposeVideoResources) {
vlcPlayer.Dispose();
}
} catch (Exception e) {
OnErrorReceived?.Invoke(this, new MediaError() { Exception = e });
shouldNotifyStopped = true;
}
}

try {
player?.Dispose();
} catch (Exception e) {
if (isNpc) {
_parent?.TraceDiagnostic($"NPC media stream dispose failed name='{objectName}' type={e.GetType().Name} message='{e.Message}'");
}
}

if (disposeVideoResources) {
try { vlc?.Dispose(); } catch { }
}

if (shouldNotifyStopped) {
PlaybackStopped?.Invoke(this, "OK");
}
try { _player?.Dispose(); } catch { }
_player = null;
Volume = 0;
Invalidated = true;
}
public void LoopEarly() {
_loopStream?.LoopEarly();
}

public async void Play(WaveStream soundPath, float volume, int delay, bool useSmbPitch,
Expand Down Expand Up @@ -712,12 +785,7 @@ public void Dispose() {
}
_disposed = true;
_parent.OnCleanupTime -= _parent_OnCleanupTime;
Stop();
Volume = 0;
try { _vlcPlayer?.Dispose(); } catch { }
_vlcPlayer = null;
try { libVLC?.Dispose(); } catch { }
libVLC = null;
StopAndDisposePlayback(true);
}
}
public enum SoundType {
Expand Down