mirror of
https://github.com/WinampDesktop/winamp.git
synced 2024-09-24 15:54:12 +00:00
568 lines
19 KiB
C++
568 lines
19 KiB
C++
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#endif
|
|
#include <stdio.h>
|
|
#include "protocol_shoutcast2Client.h"
|
|
#include "ripList.h"
|
|
#include "stats.h"
|
|
#include "streamData.h"
|
|
#include "w3cLog.h"
|
|
#include "metadata.h"
|
|
#include "uvox2Common.h"
|
|
#include "global.h"
|
|
#include "bandwidth.h"
|
|
#include "MP3Header.h"
|
|
#include "ADTSHeader.h"
|
|
#include "file/fileUtils.h"
|
|
#include "services/stdServiceImpl.h"
|
|
|
|
using namespace std;
|
|
using namespace uniString;
|
|
using namespace stringUtil;
|
|
|
|
#define LOGNAME "DST"
|
|
#define DEBUG_LOG(...) do { if (gOptions.shoutcast2ClientDebug()) DLOG(__VA_ARGS__); } while(0)
|
|
#define AD_DEBUG_LOG(...) do { if (gOptions.adMetricsDebug()) DLOG(__VA_ARGS__); } while(0)
|
|
|
|
protocol_shoutcast2Client::protocol_shoutcast2Client (protocol_HTTPStyle &hs, const streamData::streamID_t streamID,
|
|
const uniString::utf8 &hostName, const uniString::utf8 &addr,const uniString::utf8 &XFF, const bool cdnSlave)
|
|
|
|
: protocol_shoutcastClient (hs, streamID, hostName, addr, XFF, streamData::SHOUTCAST2), m_cdnSlave(cdnSlave)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__, LOGNAME, streamID);
|
|
m_state = &protocol_shoutcast2Client::state_AttachToStream;
|
|
m_nextState = NULL;
|
|
}
|
|
|
|
protocol_shoutcast2Client::~protocol_shoutcast2Client() throw()
|
|
{
|
|
cleanup("Shoutcast 2", gOptions.shoutcast2ClientDebug(), true);
|
|
}
|
|
|
|
///////////////////////////////////// W3C Logging //////////////////////////////////////////////
|
|
|
|
// create W3C entry. Entries describe the duration a client has listened to a specific
|
|
// title. the entry is generated on a title change, or when the client disconnects
|
|
void protocol_shoutcast2Client::logW3C() throw()
|
|
{
|
|
if (gOptions.w3cEnable())
|
|
{
|
|
vector<utf8::value_type> md;
|
|
if (!m_lastMetadata.empty())
|
|
{
|
|
// loop through packets and reassemble. Since we put this stuff in there, we don't need to do integrity checks
|
|
size_t total_data_size = m_lastMetadata.size();
|
|
for (size_t x = 0; x < total_data_size;)
|
|
{
|
|
if ((UV2X_OVERHEAD + UV2X_META_HDR_SIZE) > (total_data_size - x))
|
|
{
|
|
break; // out of data
|
|
}
|
|
|
|
const uv2xHdr *voxHdr = (const uv2xHdr*)(&(m_lastMetadata[x]));
|
|
const __uint8 *contents = (const __uint8 *)((&(m_lastMetadata[x])) + UV2X_HDR_SIZE);
|
|
const __uint8* metadataContents = contents + UV2X_META_HDR_SIZE;
|
|
const size_t metadataContentsSize = ntohs(voxHdr->msgLen) - UV2X_META_HDR_SIZE;
|
|
|
|
if ((UV2X_OVERHEAD + UV2X_META_HDR_SIZE + metadataContentsSize) > (total_data_size - x))
|
|
{
|
|
break; // out of data
|
|
}
|
|
|
|
md.insert(md.end(), metadataContents,metadataContents + metadataContentsSize);
|
|
x += UV2X_OVERHEAD + UV2X_META_HDR_SIZE + metadataContentsSize;
|
|
}
|
|
}
|
|
|
|
utf8 md_s(md.begin(), md.end());
|
|
|
|
// do a sanity check when trying to form the metadata if a uvox 2 connection
|
|
// is attempted from an older Winamp client e.g. 5.54 - means a connection
|
|
// can be made from the client without it aborting if there's no metadata.
|
|
utf8 title;
|
|
if (!md_s.empty())
|
|
{
|
|
title = metadata::get_song_title_from_3902(md_s);
|
|
}
|
|
|
|
doLogW3C(title);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
void protocol_shoutcast2Client::timeSlice() throw(exception)
|
|
{
|
|
int ret = doTimeSlice(true);
|
|
if (ret == 1)
|
|
{
|
|
m_state = &protocol_shoutcastClient::state_Stream;
|
|
return;
|
|
}
|
|
else if (ret == 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
(this->*m_state)();
|
|
}
|
|
|
|
|
|
void protocol_shoutcast2Client::setCallback (protocol_shoutcastClient::state_t callback, protocol_shoutcastClient::state_t next)
|
|
{
|
|
m_state = callback ? callback : m_nextState;
|
|
m_nextState = callback ? next : NULL;
|
|
}
|
|
|
|
|
|
void protocol_shoutcast2Client::state_Close() throw(exception)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__);
|
|
|
|
m_result.done();
|
|
}
|
|
|
|
void protocol_shoutcast2Client::state_SendText() throw(exception)
|
|
{
|
|
#if defined(_DEBUG) || defined(DEBUG)
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__);
|
|
#endif
|
|
|
|
if (sendText())
|
|
{
|
|
m_state = m_nextState;
|
|
}
|
|
}
|
|
|
|
// find the appropriate stream and try to attach to it
|
|
void protocol_shoutcast2Client::state_AttachToStream() throw(exception)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__);
|
|
|
|
int read_bitrate = 0;
|
|
m_streamData = streamData::accessStream(m_streamID);
|
|
if (!m_streamData)
|
|
{
|
|
if (processReject("Shoutcast 2", bandWidth::CLIENT_V2_SENT, MSG_HTTP404,
|
|
MSG_HTTP404_LEN, &read_bitrate, 0, true))
|
|
{
|
|
goto fall_through;
|
|
}
|
|
|
|
m_state = &protocol_shoutcast2Client::state_SendText;
|
|
m_nextState = &protocol_shoutcast2Client::state_Close;
|
|
}
|
|
else
|
|
{
|
|
fall_through:
|
|
const utf8 movedUrl = gOptions.stream_movedUrl(m_streamID);
|
|
if (movedUrl.empty())
|
|
{
|
|
// we use this to control the cdn mode so we'll only provide the
|
|
// headers needed if it's all enabled and the client asks for it
|
|
string cdn;
|
|
if (isCDNMaster(m_streamID) && m_cdnSlave)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + "CDN slave request received by master", LOGNAME, m_streamID);
|
|
utf8 authhash = (m_streamData ? m_streamData->streamAuthhash() : "");
|
|
if (yp2::isValidAuthhash(authhash))
|
|
{
|
|
utf8 key = XTEA_encipher(authhash.c_str(), authhash.size(), bob().c_str(), bob().size());
|
|
cdn = "cdn-master:1\r\n"
|
|
"cdn-token:" + key.hideAsString() + "\r\n";
|
|
m_clientType = ((streamData::source_t)(m_clientType | streamData::SC_CDN_SLAVE));
|
|
}
|
|
else
|
|
{
|
|
DEBUG_LOG(m_clientLogString + "CDN slave request not sent - invalid authhash provided", LOGNAME, m_streamID);
|
|
}
|
|
}
|
|
|
|
const int add = processAdd("FLV", bandWidth::CLIENT_V2_SENT,
|
|
MSG_HTTP404, MSG_HTTP404_LEN, movedUrl,
|
|
(m_streamData ? m_streamData->streamBackupServer() : ""));
|
|
if (add != 1)
|
|
{
|
|
m_state = &protocol_shoutcast2Client::state_SendText;
|
|
m_nextState = &protocol_shoutcast2Client::state_Close;
|
|
}
|
|
else
|
|
{
|
|
utf8 pub = "0", genre = "";
|
|
const bool isPodcast = (!m_streamData && (gOptions.getBackupLoop(m_streamID) == 1));
|
|
if (!isPodcast)
|
|
{
|
|
pub = (m_streamData ? tos(m_streamData->streamPublic()) : "1");
|
|
if (m_streamData)
|
|
{
|
|
if (isUserAgentRelay(toLower(m_userAgent)) && (!m_streamData->allowPublicRelay()))
|
|
{
|
|
pub = "0";
|
|
}
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
if (!m_streamData->m_streamInfo.m_streamGenre[i].empty())
|
|
{
|
|
genre += (i ? ", " : "") + m_streamData->m_streamInfo.m_streamGenre[i];
|
|
}
|
|
}
|
|
}
|
|
// if running from a backup file then no need to set the states
|
|
else
|
|
{
|
|
pub = toLower(gOptions.stream_publicServer(m_streamID));
|
|
if (pub.empty())
|
|
{
|
|
pub = toLower(gOptions.publicServer());
|
|
}
|
|
if (pub == "always")
|
|
{
|
|
pub = "1";
|
|
}
|
|
else if (pub == "never")
|
|
{
|
|
pub = "0";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO how do we handle podcasts for 2.x
|
|
// as we should somehow swap to 1.x
|
|
}
|
|
|
|
utf8 title = (m_streamData ? m_streamData->streamName() : gOptions.stream_backupTitle(m_streamID));
|
|
if (!m_streamData)
|
|
{
|
|
if (!gOptions.read_stream_backupTitle(m_streamID))
|
|
{
|
|
title = gOptions.backupTitle();
|
|
}
|
|
|
|
if (title.empty())
|
|
{
|
|
title = gOptions.stream_backupFile(m_streamID);
|
|
if (!gOptions.read_stream_backupFile(m_streamID))
|
|
{
|
|
title = gOptions.backupFile();
|
|
}
|
|
|
|
if (!title.empty())
|
|
{
|
|
title = fileUtil::stripSuffix(fileUtil::stripPath(title));
|
|
}
|
|
}
|
|
}
|
|
|
|
m_OKResponse = MSG_UVOX_HTTP200 +
|
|
"icy-pub:" + pub + "\r\n" + cdn +
|
|
"Ultravox-SID:" + tos(m_streamID) + "\r\n" +
|
|
"Ultravox-Bitrate:" + tos(m_streamData ? m_streamData->streamAvgBitrate() : (read_bitrate * 1000)) + "\r\n" +
|
|
"Ultravox-Samplerate:" + tos(m_streamData ? m_streamData->streamSampleRate() : 0) + "\r\n" +
|
|
"Ultravox-Title:" + title + "\r\n" +
|
|
"Ultravox-Genre:" + genre + "\r\n" +
|
|
"Ultravox-URL:" + (m_streamData ? m_streamData->streamURL() : (utf8)""/*"TODO"*/) + "\r\n" +
|
|
"Ultravox-Max-Msg:" + tos(MAX_PAYLOAD_SIZE) + "\r\n" +
|
|
"Ultravox-Class-Type:" + tohex(m_streamData ? m_streamData->streamUvoxDataType() : MP3_DATA) + "\r\n" +
|
|
(m_streamData && m_streamData->streamIsVBR() ? "Ultravox-VBR:1\r\n" : "") +
|
|
(gOptions.clacks() ? "X-Clacks-Overhead:GNU Terry Pratchett\r\n\r\n" : "\r\n");
|
|
|
|
DEBUG_LOG(m_clientLogString + "Sending [" + eol() + stripWhitespace(m_OKResponse) + eol() + "]");
|
|
m_outBuffer = m_OKResponse.c_str();
|
|
bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_OKResponse.size()));
|
|
m_state = &protocol_shoutcast2Client::state_SendText;
|
|
if (!m_headRequest)
|
|
{
|
|
m_nextState = &protocol_shoutcast2Client::state_InitiateStream;
|
|
}
|
|
else
|
|
{
|
|
m_removeClientFromStats = false;
|
|
m_ignoreDisconnect = true;
|
|
m_nextState = &protocol_shoutcast2Client::state_Close;
|
|
}
|
|
m_result.write();
|
|
m_result.timeoutSID(m_streamID);
|
|
m_result.run();
|
|
|
|
// when the client is added, we get back the unique id of the connection
|
|
// but we now check for being > 0 as we need to filter out some of the
|
|
// YP connections from being counted as valid clients for stats, etc
|
|
reportNewListener("Shoutcast 2");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if we get to here then we attempt to redirect the clients to the moved url
|
|
// which is useful if the stream has moved hosting or it has been deprecated.
|
|
streamMovedOrRejected("Shoutcast 2", bandWidth::CLIENT_V2_SENT, movedUrl, 2);
|
|
m_state = &protocol_shoutcast2Client::state_SendText;
|
|
m_nextState = &protocol_shoutcast2Client::state_Close;
|
|
}
|
|
}
|
|
}
|
|
|
|
void protocol_shoutcast2Client::state_SendCachedMetadata() throw(exception)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__);
|
|
|
|
if (sendDataBuffer(m_streamID, m_outBuffer, m_outBufferSize, m_clientLogString))
|
|
{
|
|
bool playingAlbumArt = false;
|
|
|
|
// send intro file if we have it
|
|
acquireIntroFile(true);
|
|
|
|
// see if there's any albumart and attempt to send stream and then playing or whatever is present
|
|
m_cachedMetadata = (m_streamData ? m_streamData->getSc21StreamAlbumArt(0xFFFFFFFF) : streamData::uvoxMetadata_t());
|
|
if (m_cachedMetadata.empty())
|
|
{
|
|
m_cachedMetadata = m_streamData->getSc21PlayingAlbumArt(m_readPtr);
|
|
playingAlbumArt = true;
|
|
}
|
|
|
|
if (m_cachedMetadata.empty())
|
|
{
|
|
// send intro file if we have it
|
|
acquireIntroFile(true);
|
|
m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntroFile);
|
|
}
|
|
else
|
|
{
|
|
bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_cachedMetadata.size()));
|
|
m_outBuffer = (uniString::utf8::value_type*)&(m_cachedMetadata[0]); // slam cast so we can reuse variable
|
|
m_state = (playingAlbumArt == false ? &protocol_shoutcast2Client::state_SendCachedStreamAlbumArt : &protocol_shoutcast2Client::state_SendCachedPlayingAlbumArt);
|
|
}
|
|
}
|
|
}
|
|
|
|
void protocol_shoutcast2Client::state_InitiateStream() throw(exception)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__);
|
|
|
|
resetReadPtr(true);
|
|
|
|
m_cachedMetadata = (m_streamData ? m_streamData->getSc21Metadata(m_readPtr) : streamData::uvoxMetadata_t());
|
|
|
|
if (m_cachedMetadata.empty())
|
|
{
|
|
// send intro file if we have it
|
|
acquireIntroFile(true);
|
|
m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntro);
|
|
}
|
|
else
|
|
{
|
|
bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_cachedMetadata.size()));
|
|
m_outBuffer = (uniString::utf8::value_type*)&(m_cachedMetadata[0]); // slam cast so we can reuse variable
|
|
m_state = &protocol_shoutcast2Client::state_SendCachedMetadata;
|
|
}
|
|
|
|
m_result.run();
|
|
}
|
|
|
|
void protocol_shoutcast2Client::state_SendCachedStreamAlbumArt() throw(exception)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__ + "sending stream albumart: " + tos(m_outBufferSize) +
|
|
" bytes, mime type: " + tos(m_streamData->streamAlbumArtMime()));
|
|
|
|
if (sendDataBuffer(m_streamID, m_outBuffer, m_outBufferSize, m_clientLogString))
|
|
{
|
|
// if all went ok then look to send the playing album art if it is present otherwise do intro / stream data
|
|
m_cachedMetadata = m_streamData->getSc21PlayingAlbumArt(m_readPtr);
|
|
|
|
if (m_cachedMetadata.empty())
|
|
{
|
|
// send intro file if we have it
|
|
acquireIntroFile(true);
|
|
m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntroFile);
|
|
}
|
|
else
|
|
{
|
|
bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_cachedMetadata.size()));
|
|
m_outBuffer = (uniString::utf8::value_type*)&(m_cachedMetadata[0]); // slam cast so we can reuse variable
|
|
m_state = &protocol_shoutcast2Client::state_SendCachedPlayingAlbumArt;
|
|
}
|
|
}
|
|
}
|
|
|
|
void protocol_shoutcast2Client::state_SendCachedPlayingAlbumArt() throw(exception)
|
|
{
|
|
DEBUG_LOG(m_clientLogString + __FUNCTION__ + "sending playing albumart: " + tos(m_outBufferSize) +
|
|
" bytes, mime type: " + tos(m_streamData->streamPlayingAlbumArtMime()));
|
|
|
|
if (sendDataBuffer(m_streamID, m_outBuffer, m_outBufferSize, m_clientLogString))
|
|
{
|
|
// send intro file if we have it
|
|
acquireIntroFile(true);
|
|
m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntroFile);
|
|
}
|
|
}
|
|
|
|
|
|
void protocol_shoutcast2Client::return_403(void)
|
|
{
|
|
protocol_shoutcastClient::return_403();
|
|
m_state = &protocol_shoutcast2Client::state_SendText;
|
|
m_nextState = &protocol_shoutcast2Client::state_Close;
|
|
}
|
|
|
|
|
|
const int protocol_shoutcast2Client::doFrameSync(const int type, const bool debug, const int len,
|
|
const int offset, const std::vector<__uint8>& inbuf,
|
|
const time_t /*cur_time */, const int, const unsigned int,
|
|
int &frames, bool &advert,
|
|
bool fill_remainder) throw()
|
|
{
|
|
bool mp3;
|
|
int end = 0;
|
|
|
|
if (streamData::isAllowedType(type, mp3))
|
|
{
|
|
int last = min (len, (int)(inbuf.size() - offset));
|
|
const unsigned char *buf;
|
|
|
|
if (m_remainder.empty () || offset < last)
|
|
{
|
|
buf = &inbuf [offset];
|
|
// DLOG ("last " + tos(last) + ", off " + tos(offset));
|
|
}
|
|
else
|
|
{
|
|
const std::vector<__uint8>::const_iterator pos = inbuf.begin();
|
|
size_t cc = min (inbuf.size(), (size_t)len);
|
|
|
|
cc = min (cc, (size_t)4096);
|
|
if (cc > m_remainder.size())
|
|
cc -= m_remainder.size();
|
|
if (cc < 1 || cc > inbuf.size())
|
|
{
|
|
ILOG ("sync2, cc is " + tos (cc));
|
|
abort();
|
|
}
|
|
m_remainder.insert (m_remainder.end(), inbuf.begin(), inbuf.begin() + cc);
|
|
|
|
buf = &m_remainder [0];
|
|
last = min (len, (int)m_remainder.size());
|
|
fill_remainder = false;
|
|
// DLOG ("merged remainder, now " + tos (last) + ", added " + tos(cc));
|
|
}
|
|
if (last > 8)
|
|
{
|
|
int last_size = 0;
|
|
//double fps_limit = m_fps*2;
|
|
|
|
fill_remainder = false;
|
|
|
|
for (int i = 0; (i < last-8) && !iskilled();)
|
|
{
|
|
const uv2xHdr *voxHdr = (const uv2xHdr*)(&(buf[i]));
|
|
const int msgLen = ntohs(voxHdr->msgLen);
|
|
const int found = ((voxHdr->sync == UVOX2_SYNC_BYTE) &&
|
|
(msgLen > UV2X_OVERHEAD) &&
|
|
(msgLen <= MAX_PAYLOAD_SIZE) ? (msgLen + UV2X_OVERHEAD) : 0);
|
|
|
|
// need to find frames and that the input is the correct format!
|
|
//
|
|
// is a bit of a pain for AAC though as we've already done the
|
|
// rough bitrate match when the advert / intro / backup was read
|
|
// we'll just pass things through as though the bitrate is ok...
|
|
if ((found > 0)) // && (found <= len))
|
|
{
|
|
if (!frames)
|
|
{
|
|
end = i;
|
|
protocol_shoutcastClient::createFrameRate(mp3, (m_streamData ? m_streamData->streamSampleRate() : 0));
|
|
}
|
|
|
|
i += (last_size = found);
|
|
// only count valid full-size frames
|
|
if (i <= last)
|
|
{
|
|
//const std::vector<__uint8>::const_iterator pos = inbuf.begin();
|
|
|
|
m_output.insert(m_output.end(), buf+end, buf+end+last_size);
|
|
end += last_size;
|
|
|
|
// we only want to count audio frames and not metadata
|
|
const __uint16 voxMsgType = ntohs(voxHdr->msgType);
|
|
if ((voxMsgType >= 0x7000) && (voxMsgType < 0x9000))
|
|
{
|
|
++frames;
|
|
}
|
|
// DLOG ("frame " + tos(m_frameCount+frames) + " (" + tos(found) + ") last " + tos(last) + " end " + tos(end) + " i " + tos(i));
|
|
}
|
|
else
|
|
{
|
|
// DLOG ("EOB, " + tos (i) + ", " + tos(offset) + ", " + tos(inbuf.size()));
|
|
if (m_remainder.empty())
|
|
{
|
|
if (i+offset > inbuf.size())
|
|
fill_remainder = true;
|
|
}
|
|
else if (end + offset < inbuf.size())
|
|
fill_remainder = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// we now look for the "SCAdvert" marker
|
|
// for detecting if we've got an advert.
|
|
if (i < (len - 8) && ((buf[i]) == 'S') && ((buf[i+1]) == 'C'))
|
|
{
|
|
if (!memcmp(&buf[i], "SCAdvert", 8))
|
|
{
|
|
if (frames == 0)
|
|
{
|
|
// DLOG ("Found SCAdvert");
|
|
advert = true;
|
|
end += 8;
|
|
m_remainder.clear();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if we found something but there is not enough
|
|
// data in the read buffer then we'll abort asap
|
|
if ((found > 0) && (found > len))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (m_frameCount && debug)
|
|
{
|
|
DLOG(m_clientLogString + "Bad frame found at pos: " +
|
|
tos(i) + " [" + tos(found) + "]", LOGNAME, m_streamID);
|
|
}
|
|
}
|
|
// otherwise we just need to move on and keep
|
|
// looking for what is a valid starting frame
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
fill_remainder = true;
|
|
if (m_remainder.empty() == false && frames)
|
|
m_remainder.clear();
|
|
if (fill_remainder)
|
|
{
|
|
const int remainder = (last - end);
|
|
if (remainder > 0)
|
|
m_remainder.insert(m_remainder.end(), buf + end, buf + (end + remainder));
|
|
}
|
|
}
|
|
return (len - end);
|
|
}
|
|
|
|
|
|
void protocol_shoutcast2Client::setIntro (vector<__uint8> &buf, int uvoxDataType)
|
|
{
|
|
m_introFile.clear();
|
|
if (buf.empty())
|
|
return;
|
|
streamData::convertRawToUvox (m_introFile, buf, uvoxDataType, 0, 0);
|
|
}
|