Здравствуйте.
Зашел в тупик с реализацией приложения для прослушивания онлайн радио. За основу взята библиотека NAudio. За небольшими правками реализация не отличается от данного примера, опубликованного вместе с библиотекой. Реализация парсера взята из этого примера.
Суть проблемы. Мне необходимо осуществить парсинг метаданных из потока для получения исполнителя и песни. В теории, при посыле запроса с заголовком:
_webRequest.Headers.Add("Icy-MetaData", "1");
нам должен прийти ответ с заголовком "icy-metaint" со значением (к примеру: 16000) позиция в потоке, содержащей метаданные исполнителя и песни. Парсинг (по идее) должен осуществляться в кастомной реализации Stream. Вот моя реализация:
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;
}
}
}
}
}
Вместо распарсенной строки мне приходит набор символов:
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
Т.к. в комментариях форма не позволяет вставить код, продублирую здесь.
Вот метод получения данных:
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();
}
}
Вот класс переопределенного потока:
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;
}
}
}
}
}
Загвоздка состоит в том, как имея на руках значение MetaInt (позиция метаданных в потоке) вытащить данные о текущей композиции в эфире.
А именно метод Read:
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;
}
И метод для парсинга:
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;
}
}
}
}
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты
Сам массив (слишком большой, чтобы поместить сюда)Нужно вывести description (163 строчка)
Я через миграцию добавил в таблицу одну колонку, nameВ контроллере создаю модель, чтобы записать комментарии в нее из вьюхи