Question:
I'm at an impasse with the implementation of an application for listening to online radio. Based on the NAudio library. With minor edits, the implementation does not differ from this example published with the library. The parser implementation is takenfrom this example.
The essence of the problem. I need to parse the metadata from the stream to get the artist and song. In theory, when sending a request with a header:
_webRequest.Headers.Add("Icy-MetaData", "1");
we should receive a response with the title "icy-metaint" with the value (for example: 16000) the position in the stream containing the artist and song metadata. Parsing (in theory) should be done in a custom Stream implementation. Here is my implementation:
public class RadioPlayerStream : Stream
{
private readonly Stream _sourceStream;
private long _pos;
private readonly byte[] _readAheadBuffer;
private int _readAheadLength;
private int _readAheadOffset;
private int _icyMetaInt;
public RadioPlayerStream(Stream sourceStream, int icyMetaInt)
{
_sourceStream = sourceStream;
_icyMetaInt = icyMetaInt;
_readAheadBuffer = new byte[4096];
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override void Flush()
{
return;
}
public override long Length
{
get { return _pos; }
}
public override long Position
{
get
{
return _pos;
}
set
{
throw new InvalidOperationException();
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new InvalidOperationException();
}
public override void SetLength(long value)
{
throw new InvalidOperationException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException();
}
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = 0;
while (bytesRead < count)
{
int readAheadAvailableBytes = _readAheadLength - _readAheadOffset;
int bytesRequired = count - bytesRead;
if (readAheadAvailableBytes > 0)
{
int toCopy = Math.Min(readAheadAvailableBytes, bytesRequired);
Array.Copy(_readAheadBuffer, _readAheadOffset, buffer, offset + bytesRead, toCopy);
bytesRead += toCopy;
_readAheadOffset += toCopy;
}
else
{
_readAheadOffset = 0;
_readAheadLength = _sourceStream.Read(_readAheadBuffer, 0, _readAheadBuffer.Length);
if (_readAheadLength == 0)
{
break;
}
}
}
if (_icyMetaInt != 0)
{
StreamParser(_readAheadBuffer, _readAheadLength, _icyMetaInt);
}
_pos += bytesRead;
return bytesRead;
}
// parser
int count = 0;
int metadataLength = 0;
string metadataHeader = "";
string oldMetadataHeader = null;
private void StreamParser(byte[] buffer, int bufLen, int icyMetaInt)
{
for (var i = 0; i < bufLen; i++)
{
if (metadataLength != 0)
{
metadataHeader += Convert.ToChar(buffer[i]);
metadataLength--;
if (metadataLength == 0) // all metadata informations were written to the 'metadataHeader' string
{
string fileName = "";
// if songtitle changes, create a new file
if (!metadataHeader.Equals(oldMetadataHeader))
{
var x = metadataHeader;
// extract songtitle from metadata header. Trim was needed, because some stations don't trim the songtitle
fileName = Regex.Match(metadataHeader, "(StreamTitle=')(.*)(';StreamUrl)").Groups[2].Value.Trim();
// save new header to 'oldMetadataHeader' string, to compare if there's a new song starting
oldMetadataHeader = metadataHeader;
}
metadataHeader = "";
}
}
else // write mp3 data to file or extract metadata headerlength
{
if (count++ < icyMetaInt) // write bytes to filestream
{
}
else // get headerlength from lengthbyte and multiply by 16 to get correct headerlength
{
metadataLength = Convert.ToInt32(buffer[i]) * 16;
count = 0;
}
}
}
}
}
Instead of a parsed string, a set of characters comes to me:
lØ|ÊB\u0003\u0014Qä¢7BW\u0093`Á\u0087+[§cÔ\a8àNyåÿû\u0090`¾\0\u0003@HÞc\u00033jTÅ\u009c\r\u001c£[\rÉ!w¬\u0014k\u0089P\u001ap4±\tlv×\u0014\u0012H\u0013e\u0011 \u0015\u0002\u00944\u000eÙâ¤\u0096\u0010\u0085Öá°Ñ ,A&]æÌü\u008d¤û<Ëo¹\u001f\tIÖ\u0003upF\b\u0002\f,cC\b,%\u0001+\u008aw|,j\u0080Ê\u000e
Here is the method to get the data:
private void StreamMp3(object state)
{
_buffer = new byte[BufferSize]; // needs to be big enough to hold a decompressed frame
bool riseMp3FrameInfoEvent = false;
_fullyDownloaded = false;
var url = (string)state;
int metaInt = 0;
#region metadata
// create web request
_webRequest = (HttpWebRequest)WebRequest.Create(url);
if (MetaDataEnabled)
{
// clear old request header and build own header to receive ICY-metadata
var serverPath = "/";
_webRequest.Headers.Clear();
_webRequest.Headers.Add("GET", serverPath + " HTTP/1.0");
_webRequest.Headers.Add("Icy-MetaData", "1");
// needed to receive metadata informations
_webRequest.UserAgent = "WinampMPEG/5.09";
}
#endregion
#region make HttpWebRequest
HttpWebResponse resp;
try
{
resp = (HttpWebResponse)_webRequest.GetResponse();
if (resp.Headers.AllKeys.Contains("icy-metaint"))
metaInt = Convert.ToInt32(resp.GetResponseHeader("icy-metaint"));
StreamInfoUpdated?.Invoke(StreamInfo = new StreamInfoModel(resp));
}
catch (WebException e)
{
if (e.Status != WebExceptionStatus.RequestCanceled)
{
ShowError(e.Message);
}
return;
}
#endregion
try
{
using (var responseStream = resp.GetResponseStream())
{
using (_stream = new RadioPlayerStream(responseStream, metaInt))
{
do
{
if (IsBufferNearlyFull)
{
DebugEvent?.Invoke("Buffer getting full, taking a break");
Thread.Sleep(500);
}
else
{
Mp3Frame frame;
#region get mp3frame
try
{
frame = Mp3Frame.LoadFromStream(_stream);
Mp3FrameInfo = new Mp3FrameInfoModel(frame);
if (!riseMp3FrameInfoEvent)
{
Mp3FrameInfoUpdated?.Invoke(Mp3FrameInfo);
riseMp3FrameInfoEvent = true;
}
}
catch (EndOfStreamException) // reached the end of the MP3 file / stream
{
_fullyDownloaded = true;
EndOfStream.Invoke(this, EventArgs.Empty);
break;
}
catch (WebException) // probably we have aborted download from the GUI thread
{
ShowError("probably we have aborted download from the GUI thread");
break;
}
#endregion
#region create decompressor
if (_decompressor == null)
{
// don't think these details matter too much - just help ACM select the right codec
// however, the buffered provider doesn't know what sample rate it is working at
// until we have a frame
_decompressor = CreateFrameDecompressor(frame);
_bufferedWaveProvider = new BufferedWaveProvider(_decompressor.OutputFormat);
_bufferedWaveProvider.BufferDuration = TimeSpan.FromSeconds(WaveProviderBufferDuration); // allow us to get well ahead of ourselves
WaveInfoUpdated?.Invoke(WaveInfo = new WaveInfoModel(_bufferedWaveProvider));
}
#endregion
// decompress
int decompressed = _decompressor.DecompressFrame(frame, _buffer, 0);
_bufferedWaveProvider.AddSamples(_buffer, 0, decompressed);
//DebugEvent?.Invoke($"Decompressed a frame {decompressed}");
}
}
while (_playbackState != StreamingPlaybackState.Stopped);
}
// was doing this in a finally block, but for some reason
// we are hanging on response stream .Dispose so never get there
_decompressor?.Dispose();
DebugEvent?.Invoke("Exiting...");
}
}
finally
{
_decompressor?.Dispose();
}
}
Here is the overridden stream class:
public class RadioPlayerStream : Stream
{
private readonly Stream _sourceStream;
private long _pos;
private readonly byte[] _readAheadBuffer;
private int _readAheadLength;
private int _readAheadOffset;
private int _icyMetaInt;
public RadioPlayerStream(Stream sourceStream, int icyMetaInt)
{
_sourceStream = sourceStream;
_icyMetaInt = icyMetaInt;
_readAheadBuffer = new byte[4096];
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override void Flush()
{
return;
}
public override long Length
{
get { return _pos; }
}
public override long Position
{
get
{
return _pos;
}
set
{
throw new InvalidOperationException();
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new InvalidOperationException();
}
public override void SetLength(long value)
{
throw new InvalidOperationException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new InvalidOperationException();
}
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = 0;
while (bytesRead < count)
{
int readAheadAvailableBytes = _readAheadLength - _readAheadOffset;
int bytesRequired = count - bytesRead;
if (readAheadAvailableBytes > 0)
{
int toCopy = Math.Min(readAheadAvailableBytes, bytesRequired);
Array.Copy(_readAheadBuffer, _readAheadOffset, buffer, offset + bytesRead, toCopy);
bytesRead += toCopy;
_readAheadOffset += toCopy;
}
else
{
_readAheadOffset = 0;
_readAheadLength = _sourceStream.Read(_readAheadBuffer, 0, _readAheadBuffer.Length);
if (_readAheadLength == 0)
{
break;
}
}
}
if (_icyMetaInt != 0)
{
StreamParser(_readAheadBuffer, _readAheadLength, _icyMetaInt);
}
_pos += bytesRead;
return bytesRead;
}
// parser
int count = 0;
int metadataLength = 0;
string metadataHeader = "";
public string oldMetadataHeader = "";
private void StreamParser(byte[] buffer, int bufLen, int icyMetaInt)
{
for (var i = 0; i < bufLen; i++)
{
if (metadataLength != 0)
{
metadataHeader += Convert.ToChar(buffer[i]);
metadataLength--;
if (metadataLength == 0) // all metadata informations were written to the 'metadataHeader' string
{
string fileName = "";
// if songtitle changes, create a new file
if (!metadataHeader.Equals(oldMetadataHeader))
{
var x = metadataHeader;
// extract songtitle from metadata header. Trim was needed, because some stations don't trim the songtitle
fileName = Regex.Match(metadataHeader, "(StreamTitle=')(.*)(';StreamUrl)").Groups[2].Value.Trim();
// save new header to 'oldMetadataHeader' string, to compare if there's a new song starting
oldMetadataHeader = metadataHeader;
}
metadataHeader = "";
}
}
else // write mp3 data to file or extract metadata headerlength
{
if (count++ < icyMetaInt) // write bytes to filestream
{
}
else // get headerlength from lengthbyte and multiply by 16 to get correct headerlength
{
metadataLength = Convert.ToInt32(buffer[i]) * 16;
count = 0;
}
}
}
}
}
The catch is how, having on hand the MetaInt value (the position of the metadata in the stream), we can pull out the data about the current composition on the air.
Namely the Read method:
public override int Read(byte[] buffer, int offset, int count)
{
int bytesRead = 0;
while (bytesRead < count)
{
int readAheadAvailableBytes = _readAheadLength - _readAheadOffset;
int bytesRequired = count - bytesRead;
if (readAheadAvailableBytes > 0)
{
int toCopy = Math.Min(readAheadAvailableBytes, bytesRequired);
Array.Copy(_readAheadBuffer, _readAheadOffset, buffer, offset + bytesRead, toCopy);
bytesRead += toCopy;
_readAheadOffset += toCopy;
}
else
{
_readAheadOffset = 0;
_readAheadLength = _sourceStream.Read(_readAheadBuffer, 0, _readAheadBuffer.Length);
if (_readAheadLength == 0)
{
break;
}
}
}
if (_icyMetaInt != 0)
{
StreamParser(_readAheadBuffer, _readAheadLength, _icyMetaInt);
}
_pos += bytesRead;
return bytesRead;
}
And the method for parsing:
private void StreamParser(byte[] buffer, int bufLen, int icyMetaInt)
{
for (var i = 0; i < bufLen; i++)
{
if (metadataLength != 0)
{
metadataHeader += Convert.ToChar(buffer[i]);
metadataLength--;
if (metadataLength == 0) // all metadata informations were written to the 'metadataHeader' string
{
string fileName = "";
// if songtitle changes, create a new file
if (!metadataHeader.Equals(oldMetadataHeader))
{
var x = metadataHeader;
// extract songtitle from metadata header. Trim was needed, because some stations don't trim the songtitle
fileName = Regex.Match(metadataHeader, "(StreamTitle=')(.*)(';StreamUrl)").Groups[2].Value.Trim();
// save new header to 'oldMetadataHeader' string, to compare if there's a new song starting
oldMetadataHeader = metadataHeader;
}
metadataHeader = "";
}
}
else // write mp3 data to file or extract metadata headerlength
{
if (count++ < icyMetaInt) // write bytes to filestream
{
}
else // get headerlength from lengthbyte and multiply by 16 to get correct headerlength
{
metadataLength = Convert.ToInt32(buffer[i]) * 16;
count = 0;
}
}
}
}
Answer:
Written for UWP, there are some minor flaws in terms of breaking connections, but this will be exhaustive for you.
using Portable.Text;
using ShoutcastMSS.Enums;
using ShoutcastMSS.Exceptions;
using ShoutcastStream.EventArguments;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
using Windows.Media.Core;
using Windows.Media.MediaProperties;
using Windows.Storage;
using Windows.Storage.Streams;
namespace ShoutcastStream
{
public class ShoutcastMSSManager
{
#region Public Fields
public event EventHandler<SMSSMetadataChangedEventArgs> MetadataChanged;
public event EventHandler<SMSSConnectionFailedEventArgs> ConnectionFailed;
public event EventHandler<SMSSRecordStateChangedEventArgs> RecordStateChanged;
public MediaStreamSource MediaStreamSource { get; private set; }
public ShoutcastStationInfo StationInfo = new ShoutcastStationInfo();
public Uri StreamUrl { get; }
#endregion
#region Private Fields
private IRandomAccessStream _recordStream = null;
private bool _isStreamRecords = false;
private readonly uint _sampleSize = 1152;
private uint _metadataInterval = 16000;
private uint _metadataPosition = 0;
private TimeSpan _timeOffSet = new TimeSpan();
private readonly TimeSpan _sampleDuration = TimeSpan.FromMilliseconds(70);
private DataReader _shoutcastDataReader = null;
private readonly int _connectionTimeout;
#endregion
public ShoutcastMSSManager(string url, int connectionTimeout = 4000)
{
StreamUrl = new Uri(url);
_connectionTimeout = connectionTimeout;
}
public async void StartRecord()
{
try
{
string fileName = MediaStreamSource.MusicProperties.AlbumArtist + "-" + MediaStreamSource.MusicProperties.Title + ".mp3";
StorageFile recordFile = await KnownFolders.MusicLibrary.CreateFileAsync(fileName, CreationCollisionOption.GenerateUniqueName);
_recordStream = await recordFile.OpenAsync(FileAccessMode.ReadWrite);
_isStreamRecords = true;
RecordStateChanged?.Invoke(this, new SMSSRecordStateChangedEventArgs(RecordState.Records, fileName));
}
catch { }
}
public void StopRecord()
{
try
{
if (_recordStream != null)
{
_recordStream.Dispose();
_recordStream = null;
}
_isStreamRecords = false;
RecordStateChanged?.Invoke(this, new SMSSRecordStateChangedEventArgs(RecordState.Stopped, null));
}
catch { }
}
public void Disconnect()
{
try
{
if (_isStreamRecords)
{
StopRecord();
}
MediaStreamSource.SampleRequested -= MediaStreamSource_SampleRequested;
MediaStreamSource.Starting -= MediaStreamSource_Starting;
MediaStreamSource.Closed -= MediaStreamSource_Closed;
MediaStreamSource.Paused -= MediaStreamSource_Paused;
}
catch { }
if (_shoutcastDataReader != null)
{
_shoutcastDataReader.Dispose();
}
}
public async Task ConnectAsync()
{
HttpClient shoutcastHttpClient = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false });
shoutcastHttpClient.DefaultRequestHeaders.Add("icy-metadata", "1");
MediaEncodingProfile audioProfile = null;
try
{
HttpResponseMessage responseMessage = await shoutcastHttpClient.GetAsync(StreamUrl, HttpCompletionOption.ResponseHeadersRead);
ParseResponse(responseMessage.Headers);
_shoutcastDataReader = new DataReader((await responseMessage.Content.ReadAsStreamAsync()).AsInputStream())
{
UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf8,
InputStreamOptions = InputStreamOptions.None
};
if (_metadataInterval < 16000)
{
using (InMemoryRandomAccessStream memoryStream = new InMemoryRandomAccessStream())
{
await _shoutcastDataReader.LoadAsync(_metadataInterval);
_metadataPosition += _metadataInterval;
IBuffer tempBufferOne = _shoutcastDataReader.ReadBuffer(_metadataInterval);
await memoryStream.WriteAsync(tempBufferOne);
await HandleMetadataAsync();
await _shoutcastDataReader.LoadAsync(_metadataInterval);
_metadataPosition += _metadataInterval;
IBuffer tempBufferTwo = _shoutcastDataReader.ReadBuffer(_metadataInterval);
await memoryStream.WriteAsync(tempBufferTwo);
audioProfile = await MediaEncodingProfile.CreateFromStreamAsync(memoryStream);
await HandleMetadataAsync();
}
}
else
{
await _shoutcastDataReader.LoadAsync(16000);
using (IRandomAccessStream stream = _shoutcastDataReader.ReadBuffer(16000).AsStream().AsRandomAccessStream())
{
audioProfile = await MediaEncodingProfile.CreateFromStreamAsync(stream);
}
_metadataPosition += 16000;
}
AudioEncodingProperties audioProperties = AudioEncodingProperties.CreateMp3(audioProfile.Audio.SampleRate, audioProfile.Audio.ChannelCount, audioProfile.Audio.Bitrate / 1000);
AudioStreamDescriptor streamDescriptor = new AudioStreamDescriptor(audioProperties);
MediaStreamSource = new MediaStreamSource(streamDescriptor)
{
BufferTime = TimeSpan.FromSeconds(3),
};
MediaStreamSource.SampleRequested += MediaStreamSource_SampleRequested;
MediaStreamSource.CanSeek = false;
MediaStreamSource.Starting += MediaStreamSource_Starting;
MediaStreamSource.Closed += MediaStreamSource_Closed;
MediaStreamSource.Paused += MediaStreamSource_Paused;
}
catch (Exception ex)
{
throw new ConnectionException(ex.Message, ExceptionCode.ConnectionError);
}
}
private void MediaStreamSource_Paused(MediaStreamSource sender, object args)
{
}
private void MediaStreamSource_Closed(MediaStreamSource sender, MediaStreamSourceClosedEventArgs args)
{
}
private void MediaStreamSource_Starting(MediaStreamSource sender, MediaStreamSourceStartingEventArgs args)
{
}
private void ParseResponse(HttpResponseHeaders headers)
{
foreach (var header in headers)
{
switch (header.Key)
{
case "icy-name":
StationInfo.StationName = header.Value.FirstOrDefault();
break;
case "icy-genre":
StationInfo.StationGenre = header.Value.FirstOrDefault();
break;
case "icy-metaint":
_metadataInterval = Convert.ToUInt16(header.Value.FirstOrDefault());
break;
}
}
}
private async void MediaStreamSource_SampleRequested(MediaStreamSource sender, MediaStreamSourceSampleRequestedEventArgs args)
{
MediaStreamSourceSampleRequest sampleRequest = args.Request;
MediaStreamSourceSampleRequestDeferral requestDeferral = sampleRequest.GetDeferral();
MediaStreamSample sample = null;
try
{
sampleRequest.ReportSampleProgress(25);
if (_metadataInterval - _metadataPosition <= _sampleSize && _metadataInterval - _metadataPosition > 0)
{
uint partial = _metadataInterval - _metadataPosition;
DataReaderLoadOperation loadOperation = _shoutcastDataReader.LoadAsync(partial);
await loadOperation.AsTask(new CancellationTokenSource(_connectionTimeout).Token);
//await _shoutcastDataReader.LoadAsync(partial);
_metadataPosition += _metadataInterval - _metadataPosition;
IBuffer buffer = _shoutcastDataReader.ReadBuffer(partial);
sample = await ParseSampleAsync(buffer);
}
else
{
await HandleMetadataAsync();
sampleRequest.ReportSampleProgress(50);
DataReaderLoadOperation loadOperation = _shoutcastDataReader.LoadAsync(_sampleSize);
await loadOperation.AsTask(new CancellationTokenSource(_connectionTimeout).Token);
//await _shoutcastDataReader.LoadAsync(_sampleSize);
_metadataPosition += _sampleSize;
IBuffer buffer = _shoutcastDataReader.ReadBuffer(_sampleSize);
sample = await ParseSampleAsync(buffer);
}
sampleRequest.Sample = sample;
sampleRequest.ReportSampleProgress(100);
}
catch (Exception)
{
MediaStreamSource.NotifyError(MediaStreamSourceErrorStatus.ConnectionToServerLost);
//throw new ConnectionException(ex.Message, ExceptionCode.ConnectionLost);
}
finally
{
requestDeferral.Complete();
}
}
private async Task HandleMetadataAsync()
{
if (_metadataPosition == _metadataInterval)
{
_metadataPosition = 0;
try
{
await _shoutcastDataReader.LoadAsync(1);
uint lengthByte = _shoutcastDataReader.ReadByte();
if (lengthByte > 0)
{
uint metadataRange = lengthByte * 16;
await _shoutcastDataReader.LoadAsync(metadataRange);
byte[] metadataBytes = new byte[metadataRange];
_shoutcastDataReader.ReadBytes(metadataBytes);
string[] encodedMetadata = new string[]
{
Encoding.GetEncoding("windows-1251").GetString(metadataBytes, 0, metadataBytes.Length - 1),
Encoding.UTF8.GetString(metadataBytes, 0, metadataBytes.Length - 1)
};
ParseSongMetadata(encodedMetadata.FirstOrDefault(x => (x.Contains(";"))));
}
}
catch { }
}
}
private void ParseSongMetadata(string metadata)
{
string track = "";
string artist = "";
string[] semiColonSplit = metadata.Split(';');
KeyValuePair<string, string>[] headers = semiColonSplit.Where(line => line.Contains("=")).Select(line =>
{
string header = line.Substring(0, line.IndexOf("="));
string value = line.Substring(line.IndexOf("=") + 1);
var pair = new KeyValuePair<string, string>(header.ToUpper(), value.Trim('\'').Trim());
return pair;
}).ToArray();
string songInfo = headers.First(x => x.Key == "STREAMTITLE").Value;
if (songInfo.Split('-').Count() >= 2)
{
artist = songInfo.Split(new string[] { " - " }, StringSplitOptions.None)[0].Trim();
track = songInfo.Split(new string[] { " - " }, StringSplitOptions.None)[1].Trim();
}
else
{
track = songInfo.Trim();
artist = "Unknown Artist";
}
MetadataChanged?.Invoke(this, new SMSSMetadataChangedEventArgs(artist, track));
}
//============MP3Parser==============================================================================
private async Task<MediaStreamSample> ParseSampleAsync(IBuffer bufferBytes)
{
MediaStreamSample sample = null;
if (_isStreamRecords)
{
await _recordStream.WriteAsync(bufferBytes);
}
sample = MediaStreamSample.CreateFromBuffer(bufferBytes, _timeOffSet);
sample.KeyFrame = true;
_timeOffSet = _timeOffSet.Add(_sampleDuration);
return sample;
}
}
}