77using SoundFlow . Interfaces ;
88using SoundFlow . Structs ;
99
10-
1110namespace SoundFlow . Providers ;
1211
1312/// <summary>
@@ -239,6 +238,7 @@ public virtual void Dispose()
239238
240239/// <summary>
241240/// Handles direct audio streams (e.g., MP3, WAV, OGG files).
241+ /// Uses a background buffering strategy for large files to prevent network issues from crashing the audio thread.
242242/// </summary>
243243internal sealed class DirectStreamProvider ( AudioEngine engine , AudioFormat format , string url , HttpClient client )
244244 : NetworkDataProviderBase ( engine , format , url , client )
@@ -270,8 +270,10 @@ public override async Task InitializeAsync()
270270 }
271271 else
272272 {
273- // Otherwise, use the live network stream. Seeking will not be supported in this case.
274- _stream = await response . Content . ReadAsStreamAsync ( ) ;
273+ // For large or chunked streams, use a buffered stream.
274+ var bufferedStream = new BufferedNetworkStream ( ) ;
275+ bufferedStream . StartProducerTask ( response ) ; // Starts the background download.
276+ _stream = bufferedStream ;
275277 }
276278
277279 _decoder = Engine . CreateDecoder ( _stream , Format ) ;
@@ -606,4 +608,170 @@ public override void Dispose()
606608 _audioBuffer . Clear ( ) ;
607609 }
608610 }
611+ }
612+
613+ /// <summary>
614+ /// A thread-safe, in-memory stream that acts as a circular buffer between a producer (network download)
615+ /// and a consumer (audio decoder). It blocks reads when empty and writes when full.
616+ /// </summary>
617+ internal sealed class BufferedNetworkStream ( int bufferSize = 1 * 1024 * 1024 ) : Stream
618+ {
619+ private enum DownloadState { Buffering , Completed , Failed }
620+
621+ private readonly byte [ ] _buffer = new byte [ bufferSize ] ;
622+ private int _writePosition ;
623+ private int _readPosition ;
624+ private int _bytesAvailable ;
625+
626+ private readonly object _lock = new ( ) ;
627+ private volatile DownloadState _state = DownloadState . Buffering ;
628+ private CancellationTokenSource ? _cts ;
629+ private Task ? _producerTask ;
630+ private bool _isDisposed ;
631+
632+ /// <summary>
633+ /// Starts the background producer task that reads from the network response and fills the buffer.
634+ /// This method takes ownership of the HttpResponseMessage.
635+ /// </summary>
636+ /// <param name="sourceResponse">The HTTP response message containing the content stream.</param>
637+ public void StartProducerTask ( HttpResponseMessage sourceResponse )
638+ {
639+ _cts = new CancellationTokenSource ( ) ;
640+ _producerTask = Task . Run ( async ( ) =>
641+ {
642+ var tempBuffer = ArrayPool < byte > . Shared . Rent ( 16384 ) ; // 16KB read buffer
643+ try
644+ {
645+ await using var sourceStream = await sourceResponse . Content . ReadAsStreamAsync ( _cts . Token ) ;
646+ while ( ! _cts . IsCancellationRequested )
647+ {
648+ var bytesRead = await sourceStream . ReadAsync ( tempBuffer , _cts . Token ) ;
649+ if ( bytesRead == 0 ) break ; // End of network stream
650+
651+ Write ( tempBuffer , 0 , bytesRead ) ;
652+ }
653+
654+ if ( ! _cts . IsCancellationRequested ) SignalCompletion ( ) ;
655+ }
656+ catch ( Exception ex ) when ( ex is not OperationCanceledException )
657+ {
658+ // Network error occurred.
659+ SignalFailure ( ) ;
660+ }
661+ finally
662+ {
663+ ArrayPool < byte > . Shared . Return ( tempBuffer ) ;
664+ sourceResponse . Dispose ( ) ;
665+ }
666+ } ) ;
667+ }
668+
669+ public override int Read ( byte [ ] buffer , int offset , int count )
670+ {
671+ lock ( _lock )
672+ {
673+ while ( _bytesAvailable == 0 && _state == DownloadState . Buffering && ! _isDisposed )
674+ {
675+ Monitor . Wait ( _lock ) ;
676+ }
677+
678+ if ( _bytesAvailable == 0 ) return 0 ; // End of stream or failure
679+
680+ var bytesToRead = Math . Min ( count , _bytesAvailable ) ;
681+
682+ // Read from circular buffer
683+ var firstChunkSize = Math . Min ( bytesToRead , _buffer . Length - _readPosition ) ;
684+ Buffer . BlockCopy ( _buffer , _readPosition , buffer , offset , firstChunkSize ) ;
685+ _readPosition = ( _readPosition + firstChunkSize ) % _buffer . Length ;
686+
687+ if ( firstChunkSize < bytesToRead )
688+ {
689+ var secondChunkSize = bytesToRead - firstChunkSize ;
690+ Buffer . BlockCopy ( _buffer , _readPosition , buffer , offset + firstChunkSize , secondChunkSize ) ;
691+ _readPosition = ( _readPosition + secondChunkSize ) % _buffer . Length ;
692+ }
693+
694+ _bytesAvailable -= bytesToRead ;
695+ Monitor . PulseAll ( _lock ) ; // Signal producer that space is available
696+ return bytesToRead ;
697+ }
698+ }
699+
700+ public override void Write ( byte [ ] buffer , int offset , int count )
701+ {
702+ if ( count == 0 ) return ;
703+
704+ lock ( _lock )
705+ {
706+ while ( _buffer . Length - _bytesAvailable < count && ! _isDisposed )
707+ {
708+ Monitor . Wait ( _lock ) ;
709+ }
710+
711+ if ( _isDisposed ) return ;
712+
713+ // Write to circular buffer
714+ var firstChunkSize = Math . Min ( count , _buffer . Length - _writePosition ) ;
715+ Buffer . BlockCopy ( buffer , offset , _buffer , _writePosition , firstChunkSize ) ;
716+ _writePosition = ( _writePosition + firstChunkSize ) % _buffer . Length ;
717+
718+ if ( firstChunkSize < count )
719+ {
720+ var secondChunkSize = count - firstChunkSize ;
721+ Buffer . BlockCopy ( buffer , offset + firstChunkSize , _buffer , _writePosition , secondChunkSize ) ;
722+ _writePosition = ( _writePosition + secondChunkSize ) % _buffer . Length ;
723+ }
724+
725+ _bytesAvailable += count ;
726+ Monitor . PulseAll ( _lock ) ; // Signal consumer that data is available
727+ }
728+ }
729+
730+ private void SignalCompletion ( )
731+ {
732+ lock ( _lock )
733+ {
734+ _state = DownloadState . Completed ;
735+ Monitor . PulseAll ( _lock ) ; // Wake any waiting readers to signal EOS
736+ }
737+ }
738+
739+ private void SignalFailure ( )
740+ {
741+ lock ( _lock )
742+ {
743+ _state = DownloadState . Failed ;
744+ Monitor . PulseAll ( _lock ) ; // Wake any waiting readers to signal EOS
745+ }
746+ }
747+
748+ protected override void Dispose ( bool disposing )
749+ {
750+ if ( _isDisposed ) return ;
751+ _isDisposed = true ;
752+
753+ if ( disposing )
754+ {
755+ lock ( _lock )
756+ {
757+ _cts ? . Cancel ( ) ;
758+ Monitor . PulseAll ( _lock ) ; // Unblock any waiting threads
759+ }
760+ _producerTask ? . Wait ( TimeSpan . FromSeconds ( 5 ) ) ; // Wait for producer to finish
761+ _cts ? . Dispose ( ) ;
762+ }
763+
764+ base . Dispose ( disposing ) ;
765+ }
766+
767+ #region Not Supported Stream Members
768+ public override bool CanRead => ! _isDisposed ;
769+ public override bool CanSeek => false ;
770+ public override bool CanWrite => ! _isDisposed ;
771+ public override long Length => throw new NotSupportedException ( ) ;
772+ public override long Position { get => throw new NotSupportedException ( ) ; set => throw new NotSupportedException ( ) ; }
773+ public override void Flush ( ) { }
774+ public override long Seek ( long offset , SeekOrigin origin ) => throw new NotSupportedException ( ) ;
775+ public override void SetLength ( long value ) => throw new NotSupportedException ( ) ;
776+ #endregion
609777}
0 commit comments