winamp/Src/Plugins/DSP/sc_serv3/protocol_shoutcast2Client.cpp
2024-09-24 14:54:57 +02:00

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);
}