(C) Eskil Heyn Olsen 1995-2008. This example is a subclass of the Qt QSocketDevice class. It was written for the MythTV project to add SHOUTcast support to the MythMusic plugin. The ShoutCastIODevice class implements a QIODevice by subclassing QSocketDevice, thereby making it useable by the MythMusic audio decoders that all read from a QIODevice. On reads, the ShoutCastIODevice will keep track of how far into the stream the decoder has read, and when it reaches the metadata tag in the shoutcast stream, the ShoutCastIODevice will parse the metadata and send back to the playback UI, then read more bytes from the audio stream and pass to the decoder. Header file: /* Shoutcast decoder for MythTV. Eskil Heyn Olsen, 2005, distributed under the GPL as part of mythtv. */ #ifndef SHOUTCAST_H_ #define SHOUTCAST_H_ #include "config.h" #include "decoder.h" #include "decoderhandler.h" #include #include #include #include #include // debug, collect kb/s info in DecoderLauncherShoutCast #include #include typedef QMap ShoutCastMetaMap; class ShoutCastHeaderRequest { public: ShoutCastHeaderRequest(); ShoutCastHeaderRequest(const QUrl &url); ~ShoutCastHeaderRequest(); const char *data(void) { return m_data.data(); } uint size(void) { return m_data.size(); } private: void setUrl(const QUrl &url); QByteArray m_data; }; class ShoutCastHeaderResponse { public: ShoutCastHeaderResponse(); ~ShoutCastHeaderResponse(); int getMetaint(void); int getBitrate(void); QString getGenre(void); QString getName(void); int getStatus(void); bool isICY(void); QString getContent(void); QString getLocation(void); QString getString(const QString &key) { return m_data[key]; } int getInt(const QString &key) { return m_data[key].toInt(); } bool fillResponse(const char *data, int len); private: QMap m_data; }; class ShoutCastMetaParser { public: ShoutCastMetaParser(); ~ShoutCastMetaParser(); void setMetaFormat(const QString &metaformat); ShoutCastMetaMap parseMeta(QString meta); private: QString m_meta_format; int m_meta_artist_pos; int m_meta_title_pos; int m_meta_album_pos; }; class ShoutCastIODevice : public QObject, public QSocketDevice { Q_OBJECT public: enum State { NOTCONNECTED, RESOLVING, CANTRESOLVE, CANTCONNECT, READING_HEADER, STREAMING, STREAMING_META, STOPPED }; ShoutCastIODevice(QObject *parent = 0); ~ShoutCastIODevice(); bool open(int mode); void close(); Q_LONG readBlock(char *data, Q_ULONG maxlen); void connectToHost(const QString & host, Q_UINT16 port); int getResponse(ShoutCastHeaderResponse &response); signals: void meta(const QString &metadata); void changedState(ShoutCastIODevice::State newstate); private slots: void dnsResultsReady(); private: void switchToState(State s); int parseStream(char *data, Q_ULONG maxlen); bool parseMeta(void); void deleteDns(void); void tryAllDnsResults(QValueList); void gotResponse(ShoutCastHeaderResponse &response); void doKbPerSecond(int bytes_read); ShoutCastHeaderResponse m_response; State m_state; Q_ULONG m_bytes_till_next_meta; QString m_last_metadata; QString m_response_buf; QDns *m_dns; Q_UINT16 m_port; // debug, collect kb/s info struct timeval m_sample_tv; uint m_bytes; }; class DecoderLauncherShoutCast : public DecoderLauncher { Q_OBJECT public: DecoderLauncherShoutCast(DecoderHandler *parent); ~DecoderLauncherShoutCast(); void start(); void stop(); QIODevice *getInput(void) { return m_input; } protected slots: void periodicallyCheckResponse(void); void periodicallyCheckBuffered(void); void shoutcastMeta(const QString &metadata); void shoutcastChangedState(ShoutCastIODevice::State newstate); private: void socketConnected(void); void socketClosed(void); void socketError(int); int isResponseOK(); void makeIODevice(); void closeIODevice(); QTimer *m_timer; ShoutCastIODevice *m_input; }; #endif /* SHOUTCAST_H_ */ Source file: /* Shoutcast decoder for MythTV. Eskil Heyn Olsen, 2005, distributed under the GPL as part of mythtv. */ #include #include #include #include "shoutcast.h" #include "metadata.h" #include /****************************************************************************/ ShoutCastHeaderRequest::ShoutCastHeaderRequest() { } ShoutCastHeaderRequest::ShoutCastHeaderRequest(const QUrl &url) { setUrl (url); } ShoutCastHeaderRequest::~ShoutCastHeaderRequest() {} void ShoutCastHeaderRequest::setUrl(const QUrl &url) { QString hdr; hdr = QString("GET %1 HTTP/1.1\r\n" "Host: %2\r\n" "User-Agent: mythmusic/svn\r\n" "Keep-Alive:\r\n" "Connection: TE, Keep-Alive\r\n" "TE: trailers\r\n" "icy-metadata:1\r\n" "\r\n").arg(url.path()).arg(url.host()); m_data.duplicate(hdr.ascii(), hdr.length()); } /****************************************************************************/ ShoutCastHeaderResponse::ShoutCastHeaderResponse() { } ShoutCastHeaderResponse::~ShoutCastHeaderResponse() { } int ShoutCastHeaderResponse::getBitrate(void) { return getInt("icy-br"); } int ShoutCastHeaderResponse::getMetaint(void) { return getInt("icy-metaint"); } QString ShoutCastHeaderResponse::getGenre(void) { return getString("icy-genre"); } QString ShoutCastHeaderResponse::getName(void) { return getString("icy-name"); }; int ShoutCastHeaderResponse::getStatus(void) { return getInt("status"); } bool ShoutCastHeaderResponse::isICY(void) { return QString(m_data["protocol"]).left(3) == "ICY"; } QString ShoutCastHeaderResponse::getContent(void) { return getString("content-type"); } QString ShoutCastHeaderResponse::getLocation(void) { return getString("location"); } bool ShoutCastHeaderResponse::fillResponse(const char *s, int l) { QCString d(s, l); // check each line for (;;) { int pos = d.find("\r"); if (pos == -1) break; // Extract the line QCString snip(d.data(), pos + 1); d.remove(0, pos + 1); while (d.at(0) == '\r' || d.at(0) == '\n') d.remove(0, 1); if (pos == 0) break; if (snip.left(4) == "ICY ") { int space = snip.find(' '); m_data["protocol"] = "ICY"; QString tmp = snip.mid(space).simplifyWhiteSpace (); int second_space = tmp.find (' '); if (second_space > 0) { m_data["status"] = tmp.left (second_space); } else { m_data["status"] = tmp; } } else if (snip.left(7) == "HTTP/1.") { int space = snip.find(' '); m_data["protocol"] = snip.left(space); QString tmp = snip.mid(space).simplifyWhiteSpace (); int second_space = tmp.find (' '); if (second_space > 0) { m_data["status"] = tmp.left (second_space); } else { m_data["status"] = tmp; } } else if (snip.left(9).lower() == "location:") { m_data["location"] = snip.mid(9).stripWhiteSpace(); } else if (snip.left(13).lower() == "content-type:") { m_data["content-type"] = snip.mid(13).stripWhiteSpace(); } else if (snip.left(4) == "icy-") { int pos = snip.find(':'); QString key = snip.left(pos); m_data[key.ascii()] = snip.mid(pos+1).stripWhiteSpace(); } } return true; } /****************************************************************************/ ShoutCastMetaParser::ShoutCastMetaParser () { } ShoutCastMetaParser::~ShoutCastMetaParser () { } void ShoutCastMetaParser::setMetaFormat(const QString &metaformat) { /* We support these metatags : %a - artist %t - track %b - album %r - random bytes */ m_meta_format = metaformat; m_meta_artist_pos = 0; m_meta_title_pos = 0; m_meta_album_pos = 0; int assign_index = 1; int pos = 0; pos = m_meta_format.find("%", pos); while (pos >= 0) { pos++; QChar ch = m_meta_format.at(pos); if (ch == '%') { pos++; } else if (ch == 'r' || ch == 'a' || ch == 'b' || ch == 't') { if (ch == 'a') m_meta_artist_pos = assign_index; if (ch == 'b') m_meta_album_pos = assign_index; if (ch == 't') m_meta_title_pos = assign_index; assign_index++; } else fprintf (stderr, "CastDecoder: malformed metaformat '%s'\n", m_meta_format.ascii()); pos = m_meta_format.find("%", pos); } m_meta_format.replace("%a", "(.*)"); m_meta_format.replace("%t", "(.*)"); m_meta_format.replace("%b", "(.*)"); m_meta_format.replace("%r", "(.*)"); m_meta_format.replace("%%", "%"); } ShoutCastMetaMap ShoutCastMetaParser::parseMeta(QString meta) { QCString metastring(meta); ShoutCastMetaMap result; int title_begin_pos = metastring.find("StreamTitle='"); int title_end_pos; if (title_begin_pos >= 0) { title_begin_pos += 13; title_end_pos = metastring.find("';", title_begin_pos); QCString title = metastring.mid(title_begin_pos, title_end_pos - title_begin_pos); QRegExp rx; rx.setPattern(m_meta_format); if (rx.search (title) != -1) { VERBOSE(VB_PLAYBACK, QString("ShoutCast: Meta : '%1'"). arg(meta)); VERBOSE(VB_PLAYBACK, QString("ShoutCast: Parsed as: '%1' by '%2'"). arg(rx.cap(m_meta_title_pos)). arg(rx.cap(m_meta_artist_pos))); if (m_meta_title_pos > 0) result["title"] = rx.cap(m_meta_title_pos); if (m_meta_artist_pos > 0) result["artist"] = rx.cap(m_meta_artist_pos); if (m_meta_album_pos > 0) result["album"] = rx.cap(m_meta_album_pos); } } return result; } /****************************************************************************/ ShoutCastIODevice::ShoutCastIODevice(QObject *parent) : QObject (parent) { m_dns = 0; setState(0); switchToState(NOTCONNECTED); } ShoutCastIODevice::~ShoutCastIODevice() { deleteDns(); } bool ShoutCastIODevice::open(int mode) { if (!QSocketDevice::open(mode)) return false; setReceiveBufferSize(49152); switchToState(READING_HEADER); return true; } void ShoutCastIODevice::close() { if (m_state != STOPPED) { switchToState(STOPPED); } return QSocketDevice::close(); } Q_LONG ShoutCastIODevice::readBlock(char *data, Q_ULONG maxlen) { #if defined(QT_CHECK_NULL) if (data == 0) { qWarning("ShoutCastIODevice::readBlock: null pointer error"); return -1; } #endif #if defined(QT_CHECK_STATE) if (! isOpen()) { qWarning( "ShoutCastIODevice::readBlock: iobuffer not open" ); return -1; } if (! isReadable()) { qWarning( "ShoutCastIODevice::readBlock: read operation not permitted" ); return -1; } #endif if (! isOpen()) return 0; if (m_state == READING_HEADER) return QSocketDevice::readBlock(data, maxlen); int result = 0; if (m_state == STREAMING_META && parseMeta()) switchToState(STREAMING); if (m_state == STREAMING) { result = parseStream(data, maxlen); if (m_bytes_till_next_meta == 0) switchToState(STREAMING_META); } if (m_state != STOPPED) VERBOSE(VB_NETWORK, QString("ShoutCast: %1 kb in buffer, btnm=%2/%3 state=%4, rb=%5"). arg(bytesAvailable()/1024, 4). arg(m_bytes_till_next_meta, 4). arg(m_response.getMetaint()). arg(m_state). arg(result)); else VERBOSE(VB_NETWORK, QString("ShoutCast: stopped")); return result; } void ShoutCastIODevice::switchToState(State state) { switch(state) { case STREAMING: if (m_state == STREAMING_META) m_bytes_till_next_meta = m_response.getMetaint(); break; case STOPPED: setState(0); setStatus(0); break; default: break; } m_state = state; emit changedState(m_state); } void ShoutCastIODevice::gotResponse(ShoutCastHeaderResponse &response) { m_bytes_till_next_meta = m_response.getMetaint(); response = m_response; switchToState(STREAMING); // debug, collect kb/s info gettimeofday (&m_sample_tv, NULL); m_bytes = 0; } int ShoutCastIODevice::getResponse(ShoutCastHeaderResponse &response) { #if defined(QT_CHECK_STATE) if (! isOpen()) { qWarning("ShoutCastIODevice::getResponse: iobuffer not open"); return -1; } if (! isReadable()) { qWarning("ShoutCastIODevice::getResponse: read operation not permitted"); return -1; } #endif if (! isOpen()) return -1; while (1) { if (bytesAvailable() <= 0) break; char data[2048]; int len = readLine(data, sizeof(data)-1); m_response_buf.append (data); // keep reading until we've got a line if (len == sizeof(data) - 1) continue; if (m_response_buf == "\r\n") { gotResponse (response); return 0; } m_response.fillResponse(m_response_buf.data(), m_response_buf.length()); m_response_buf = ""; } return 1; } void ShoutCastIODevice::doKbPerSecond(int bytes_read) { // debug, collect kb/s info m_bytes += bytes_read; struct timeval tv; gettimeofday(&tv, NULL); float msecs = ((tv.tv_sec * 1000000 + tv.tv_usec) - (m_sample_tv.tv_sec * 1000000 + m_sample_tv.tv_usec)) / 1000.0; if (msecs > 5000.0) { VERBOSE(VB_NETWORK, QString("ShoutCast: download speed, %1 kb in %2 s = %3 kb/s"). arg(m_bytes/1024.0, 1, 'f', 1). arg(msecs/1000.0, 1, 'f', 1). arg((m_bytes/1024)/(msecs/1000.0), 3, 'f', 1)); m_sample_tv = tv; m_bytes = 0; } } int ShoutCastIODevice::parseStream(char *data, Q_ULONG maxlen) { if (maxlen > m_bytes_till_next_meta) maxlen = m_bytes_till_next_meta; int result = QSocketDevice::readBlock(data, maxlen); doKbPerSecond(result); m_bytes_till_next_meta -= result; return result; } bool ShoutCastIODevice::parseMeta() { char ch; QSocketDevice::readBlock (&ch, 1); int metasz = ch; if (metasz < 0) metasz = 0x100 + metasz; metasz *= 16; if (metasz == 0) return true; VERBOSE(VB_NETWORK, QString("ShoutCast: Reading %1 bytes of meta").arg(metasz)); char *tmp = (char*)alloca(metasz + 1); int len = QSocketDevice::readBlock(tmp, metasz); // this should not happen since we're reading in blocking mode if (len != metasz) { switchToState(STOPPED); return false; } tmp[len] = '\0'; QString metadata(tmp); m_bytes += len; // Avoid sending signals if the data hasn't changed if (metadata == m_last_metadata) return true; m_last_metadata = metadata; emit meta(metadata); return true; } void ShoutCastIODevice::connectToHost(const QString & host, Q_UINT16 port) { assert(m_state == NOTCONNECTED); switchToState(RESOLVING); m_port = port; deleteDns(); m_dns = new QDns(host, QDns::A); dnsResultsReady(); if (m_state == RESOLVING) { QObject::connect(m_dns, SIGNAL(resultsReady()), this, SLOT(dnsResultsReady())); } } void ShoutCastIODevice::deleteDns() { if (m_dns == 0) return; m_dns->deleteLater(); m_dns = 0; } void ShoutCastIODevice::dnsResultsReady() { if (m_dns->isWorking() || m_state != RESOLVING) return; QValueList addrs = m_dns->addresses(); if (addrs.isEmpty()) { switchToState(CANTRESOLVE); return; } tryAllDnsResults(addrs); } void ShoutCastIODevice::tryAllDnsResults(QValueList addrs) { for (QValueList::Iterator addr = addrs.begin(); addr != addrs.end (); addr++) { if (QSocketDevice::connect(*addr, m_port) && error() == NoError) { open(IO_ReadWrite); // Set to blocking mode, otherwise reads may return zero causing // decoders to stop reading (since they were written to handle local // files. setBlocking(true); break; } } if (m_state == RESOLVING) switchToState(CANTCONNECT); } /**********************************************************************/ DecoderLauncherShoutCast::DecoderLauncherShoutCast(DecoderHandler *parent) : DecoderLauncher(parent) { m_input = NULL; m_timer = NULL; } DecoderLauncherShoutCast::~DecoderLauncherShoutCast() { closeIODevice(); // remaining objects (m_input, m_timer) are deallocated by the // qobject model. } void DecoderLauncherShoutCast::makeIODevice() { closeIODevice(); m_input = new ShoutCastIODevice(this); connect(m_input, SIGNAL(meta(const QString&)), this, SLOT(shoutcastMeta(const QString&))); connect(m_input, SIGNAL(changedState(ShoutCastIODevice::State)), this, SLOT(shoutcastChangedState(ShoutCastIODevice::State))); } void DecoderLauncherShoutCast::closeIODevice() { if (m_input && m_input->isOpen()) m_input->close(); } void DecoderLauncherShoutCast::start() { VERBOSE(VB_PLAYBACK, QString("DecoderLauncherShoutCast %1").arg(m_url.toString())); makeIODevice(); doOperationStart("Connecting"); m_input->connectToHost(m_url.host(), m_url.port() != -1 ? m_url.port() : 80); } void DecoderLauncherShoutCast::stop() { if (m_input->isOpen()) m_input->close(); doOperationStop(); Metadata mdata(*m_meta); mdata.setTitle("Stopped"); mdata.setArtist(""); mdata.setLength(-1); DecoderHandlerEvent ev(mdata); dispatch(ev); } void DecoderLauncherShoutCast::socketConnected(void) { ShoutCastHeaderRequest req(m_url); m_input->writeBlock(req.data(), req.size()); doOperationStart("Buffering"); m_timer = new QTimer(this); connect(m_timer, SIGNAL(timeout()), this, SLOT(periodicallyCheckResponse())); m_timer->start(500); } void DecoderLauncherShoutCast::periodicallyCheckResponse(void) { int res = isResponseOK(); if (res == 0) { m_timer->disconnect(); connect(m_timer, SIGNAL(timeout()), this, SLOT(periodicallyCheckBuffered())); } else if (res < 0) { m_timer->stop(); doFailed("Cannot parse this stream"); } } void DecoderLauncherShoutCast::periodicallyCheckBuffered(void) { VERBOSE(VB_NETWORK, QString("DecoderLauncherShoutCast: prebuffered %1/%2KB"). arg(m_input->bytesAvailable()/1024).arg(m_input->receiveBufferSize()/(2*1024))); if (m_input->bytesAvailable() < m_input->receiveBufferSize()/2) return; doConnectDecoder("create-mp3-decoder.mp3"); m_timer->stop(); } void DecoderLauncherShoutCast::shoutcastMeta(const QString &metadata) { ShoutCastMetaParser parser; parser.setMetaFormat(m_meta->CompilationArtist()); ShoutCastMetaMap map = parser.parseMeta(metadata); Metadata mdata(*m_meta); mdata.setTitle(map["title"]); mdata.setArtist(map["artist"]); //mdata.setAlbum(map["album"]); mdata.setLength(-1); DecoderHandlerEvent ev(mdata); dispatch(ev); } void DecoderLauncherShoutCast::shoutcastChangedState(ShoutCastIODevice::State state) { if (state == ShoutCastIODevice::READING_HEADER) return socketConnected(); if (state == ShoutCastIODevice::STOPPED) stop(); } int DecoderLauncherShoutCast::isResponseOK() { ShoutCastHeaderResponse response; int res = m_input->getResponse(response); if (res != 0) return res; if (! response.isICY() && response.getStatus() == 302 && ! response.getLocation().isEmpty()) { // restart with new location... setUrl(response.getLocation()); start(); return 1; } if (! response.isICY() || response.getStatus() != 200) return -1; return 0; }