NAudio, парсинг метаданных из потока

257
02 марта 2017, 21:55

Здравствуйте.

Зашел в тупик с реализацией приложения для прослушивания онлайн радио. За основу взята библиотека 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
Answer 1

Т.к. в комментариях форма не позволяет вставить код, продублирую здесь.

Вот метод получения данных:

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;
                    }
                }
            }
        }
READ ALSO
Получить часть массива

Получить часть массива

Сам массив (слишком большой, чтобы поместить сюда)Нужно вывести description (163 строчка)

240
Yii2: Не передается одно значение в модель

Yii2: Не передается одно значение в модель

Я через миграцию добавил в таблицу одну колонку, nameВ контроллере создаю модель, чтобы записать комментарии в нее из вьюхи

532