2890 lines
91 KiB
C++
Executable file
2890 lines
91 KiB
C++
Executable file
/*
|
|
*
|
|
* @APPLE_LICENSE_HEADER_START@
|
|
*
|
|
* Copyright (c) 1999-2008 Apple Inc. All Rights Reserved.
|
|
*
|
|
* This file contains Original Code and/or Modifications of Original Code
|
|
* as defined in and that are subject to the Apple Public Source License
|
|
* Version 2.0 (the 'License'). You may not use this file except in
|
|
* compliance with the License. Please obtain a copy of the License at
|
|
* http://www.opensource.apple.com/apsl/ and read it before using this
|
|
* file.
|
|
*
|
|
* The Original Code and all software distributed under the License are
|
|
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
|
|
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
|
|
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
|
|
* Please see the License for the specific language governing rights and
|
|
* limitations under the License.
|
|
*
|
|
* @APPLE_LICENSE_HEADER_END@
|
|
*
|
|
*/
|
|
|
|
/*
|
|
File: QTSSMP3StreamingModule.cpp
|
|
|
|
Contains: This module will reflect a ShoutCast/IceCast-style MP3 broadcast
|
|
out to multiple clients.
|
|
|
|
Written by: Steve Ussery
|
|
|
|
*/
|
|
|
|
#ifndef kVersionString
|
|
#include "../../revision.h"
|
|
#endif
|
|
|
|
#include "QTSSMP3StreamingModule.h"
|
|
#include "QTSSModuleUtils.h"
|
|
#include "QTSSRollingLog.h"
|
|
#include "StringParser.h"
|
|
#include "OSMemory.h"
|
|
#include "OS.h"
|
|
|
|
#define DEBUG_MP3STREAMING_MODULE 0
|
|
|
|
#if DEBUG_MP3STREAMING_MODULE
|
|
#define DTRACE(s) qtss_printf(s)
|
|
#define DTRACE1(s,n1) qtss_printf(s,n1)
|
|
#define DTRACE2(s,n1,n2) qtss_printf(s,n1,n2)
|
|
#else
|
|
#define DTRACE(s)
|
|
#define DTRACE1(s,n1)
|
|
#define DTRACE2(s,n1,n2)
|
|
#endif
|
|
|
|
class QTSSMP3AccessLog;
|
|
|
|
//**************************************************
|
|
#define kOKHeader "OK\r\n"
|
|
#ifdef __MacOSX__
|
|
#define kClientAcceptHeader "HTTP/1.1 200 OK\r\nServer: QuickTime Streaming Server %s/%s\r\nContent-Type: audio/mpeg\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\n"
|
|
#define kM3UReplyHeader "HTTP/1.1 200 OK\r\nServer: QuickTime Streaming Server %s/%s\r\nContent-Type: audio/mpegurl\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\nContent-Length:"
|
|
#else
|
|
#define kClientAcceptHeader "HTTP/1.1 200 OK\r\nServer: Darwin Streaming Server %s/%s\r\nContent-Type: audio/mpeg\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\n"
|
|
#define kM3UReplyHeader "HTTP/1.1 200 OK\r\nServer: Darwin Streaming Server %s/%s\r\nContent-Type: audio/mpegurl\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\nContent-Length:"
|
|
#endif
|
|
#define kSourceReject "Error - Mount Point Taken or Invalid\r\n"
|
|
#define kSourceBadPassword "Error - Bad Password\r\n"
|
|
#define kClientCLHeader "Content-Length: 54000000\r\n\r\n"
|
|
#define kRemoteAddressSize 20
|
|
#define kBandwidthToAddEstimate 16000
|
|
|
|
// STATIC DATA
|
|
static QTSS_ModulePrefsObject sPrefs = NULL;
|
|
static QTSS_PrefsObject sServerPrefs = NULL;
|
|
static OSBufferPool* sOSBufferPoolPtr = NULL;
|
|
static QTSSMP3AccessLog* sMP3AccessLog = NULL;
|
|
static QTSS_ServerObject sServer = NULL;
|
|
static OSMutex* sLogMutex = NULL; // MP3 access log isn't reentrant
|
|
static OSMutex* sAtomicMutex = NULL;
|
|
static Bool16 sServerIdle = true;
|
|
|
|
// Server global MP3 Session management classes.
|
|
static MP3BroadcasterQueue sMP3BroadcasterQueue;
|
|
static MP3SessionTable sMP3SessionTable;
|
|
|
|
// PREFERENCE VALUES
|
|
static UInt32 sBroadcastBufferSize = 8192;
|
|
static UInt32 sDefaultBroadcastBufferSize = 8192;
|
|
static char* sBroadcastPassword = NULL;
|
|
static char* sDefaultBroadcastPassword = " ";
|
|
static Bool16 sMP3StreamingEnabled = true;
|
|
static Bool16 sDefaultMP3StreamingEnabled = true;
|
|
static SInt32 sMaximumConnections = 0;
|
|
static SInt32 sMaximumBandwidth = 0;
|
|
static UInt32 sDefaultRollInterval = 7;
|
|
static Bool16 sDefaultLogTimeInGMT = true;
|
|
static UInt32 sDefaultMaxLogBytes = 10240000;
|
|
static SInt32 sDefaultFlowControlTimeInMSec = 10000;
|
|
static SInt32 sMaxFlowControlTimeInMSec = 10000;
|
|
|
|
static Bool16 sDefaultLogEnabled = true;
|
|
static char* sDefaultLogName = "mp3_access";
|
|
static char* sDefaultLogDir = NULL;
|
|
|
|
static char* sLogName = NULL;
|
|
static char* sLogDir = NULL;
|
|
static Bool16 sLogEnabled = true;
|
|
static UInt32 sMaxLogBytes = 10240000;
|
|
static UInt32 sRollInterval = 7;
|
|
static Bool16 sLogTimeInGMT = true;
|
|
|
|
static char* sLogHeader = "#Software: %s\n"
|
|
"#Version: %s\n" //%s == version
|
|
"#Date: %s\n" //%s == date/time
|
|
"#Remark: All date values are in %s.\n" //%s == "GMT" or "local time"
|
|
"#Remark: c-duration is in seconds.\n"
|
|
"#Fields: c-ip c-user-agent [date time] cs-uri c-status c-bytes c-duration\n";
|
|
|
|
static char gClientAcceptHeader[1024];
|
|
static char gM3UReplyHeader[1024];
|
|
|
|
// FUNCTION PROTOTYPES
|
|
|
|
static QTSS_Error QTSSMP3StreamingModuleDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParams);
|
|
static QTSS_Error Register(QTSS_Register_Params* inParams);
|
|
static QTSS_Error Initialize(QTSS_Initialize_Params* inParams);
|
|
static QTSS_Error FilterRequest(QTSS_Filter_Params* inParams);
|
|
static QTSS_Error RereadPrefs();
|
|
static QTSS_Error SessionClosing(QTSS_RTSPSession_Params* inParams);
|
|
static QTSS_Error StateChange(QTSS_StateChange_Params* stateChangeParams);
|
|
static QTSS_Error Shutdown();
|
|
|
|
static void IncrementMP3SessionCount();
|
|
static void DecrementMP3SessionCount();
|
|
static void IncrementTotalMP3Bytes(UInt32 bytes);
|
|
static Bool16 CheckBandwidth(SInt32 bandwidth);
|
|
|
|
#if DEBUG_MP3STREAMING_MODULE
|
|
static void PrintStringBuffer(StrPtrLen& stringBuffer);
|
|
#else
|
|
#define PrintStringBuffer(stringBuffer) ;
|
|
#endif
|
|
|
|
static void KeepSession(QTSS_RTSPRequestObject theRequest,Bool16 keep);
|
|
static UInt32 GetRTSPSessionID(QTSS_RTSPSessionObject session);
|
|
static Bool16 IsActiveBroadcastSession(QTSS_RTSPSessionObject session);
|
|
static MP3BroadcasterSession* FindBroadcastSession(QTSS_RTSPSessionObject session);
|
|
static Bool16 IsBroadcastPassword(StrPtrLen& theRequest);
|
|
static Bool16 IsShoutcastPassword(StrPtrLen& theRequest);
|
|
static Bool16 IsHTTPGet(StrPtrLen& theRequest);
|
|
static Bool16 IsA_m3u_URL(char* theURL);
|
|
static Bool16 IsMetaDataURL(char* theURL);
|
|
static void ParseMetaDataURL(char* theURL);
|
|
static Bool16 CheckPassword(QTSS_Filter_Params* inParams, StrPtrLen& theRequest, StrPtrLen& mountpoint);
|
|
static Bool16 NeedsMetaData(StrPtrLen& theRequest);
|
|
static Bool16 ParseURL(StrPtrLen& theRequest, char* outURL, UInt16 maxlen);
|
|
static QTSS_Error LogRequest(QTSS_RTSPSessionObject inRTSPSession, MP3ClientSession* client);
|
|
static void WriteStartupMessage();
|
|
static void WriteShutdownMessage();
|
|
static void url_strcpy(char* dest, const char* src);
|
|
static QTSS_Error ReEnterFilterRequest(QTSS_Filter_Params* inParams, MP3Session* mp3Session);
|
|
|
|
static Bool16 IsHTTP(StrPtrLen& theRequest);
|
|
static Bool16 IsRTSP(StrPtrLen& theRequest);
|
|
|
|
|
|
//**************************************************
|
|
// CLASS DECLARATIONS
|
|
//**************************************************
|
|
|
|
class QTSSMP3AccessLog : public QTSSRollingLog
|
|
{
|
|
public:
|
|
|
|
QTSSMP3AccessLog();
|
|
virtual ~QTSSMP3AccessLog() {}
|
|
|
|
virtual char* GetLogName() { return QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_name", sDefaultLogName); }
|
|
virtual char* GetLogDir() { return QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_dir", sDefaultLogDir); }
|
|
virtual UInt32 GetRollIntervalInDays() { return sRollInterval; }
|
|
virtual UInt32 GetMaxLogBytes() { return sMaxLogBytes; }
|
|
virtual time_t WriteLogHeader(FILE *inFile);
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CLASS IMPLEMENTATIONS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ****************************************************************************
|
|
// QTSSMP3AccessLog -- subclass of QTSSRollingLog
|
|
// ****************************************************************************
|
|
QTSSMP3AccessLog::QTSSMP3AccessLog() : QTSSRollingLog()
|
|
{
|
|
this->SetTaskName("QTSSMP3AccessLog");
|
|
}
|
|
|
|
time_t QTSSMP3AccessLog::WriteLogHeader(FILE *inFile)
|
|
{
|
|
//The point of this header is to record the exact time the log file was created,
|
|
//in a format that is easy to parse through whenever we open the file again.
|
|
//This is necessary to support log rolling based on a time interval, and POSIX doesn't
|
|
//support a create date in files.
|
|
time_t calendarTime = ::time(NULL);
|
|
Assert(-1 != calendarTime);
|
|
if (-1 == calendarTime)
|
|
return -1;
|
|
|
|
struct tm timeResult;
|
|
struct tm* theLocalTime = qtss_localtime(&calendarTime, &timeResult);
|
|
Assert(NULL != theLocalTime);
|
|
if (NULL == theLocalTime)
|
|
return -1;
|
|
|
|
char tempBuffer[1024] = { 0 };
|
|
qtss_strftime(tempBuffer, sizeof(tempBuffer), "#Log File Created On: %m/%d/%Y %H:%M:%S\n", theLocalTime);
|
|
this->WriteToLog(tempBuffer, !kAllowLogToRoll);
|
|
tempBuffer[0] = '\0';
|
|
|
|
//format a date for the startup time
|
|
char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes] = { 0 };
|
|
Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, false);
|
|
|
|
if (result)
|
|
{
|
|
StrPtrLen serverName;
|
|
(void)QTSS_GetValuePtr(sServer, qtssSvrServerName, 0, (void**)&serverName.Ptr, &serverName.Len);
|
|
StrPtrLen serverVersion;
|
|
(void)QTSS_GetValuePtr(sServer, qtssSvrServerVersion, 0, (void**)&serverVersion.Ptr, &serverVersion.Len);
|
|
qtss_sprintf(tempBuffer, sLogHeader, serverName.Ptr , serverVersion.Ptr,
|
|
theDateBuffer, sLogTimeInGMT ? "GMT" : "local time");
|
|
this->WriteToLog(tempBuffer, !kAllowLogToRoll);
|
|
}
|
|
|
|
return calendarTime;
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3Session -- This is a base class to hold all the MP3 related
|
|
// session state info.
|
|
// ****************************************************************************
|
|
|
|
// initialize static value of class.
|
|
SInt32 MP3Session::sTotalNumMP3Sessions = 0L;
|
|
|
|
|
|
MP3Session::MP3Session(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream) :
|
|
fState(0),
|
|
fResult(200),
|
|
fStream(stream),
|
|
fSession(sess),
|
|
fSessID(0)
|
|
{
|
|
sTotalNumMP3Sessions++;
|
|
sMP3SessionTable.RegisterSession(this);
|
|
}
|
|
|
|
MP3Session::MP3Session() :
|
|
fState(0),
|
|
fResult(0),
|
|
fStream(NULL),
|
|
fSession(NULL),
|
|
fSessID(0)
|
|
{
|
|
// this should never be called
|
|
Assert(0);
|
|
}
|
|
|
|
MP3Session::~MP3Session()
|
|
{
|
|
--sTotalNumMP3Sessions;
|
|
sMP3SessionTable.UnRegisterSession(this);
|
|
fStream = NULL;
|
|
fSession = NULL;
|
|
fSessID = 0;
|
|
}
|
|
|
|
UInt8 MP3Session::IsA() const
|
|
{
|
|
return kMP3UndefinedSessionType;
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3BroadcasterSession -- This is a class to hold all the MP3 Broadcaster
|
|
// session state info.
|
|
// ****************************************************************************
|
|
MP3BroadcasterSession::MP3BroadcasterSession()
|
|
{
|
|
// this should never be called.
|
|
Assert(0);
|
|
}
|
|
|
|
MP3BroadcasterSession::MP3BroadcasterSession(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream) :
|
|
|
|
MP3Session(sess, stream),
|
|
fMP3ClientQueue(NULL),
|
|
fDataBufferLen(0),
|
|
fDataBufferSize(sBroadcastBufferSize),
|
|
fNewSongName(false)
|
|
{
|
|
fBuffer = (char*) sOSBufferPoolPtr->Get();
|
|
InitBroadcastSessionState();
|
|
fHeader[0] = '\0';
|
|
fSongName[0] = '\0';
|
|
}
|
|
|
|
MP3BroadcasterSession::~MP3BroadcasterSession()
|
|
{
|
|
SetState(MP3BroadcasterSession::kBroadcasterShutDownState);
|
|
delete fMP3ClientQueue;
|
|
fMP3ClientQueue = NULL;
|
|
sOSBufferPoolPtr->Put(fBuffer);
|
|
fBuffer = NULL;
|
|
}
|
|
|
|
UInt8 MP3BroadcasterSession::IsA() const
|
|
{
|
|
return kMP3BroadcasterSessionType;
|
|
}
|
|
|
|
void MP3BroadcasterSession::InitBroadcastSessionState()
|
|
{
|
|
::memset(fMountpoint, 0, kURLBufferSize);
|
|
::memset(fHeader, 0, kHeaderBufferSize);
|
|
::memset(fSongName, 0, kSongNameBufferSize);
|
|
::memset(fBuffer, 0, fDataBufferSize);
|
|
SetMountpoint("/");
|
|
fDataBufferLen = 0;
|
|
SetSessionID(0);
|
|
if (fMP3ClientQueue == NULL)
|
|
fMP3ClientQueue = NEW MP3ClientQueue();
|
|
SetState(MP3BroadcasterSession::kBroadcasterInitState);
|
|
}
|
|
|
|
// Run this broadcaster session's state machine.
|
|
// This is called each time through the QTSS_FilterRequest mode which
|
|
// is highly re-entrant.
|
|
QTSS_Error MP3BroadcasterSession::ExecuteState()
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
|
|
// The only states we handle here are kBroadcasterGetHeaderState and
|
|
// the kBroadcasterRecvDataState. All others are ignored.
|
|
|
|
if (GetState() == MP3BroadcasterSession::kBroadcasterGetHeaderState)
|
|
{
|
|
err = GetBroadcastHeaders();
|
|
}
|
|
else if (GetState() == MP3BroadcasterSession::kBroadcasterRecvDataState)
|
|
{
|
|
err = GetBroadcastData();
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
// Put this broadcast session in a state where it will accept incoming
|
|
// broadcaster connection request.
|
|
void MP3BroadcasterSession::AcceptBroadcastSessionState()
|
|
{
|
|
SetState(MP3BroadcasterSession::kBroadcasterValidatePasswordState);
|
|
}
|
|
|
|
// Put this broadcast session in a state where it has accepted
|
|
// the broadcaster connection request.
|
|
void MP3BroadcasterSession::AcceptPasswordState()
|
|
{
|
|
SetState(MP3BroadcasterSession::kBroadcasterGetHeaderState);
|
|
}
|
|
|
|
// Put this broadcast session in a state where it will reflect
|
|
// MP3 data to connected clients.
|
|
void MP3BroadcasterSession::AcceptDataStreamState()
|
|
{
|
|
SetState(MP3BroadcasterSession::kBroadcasterRecvDataState);
|
|
}
|
|
|
|
// Put this broadcast session in a state where it will no
|
|
// longer accept connections or reflect MP3 data.
|
|
void MP3BroadcasterSession::ShutDownState()
|
|
{
|
|
SetState(MP3BroadcasterSession::kBroadcasterShutDownState);
|
|
}
|
|
|
|
// Set the mount point URL that clients will associate
|
|
// with this broadcaster session.
|
|
void MP3BroadcasterSession::SetMountpoint(char* mp)
|
|
{
|
|
StrPtrLen mpStr(mp);
|
|
SetMountpoint(mpStr);
|
|
}
|
|
|
|
// Set the mount point URL that clients will associate
|
|
// with this broadcaster session.
|
|
void MP3BroadcasterSession::SetMountpoint(StrPtrLen& mp)
|
|
{
|
|
::memset(fMountpoint, 0, kURLBufferSize);
|
|
if (mp.Ptr != NULL && mp.Len > 0 && mp.Len < kURLBufferSize)
|
|
::memcpy(fMountpoint, mp.Ptr, mp.Len);
|
|
}
|
|
|
|
// Set the song name of the currently broadcasting session.
|
|
void MP3BroadcasterSession::SetSongName(char* sn)
|
|
{
|
|
if (sn != NULL && *sn != '\0')
|
|
{
|
|
// This mutex is to guard against updating a
|
|
// song name string while we're updating our queued
|
|
// clients.
|
|
OSMutexLocker locker(&fSongNameMutex);
|
|
url_strcpy(fSongName, sn);
|
|
fNewSongName = true;
|
|
}
|
|
}
|
|
|
|
// See if a given mount point URL matches.
|
|
Bool16 MP3BroadcasterSession::MountpointEqual(char* mp)
|
|
{
|
|
StrPtrLen mpStr(mp);
|
|
return MountpointEqual(mpStr);
|
|
}
|
|
|
|
// See if a given mount point URL matches.
|
|
Bool16 MP3BroadcasterSession::MountpointEqual(StrPtrLen& mp)
|
|
{
|
|
if (0 == *fMountpoint)
|
|
return false;
|
|
|
|
return mp.Equal(fMountpoint);
|
|
}
|
|
|
|
// Add a MP3 client session ref to our broadcaster
|
|
QTSS_Error MP3BroadcasterSession::AddClient(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream)
|
|
{
|
|
if (fMP3ClientQueue == NULL)
|
|
{
|
|
Assert(0);
|
|
return QTSS_RequestFailed;
|
|
}
|
|
return fMP3ClientQueue->AddClient(sess, stream, this);
|
|
}
|
|
|
|
// Is this RTSP session one of my clients?
|
|
Bool16 MP3BroadcasterSession::IsMyClient(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (fMP3ClientQueue == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
return fMP3ClientQueue->InQueue(sess);
|
|
}
|
|
|
|
// Remove a MP3 client session ref from our broadcaster
|
|
QTSS_Error MP3BroadcasterSession::RemoveClient(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (fMP3ClientQueue == 0)
|
|
{
|
|
Assert(0);
|
|
return QTSS_BadArgument;
|
|
}
|
|
return fMP3ClientQueue->RemoveClient(sess);
|
|
}
|
|
|
|
//Send acknowledgement response to broacaster
|
|
QTSS_Error MP3BroadcasterSession::SendOKResponse()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
if (GetStreamRef() == 0)
|
|
{
|
|
Assert(0);
|
|
return theErr;
|
|
}
|
|
theErr = QTSS_Write(GetStreamRef(), kOKHeader, ::strlen(kOKHeader), NULL, qtssWriteFlagsBufferData);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
return theErr;
|
|
}
|
|
theErr = QTSS_Flush(GetStreamRef());
|
|
Assert(theErr == QTSS_NoErr);
|
|
return theErr;
|
|
}
|
|
|
|
// Read the broadcast headers
|
|
QTSS_Error MP3BroadcasterSession::GetBroadcastHeaders()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
UInt32 theLen = 0;
|
|
|
|
Assert(GetStreamRef() != 0);
|
|
|
|
::memset(fHeader, 0, kHeaderBufferSize);
|
|
theErr = QTSS_Read(GetStreamRef(), fHeader, kHeaderBufferSize, &theLen);
|
|
if (theErr == QTSS_WouldBlock)
|
|
{
|
|
// we're blocked from reading the broadcast headers so we will
|
|
// schedule a read so we can try again later.
|
|
theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent);
|
|
Assert(theErr == QTSS_NoErr);
|
|
}
|
|
else if (theErr == QTSS_NoErr)
|
|
{
|
|
// The read of the headers succeeded
|
|
TerminateHeaders();
|
|
// parse headers here...
|
|
if (strncmp(fHeader, "icy-name", 8) != 0)
|
|
AcceptDataStreamState();
|
|
// Send another OK response.
|
|
SendOKResponse();
|
|
// schedule the first read of the data stream.
|
|
theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent);
|
|
Assert(theErr == QTSS_NoErr);
|
|
}
|
|
else
|
|
{
|
|
// The read failed with an error other than QTSS_WouldBlock
|
|
DTRACE1("MP3BroadcasterSession::GetBroadcastHeaders() - read failed with err = %"_S32BITARG_"\n", theErr);
|
|
Assert(0);
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Read the broadcast data stream
|
|
QTSS_Error MP3BroadcasterSession::GetBroadcastData()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
|
|
if (GetStreamRef() == 0)
|
|
{
|
|
Assert(0);
|
|
return theErr;
|
|
}
|
|
theErr = QTSS_Read(GetStreamRef(), fBuffer, fDataBufferSize, &fDataBufferLen);
|
|
if (theErr == QTSS_WouldBlock)
|
|
{
|
|
// we're blocked from reading the broadcast headers so we will
|
|
// schedule a read so we can try again later.
|
|
theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent);
|
|
Assert(theErr == QTSS_NoErr);
|
|
}
|
|
else if (theErr == QTSS_NoErr)
|
|
{
|
|
// The read of the broadcast stream succeeded
|
|
// Send all our clients a copy of the data
|
|
theErr = SendClientsData();
|
|
|
|
// Set up to read the next chunk of incoming broadcast data
|
|
// If we didn't do this we'd exit the kFilterRequest state after returning.
|
|
// By doing this we get called back when more data arrives.
|
|
theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent);
|
|
Assert(theErr == QTSS_NoErr);
|
|
}
|
|
else if (theErr == QTSS_NotConnected)
|
|
{
|
|
DTRACE("MP3BroadcasterSession::GetBroadcastData() - read failed with err = QTSS_NotConnected\n");
|
|
ShutDownState();
|
|
}
|
|
else
|
|
{
|
|
DTRACE1("MP3BroadcasterSession::GetBroadcastData() - read failed with err = %"_S32BITARG_"\n", theErr);
|
|
Assert(0);
|
|
ShutDownState();
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Send the broadcast data stream to all our clients
|
|
QTSS_Error MP3BroadcasterSession::SendClientsData()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
|
|
if (fMP3ClientQueue == 0)
|
|
{
|
|
return theErr;
|
|
}
|
|
if (fDataBufferLen > 0)
|
|
{
|
|
// Make sure all queued client sessions have the
|
|
// current song name
|
|
PreflightClients();
|
|
|
|
// Send MP3 data to everything in our client queue.
|
|
|
|
theErr = fMP3ClientQueue->SendToAllClients(fBuffer, fDataBufferLen);
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Update all of our queued clients with the currently
|
|
// active song name
|
|
void MP3BroadcasterSession::PreflightClients()
|
|
{
|
|
if (fMP3ClientQueue == 0)
|
|
{
|
|
return;
|
|
}
|
|
if (fNewSongName)
|
|
{
|
|
OSMutexLocker locker(&fSongNameMutex);
|
|
// Copy new song name to all queue MP3ClientSessions.
|
|
// and attempt to resend data that was previously blocked.
|
|
fMP3ClientQueue->PreflightClients(GetSongName());
|
|
fNewSongName = false;
|
|
}
|
|
}
|
|
|
|
// Search through the audiocast headers and terminate them with a '\0'
|
|
// to mark the end.
|
|
void MP3BroadcasterSession::TerminateHeaders()
|
|
{
|
|
char *bp = fHeader;
|
|
char *end = fHeader + kHeaderBufferSize;
|
|
while (bp < end)
|
|
{
|
|
// scan for newlines until end of buffer.
|
|
if (*bp++ == '\n')
|
|
{
|
|
// if the next line after newline starts with
|
|
// 'x' or 'i' we assume it's an 'x-audiocast-xxx' line
|
|
// or an 'icy-xxxx' line, otherwise we're done.
|
|
if (*bp != 'x' && *bp != 'i')
|
|
break;
|
|
}
|
|
}
|
|
// make sure we didn't go past the end of buffer.
|
|
if (bp > end)
|
|
bp = end;
|
|
// terminate the buffer.
|
|
*bp = '\0';
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3ClientSession -- This is a class to hold all the MP3Client state info.
|
|
// ****************************************************************************
|
|
MP3ClientSession::MP3ClientSession()
|
|
{
|
|
Assert(0);
|
|
// this should never be called.
|
|
}
|
|
|
|
MP3ClientSession::MP3ClientSession( QTSS_RTSPSessionObject sess,
|
|
QTSS_StreamRef stream,
|
|
MP3BroadcasterSession* owner) :
|
|
MP3Session(sess, stream),
|
|
fBytesSent(0),
|
|
fCurrentBitRate(0),
|
|
fLastBitRateBytes(0),
|
|
fLastBitRateUpdateTime(0),
|
|
fConnectTime(0),
|
|
fSendCount(0),
|
|
fRetryCount(0),
|
|
fOwner(owner),
|
|
fNewSongName(false),
|
|
fWasBlocked(false),
|
|
fBlockTime(0),
|
|
fNeedsContentLength(false),
|
|
fWantsMetaData(true)
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
const char* kType = "MP3 Client";
|
|
|
|
::memset(fSongName, 0, kSongNameBufferSize);
|
|
::memset(fHeader, 0, kHeaderBufferSize);
|
|
::memset(fHostName, 0, kHostNameBufferSize);
|
|
::memset(fUserAgent, 0, kUserAgentBufferSize);
|
|
::memset(fRequestBuffer, 0, kRequestBufferSize);
|
|
|
|
ParseRequestParams(stream);
|
|
|
|
QTSS_LockObject(sServer);
|
|
UInt32 index;
|
|
UInt32 tempInt;
|
|
char tempBuffer[1024];
|
|
UInt32 len;
|
|
QTSS_CreateObjectValue(sServer, qtssSvrConnectedUsers, qtssConnectedUserObjectType, &index, &fQTSSObject);
|
|
QTSS_SetValue(fQTSSObject, qtssConnectionType, 0, kType, strlen(kType) + 1);
|
|
if (fHostName[0] != '\0')
|
|
QTSS_SetValue(fQTSSObject, qtssConnectionHostName, 0, fHostName, ::strlen(fHostName) + 1);
|
|
QTSS_SetValuePtr(fQTSSObject, qtssConnectionBytesSent, &fBytesSent, sizeof(fBytesSent));
|
|
QTSS_SetValuePtr(fQTSSObject, qtssConnectionCurrentBitRate, &fCurrentBitRate, sizeof(fBytesSent));
|
|
tempInt = 0;
|
|
QTSS_SetValue(fQTSSObject, qtssConnectionPacketLossPercent, 0, &tempInt, sizeof(tempInt));
|
|
fConnectTime = OS::Milliseconds();
|
|
QTSS_SetValue(fQTSSObject, qtssConnectionCreateTimeInMsec, 0, &fConnectTime, sizeof(fConnectTime));
|
|
|
|
len = sizeof(tempBuffer);
|
|
err = QTSS_GetValue(sess, qtssRTSPSesLocalAddrStr, 0, tempBuffer, &len);
|
|
if (err == QTSS_NoErr)
|
|
(void)QTSS_SetValue(fQTSSObject, qtssConnectionSessLocalAddrStr, 0, tempBuffer, len);
|
|
|
|
len = sizeof(tempBuffer);
|
|
err = QTSS_GetValue(sess, qtssRTSPSesRemoteAddrStr, 0, tempBuffer, &len);
|
|
if (err == QTSS_NoErr)
|
|
(void)QTSS_SetValue(fQTSSObject, qtssConnectionSessRemoteAddrStr, 0, tempBuffer, len);
|
|
|
|
char* mountpoint = owner->GetMountpoint();
|
|
(void)QTSS_SetValue(fQTSSObject, qtssConnectionMountPoint, 0, mountpoint, strlen(mountpoint) + 1);
|
|
|
|
QTSS_UnlockObject(sServer);
|
|
|
|
// Increment the server's MP3 session count.
|
|
IncrementMP3SessionCount();
|
|
}
|
|
|
|
MP3ClientSession::~MP3ClientSession()
|
|
{
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
|
|
QTSS_Object sessionObject;
|
|
UInt32 len = sizeof(QTSS_Object);
|
|
|
|
QTSS_LockObject(sServer);
|
|
for (int x = 0; QTSS_GetValue(sServer, qtssSvrConnectedUsers, x, &sessionObject, &len) == QTSS_NoErr; x++)
|
|
{
|
|
Assert(sessionObject != NULL);
|
|
Assert(len == sizeof(QTSS_Object));
|
|
|
|
if (sessionObject == fQTSSObject)
|
|
{
|
|
(void)QTSS_RemoveValue(sServer, qtssSvrConnectedUsers, x);
|
|
break;
|
|
}
|
|
}
|
|
QTSS_UnlockObject(sServer);
|
|
// Decrease the server's count of MP3 sessions by one.
|
|
DecrementMP3SessionCount();
|
|
}
|
|
|
|
UInt8 MP3ClientSession::IsA() const
|
|
{
|
|
return kMP3ClientSessionType;
|
|
}
|
|
|
|
// Send the client it HTTP response
|
|
QTSS_Error MP3ClientSession::SendResponse()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
UInt32 len = 0;
|
|
char buffer[512];
|
|
|
|
if (GetState() == MP3ClientSession::kClientInitState)
|
|
{
|
|
//
|
|
// Note: We don't Parse any client request HTTP parms here...
|
|
//
|
|
SetState(MP3ClientSession::kClientSendResponse);
|
|
theErr = QTSS_Write(GetStreamRef(), gClientAcceptHeader, ::strlen(gClientAcceptHeader), NULL, qtssWriteFlagsBufferData);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
// x-audiocast header parameters are added and sent here
|
|
if (fWantsMetaData)
|
|
{
|
|
// add the "icy-metaint:xxxx" parameter to the header.
|
|
qtss_sprintf(buffer, "icy-metaint:%d\r\n", kClientMetaInt);
|
|
::strcat(fHeader, buffer);
|
|
}
|
|
len = ::strlen(fHeader);
|
|
if (len > 0)
|
|
{
|
|
theErr = QTSS_Write(GetStreamRef(), fHeader, len, NULL, qtssWriteFlagsBufferData);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
}
|
|
// ...and send the phony content length or just a newline to mark end of header
|
|
if (WantsContentLength())
|
|
theErr = QTSS_Write(GetStreamRef(), kClientCLHeader, ::strlen(kClientCLHeader), NULL, qtssWriteFlagsBufferData);
|
|
else
|
|
theErr = QTSS_Write(GetStreamRef(), "\r\n", 2, NULL, qtssWriteFlagsBufferData);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
Assert(0);
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
else
|
|
{
|
|
SetState(MP3ClientSession::kClientSendDataState);
|
|
}
|
|
|
|
//
|
|
// Make sure the client session remains in FilterRequest server role.
|
|
// We do this by scheduling ourself to be called back in
|
|
// kClientPollInterval milliseconds. When called back again we will
|
|
// continue to schedule ourself until we get into another
|
|
//
|
|
|
|
KeepSession((QTSS_RTSPRequestObject) GetStreamRef(), true);
|
|
theErr = QTSS_SetIdleTimer(kClientPollInterval);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
Assert(0);
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
}
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Send the broadcast data stream to our client including meta-data
|
|
// if needed.
|
|
QTSS_Error MP3ClientSession::SendMP3Data(char* buffer, UInt32 bufferlen)
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
UInt32 sendcount;
|
|
UInt32 sendlen = 0L;
|
|
UInt32 remaininglen = 0L;
|
|
|
|
// get the broadcast sendcount into our local copy.
|
|
sendcount = fSendCount;
|
|
Assert(buffer != NULL);
|
|
// We only send data if we are int the kClientSendDataState.
|
|
if (GetState() != MP3ClientSession::kClientSendDataState)
|
|
{
|
|
return theErr;
|
|
}
|
|
// If we are using meta data then check and see if we hit our
|
|
// meta-data send interval.
|
|
// if so calculate the second write size.
|
|
if (fWantsMetaData && ((sendcount + bufferlen) > kClientMetaInt))
|
|
{
|
|
// sendlen is the amount of buffered data to send before meta data
|
|
// is interposed.
|
|
sendlen = (UInt32) kClientMetaInt - sendcount;
|
|
// remainglen is the reaming amount of buffered data to send
|
|
// after sending the meta-data.
|
|
remaininglen = bufferlen - sendlen;
|
|
}
|
|
else
|
|
{
|
|
// either it's not time to send metadata or we are not
|
|
// doing meta-data for this client.
|
|
sendlen = bufferlen;
|
|
remaininglen = 0L;
|
|
}
|
|
// Send up to sendlen bytes of data from the buffer.
|
|
QTSS_StreamRef stream = GetStreamRef();
|
|
|
|
Assert(stream != NULL);
|
|
|
|
Assert(sendlen <= bufferlen);
|
|
|
|
theErr = QTSS_Write(stream, buffer, sendlen, NULL, qtssWriteFlagsBufferData);
|
|
sendcount += sendlen;
|
|
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
// If there is meta-data this time then send it followed by remaininglen bytes
|
|
// from the buffer.
|
|
if (remaininglen != 0L)
|
|
{
|
|
// send the meta-data
|
|
theErr = SendMetaData();
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
// sendcount must be zero after sending meta-data.
|
|
sendcount = 0;
|
|
// send the remainder of the data buffer.
|
|
theErr = QTSS_Write(GetStreamRef(), buffer+sendlen, remaininglen, NULL, qtssWriteFlagsBufferData);
|
|
sendcount += remaininglen;
|
|
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
}
|
|
fSendCount = sendcount;
|
|
// Flush to perform the actuall send to the client.
|
|
theErr = QTSS_Flush(GetStreamRef());
|
|
if (theErr == QTSS_WouldBlock)
|
|
{
|
|
// the client is flow controlled.
|
|
SInt64 curTime = OS::Milliseconds();
|
|
fWasBlocked = true;
|
|
QTSS_RequestEvent(GetStreamRef(), QTSS_WriteableEvent);
|
|
DTRACE2("Got blocked at time %qd, numRetries = %"_S32BITARG_"\n", curTime, fRetryCount);
|
|
if (fBlockTime == 0)
|
|
fBlockTime = curTime;
|
|
fRetryCount++;
|
|
if (curTime - fBlockTime > sMaxFlowControlTimeInMSec)
|
|
{
|
|
SetResult(453);
|
|
DTRACE1("MP3ClientSession::SendMP3Data - too many tries. Terminating client session = %"_S32BITARG_"\n", GetSessionID());
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
}
|
|
}
|
|
else if (theErr != QTSS_NoErr)
|
|
{
|
|
DTRACE1("MP3ClientSession::SendMP3Data - Terminating client session = %"_S32BITARG_"\n", GetSessionID());
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
}
|
|
else
|
|
{
|
|
// we successfully sent data to the client.
|
|
// fSendCount only gets incremented if the QTSS_Flush() was successful
|
|
SetResult(200);
|
|
fWasBlocked = false;
|
|
fBlockTime = 0;
|
|
fRetryCount = 0;
|
|
}
|
|
|
|
fBytesSent += bufferlen;
|
|
// Increase the server's total MP3 byte count attribute
|
|
IncrementTotalMP3Bytes(bufferlen);
|
|
UpdateBitRateInternal(OS::Milliseconds());
|
|
|
|
return theErr;
|
|
}
|
|
|
|
// Retry to send previously blocked data.
|
|
QTSS_Error MP3ClientSession::RetrySendData()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
|
|
// Flush to perform the actuall send to the client.
|
|
theErr = QTSS_Flush(GetStreamRef());
|
|
if (theErr == QTSS_WouldBlock)
|
|
{
|
|
// the client is still flow controlled.
|
|
fWasBlocked = true;
|
|
QTSS_RequestEvent(GetStreamRef(), QTSS_WriteableEvent);
|
|
return theErr;
|
|
}
|
|
else if (theErr != QTSS_NoErr)
|
|
{
|
|
// some error other than QTSS_WouldBlock occured.
|
|
SetResult(454);
|
|
SetState(MP3ClientSession::kClientShutDownState);
|
|
return theErr;
|
|
}
|
|
else
|
|
{
|
|
// we successfully sent data to the client.
|
|
// cancel future retries
|
|
fWasBlocked = false;
|
|
SetResult(200);
|
|
fRetryCount = 0;
|
|
}
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
// Send the broadcast meta data stream to our client
|
|
QTSS_Error MP3ClientSession::SendMetaData()
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
char buffer[1024];
|
|
UInt16 bufferlen;
|
|
|
|
// Don't allow an update of the song name while we're sending
|
|
// it to the client.
|
|
OSMutexLocker locker(&fSongNameMutex);
|
|
// format the meta-data
|
|
::memset(buffer, 0, 1024);
|
|
// Make sure we have something to send
|
|
if (!fNewSongName || fSongName[0] == '\0')
|
|
{
|
|
// setup to write a single NULL byte.
|
|
bufferlen = 1;
|
|
}
|
|
else
|
|
{
|
|
// Setup to write meta-data + pad NULL bytes.
|
|
// first value in the buffer is the number of 16 byte chunks
|
|
// to send.
|
|
char tmp[512];
|
|
qtss_sprintf(tmp, "StreamTitle='%s';StreamUrl='';", fSongName);
|
|
bufferlen = ::strlen(tmp);
|
|
buffer[0] = (unsigned char)((bufferlen/16) + 1);
|
|
::strcat(buffer, tmp);
|
|
bufferlen = (buffer[0]*16) + 1;
|
|
}
|
|
// send the meta-data
|
|
theErr = QTSS_Write(GetStreamRef(), buffer, bufferlen, NULL, qtssWriteFlagsBufferData);
|
|
|
|
if (theErr == QTSS_NoErr)
|
|
{
|
|
// meta data was written successfully
|
|
fNewSongName = false;
|
|
}
|
|
fBytesSent += bufferlen;
|
|
return theErr;
|
|
}
|
|
|
|
// Set the x-audiocast headers of the currently broadcasting client.
|
|
// Lines that end in '\n' will have a '\r' prepended.
|
|
void MP3ClientSession::SetHeader(char* header)
|
|
{
|
|
char *bp = header;
|
|
char *cp = fHeader;
|
|
char *end = fHeader + kHeaderBufferSize;
|
|
while (cp < end)
|
|
{
|
|
if (*bp == '\0')
|
|
{
|
|
// end of header buffer string
|
|
break;
|
|
}
|
|
// scan for newlines .
|
|
if (*bp == '\n')
|
|
{
|
|
// add a CR before the newline
|
|
*cp = '\r';
|
|
cp++;
|
|
*cp = '\n';
|
|
}
|
|
else
|
|
{
|
|
// otherwise just copy the byte
|
|
*cp = *bp;
|
|
}
|
|
cp++;
|
|
bp++;
|
|
}
|
|
// make sure we didn't go past the end of buffer.
|
|
if (cp > end)
|
|
cp = end;
|
|
// terminate the buffer.
|
|
*cp = '\0';
|
|
}
|
|
|
|
// Set the song name of the currently broadcasting client.
|
|
void MP3ClientSession::SetSongName(char* sn)
|
|
{
|
|
if (sn != NULL && *sn != '\0')
|
|
{
|
|
// This mutex is to guard against updating a
|
|
// song name string while we're trying to send it
|
|
// to a remote client.
|
|
OSMutexLocker locker(&fSongNameMutex);
|
|
::strcpy(fSongName, sn);
|
|
fNewSongName = true;
|
|
}
|
|
}
|
|
|
|
void MP3ClientSession::UpdateBitRateInternal(const SInt64& curTime)
|
|
{
|
|
if (curTime < fLastBitRateUpdateTime + 10000)
|
|
return; // update the bitrate every 10 seconds
|
|
|
|
UInt32 bitsInInterval = (fBytesSent - fLastBitRateBytes) * 8;
|
|
SInt64 updateTime = (curTime - fLastBitRateUpdateTime) / 1000;
|
|
if (updateTime > 0) // leave Bit Rate the same if updateTime is 0 also don't divide by 0.
|
|
fCurrentBitRate = (UInt32) ( bitsInInterval / updateTime );
|
|
fLastBitRateBytes = fBytesSent;
|
|
fLastBitRateUpdateTime = curTime;
|
|
}
|
|
|
|
void MP3ClientSession::ParseRequestParams(QTSS_StreamRef stream)
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
char *bp = NULL;
|
|
unsigned int idx;
|
|
// get a copy of the first line of the HTTP request
|
|
QTSS_RTSPRequestObject theRequest = (QTSS_RTSPRequestObject) stream;
|
|
StrPtrLen theFullRequest;
|
|
if (theRequest != NULL)
|
|
err = QTSS_GetValuePtr(theRequest, qtssRTSPReqFullRequest, 0, (void**)&theFullRequest.Ptr, &theFullRequest.Len);
|
|
else
|
|
err = QTSS_BadArgument;
|
|
if (err == QTSS_NoErr)
|
|
{
|
|
// make sure we don't overflow our buffer
|
|
if (theFullRequest.Len >= kRequestBufferSize)
|
|
theFullRequest.Len = kRequestBufferSize-1;
|
|
// copy the request into our private class buffer.
|
|
::memcpy(fRequestBuffer, theFullRequest.Ptr, theFullRequest.Len);
|
|
// first see if the client wants to recieve meta-data
|
|
fWantsMetaData = NeedsMetaData(theFullRequest);
|
|
|
|
// Do we need to send a phony HTTP content length field?
|
|
if (::strstr(fRequestBuffer, "MSIE") != NULL)
|
|
fNeedsContentLength = true;
|
|
else if (::strstr(fRequestBuffer, "RMA/1.0") != NULL)
|
|
fNeedsContentLength = true;
|
|
else if (::strstr(fRequestBuffer, "NSPlayer") != NULL)
|
|
fNeedsContentLength = true;
|
|
else
|
|
fNeedsContentLength = false;
|
|
// Get a copy ot the requestor's host name if it exists
|
|
bp = ::strstr(fRequestBuffer, "Host:");
|
|
if (bp != NULL)
|
|
{
|
|
bp += ::strlen("Host:");
|
|
while (*bp == ' ')
|
|
bp++;
|
|
idx = 0;
|
|
while (bp[idx])
|
|
{
|
|
if (idx >= kHostNameBufferSize-1)
|
|
break;
|
|
if (bp[idx] == '\n' || bp[idx] == '\r')
|
|
break;
|
|
fHostName[idx] = bp[idx];
|
|
idx++;
|
|
}
|
|
}
|
|
// Get a copy ot the requesting User Agent's name if it exists
|
|
bp = ::strstr(fRequestBuffer, "User-Agent:");
|
|
if (bp != NULL)
|
|
{
|
|
bp += ::strlen("User-Agent:");
|
|
while (*bp == ' ')
|
|
bp++;
|
|
idx = 0;
|
|
while (bp[idx])
|
|
{
|
|
if (idx >= kUserAgentBufferSize-1)
|
|
break;
|
|
if (bp[idx] == '\n' || bp[idx] == '\r')
|
|
break;
|
|
fUserAgent[idx] = bp[idx];
|
|
idx++;
|
|
}
|
|
}
|
|
// Now that we're done parsing the request we will
|
|
// terminate first line of the request with '\0'.
|
|
for (idx=0; idx < theFullRequest.Len; idx++)
|
|
{
|
|
if (fRequestBuffer[idx] == '\r' || fRequestBuffer[idx] == '\n')
|
|
{
|
|
fRequestBuffer[idx] = '\0';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// dummy value so that we have something to log
|
|
::strcpy(fRequestBuffer, "GET /* HTTP/1.0");
|
|
}
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3SessionRef -- This class is just a wrapper class for handling the
|
|
// mapping of RTSP Session refs to the corresponding MP3 Session class refs.
|
|
// It will be the an element of our lookup hash table.
|
|
// ****************************************************************************
|
|
MP3SessionRef::MP3SessionRef(MP3Session* mp3Session) :
|
|
fNextHashEntry(NULL),
|
|
fHashValue(0),
|
|
fMP3Session(mp3Session)
|
|
{
|
|
}
|
|
|
|
MP3SessionRef::~MP3SessionRef()
|
|
{
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3SessionRefKey -- This class is used to generate hash keys for looking
|
|
// up values in our MP3SessionTable. The Hash key is just the RTSP Session
|
|
// reference value.
|
|
// ****************************************************************************
|
|
MP3SessionRefKey::MP3SessionRefKey(MP3SessionRef* mp3SessRef) :
|
|
fKeyValue(mp3SessRef)
|
|
{
|
|
fHashValue = 0L;
|
|
if (fKeyValue != NULL)
|
|
{
|
|
fMP3Session = fKeyValue->GetMP3Session();
|
|
if (fMP3Session != NULL)
|
|
fHashValue = (PointerSizedInt)fMP3Session->GetSession();
|
|
}
|
|
else
|
|
{
|
|
fMP3Session = NULL;
|
|
}
|
|
}
|
|
|
|
MP3SessionRefKey::MP3SessionRefKey(QTSS_RTSPSessionObject rtspSessRef) :
|
|
fKeyValue(NULL),
|
|
fHashValue((PointerSizedInt)rtspSessRef),
|
|
fMP3Session(NULL)
|
|
{
|
|
}
|
|
|
|
MP3SessionRefKey::~MP3SessionRefKey()
|
|
{
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3SessionTable -- This class provides a way to map RTSP Session references
|
|
// into the corresponding MP3Session class instances if any.
|
|
// ****************************************************************************
|
|
MP3SessionTable::MP3SessionTable(UInt32 tableSize) :
|
|
fTable(tableSize),
|
|
fMutex()
|
|
{
|
|
}
|
|
|
|
MP3SessionTable::~MP3SessionTable()
|
|
{
|
|
}
|
|
|
|
// Attempt to add a new MP3Session ref to the table's map.
|
|
// returns true on success or false if it fails.
|
|
Bool16 MP3SessionTable::RegisterSession(MP3Session* session)
|
|
{
|
|
// sanity check
|
|
if (session == NULL)
|
|
{
|
|
Assert(0);
|
|
return false;
|
|
}
|
|
// construct a new entry for the table.
|
|
MP3SessionRef* newEntry = NEW MP3SessionRef(session);
|
|
if (newEntry == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
// generate it's hash key.
|
|
MP3SessionRefKey key(newEntry);
|
|
// Lock the table while we try and add the new entry.
|
|
OSMutexLocker locker(&fMutex);
|
|
// check for a duplicate entry;
|
|
MP3SessionRef* duplicateRef = fTable.Map(&key);
|
|
if (duplicateRef != NULL)
|
|
{
|
|
delete newEntry;
|
|
Assert(0);
|
|
return false;
|
|
}
|
|
// add the new entry to the table.
|
|
fTable.Add(newEntry);
|
|
return true;
|
|
}
|
|
|
|
// Given an QTSS_RTSPSessionObject resolve it into a MP3Session class
|
|
// reference. Returns NULL if there's none in out map.
|
|
MP3Session* MP3SessionTable::Resolve(QTSS_RTSPSessionObject rtspSession)
|
|
{
|
|
// sanity check
|
|
if (rtspSession == NULL)
|
|
{
|
|
Assert(0);
|
|
return NULL;
|
|
}
|
|
// generate it's hash key.
|
|
MP3SessionRefKey key(rtspSession);
|
|
// Lock the table while we do our lookup.
|
|
OSMutexLocker locker(&fMutex);
|
|
// Look for the entry in the table's map;
|
|
MP3SessionRef* entry = fTable.Map(&key);
|
|
if (entry != NULL)
|
|
{
|
|
// We found it. return the entry's MP3Session ref.
|
|
return entry->GetMP3Session();
|
|
}
|
|
// it's not in the table.
|
|
return NULL;
|
|
}
|
|
|
|
// Attempt to remove a MP3Session ref from the table's map.
|
|
// returns true on success or false if it fails.
|
|
Bool16 MP3SessionTable::UnRegisterSession(MP3Session* session)
|
|
{
|
|
// sanity check
|
|
if (session == NULL)
|
|
{
|
|
Assert(0);
|
|
return false;
|
|
}
|
|
// generate it's hash key.
|
|
MP3SessionRef entryRef(session);
|
|
MP3SessionRefKey key(&entryRef);
|
|
// Lock the table while we try and remove the entry.
|
|
OSMutexLocker locker(&fMutex);
|
|
// look for the entry in the table's map;
|
|
MP3SessionRef* entry = fTable.Map(&key);
|
|
if (entry != NULL)
|
|
{
|
|
// We found it. Remove it from the table and delete the
|
|
// reference instance created by RegisterSession().
|
|
fTable.Remove(entry);
|
|
delete entry;
|
|
return true;
|
|
}
|
|
// it's not in the table.
|
|
return false;
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3BroadcasterQueue -- This is a class manages a queue of MP3Broadcaster
|
|
// objects.
|
|
// ****************************************************************************
|
|
MP3BroadcasterQueue::MP3BroadcasterQueue()
|
|
{
|
|
}
|
|
|
|
MP3BroadcasterQueue::~MP3BroadcasterQueue()
|
|
{
|
|
}
|
|
|
|
// Create a new MP3BroadcasterSession object and queue it.
|
|
QTSS_Error MP3BroadcasterQueue::CreateBroadcaster(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream, StrPtrLen& mountpt)
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
UInt32 uSessID = 0L;
|
|
|
|
if (sess == 0 || stream == 0)
|
|
{
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
if ((fQueue.GetLength() > 0) && (FindByMountPoint(mountpt) != NULL))
|
|
{
|
|
theErr = QTSS_Write(stream, kSourceReject, ::strlen(kSourceReject), NULL, qtssWriteFlagsBufferData);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
return theErr;
|
|
}
|
|
theErr = QTSS_Flush(stream);
|
|
DTRACE("MP3BroadcasterQueue::CreateBroadcaster - duplicate mountpoint\n");
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
if ( (fQueue.GetLength() > 0) && InQueue(sess) )
|
|
{
|
|
theErr = QTSS_Write(stream, kSourceBadPassword, ::strlen(kSourceBadPassword), NULL, qtssWriteFlagsBufferData);
|
|
if (theErr != QTSS_NoErr)
|
|
{
|
|
return theErr;
|
|
}
|
|
theErr = QTSS_Flush(stream);
|
|
DTRACE("MP3BroadcasterQueue::CreateBroadcaster - duplicate session\n");
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
MP3BroadcasterSession* broadcaster = NEW MP3BroadcasterSession(sess, stream);
|
|
if (broadcaster == NULL)
|
|
{
|
|
return QTSS_NotEnoughSpace;
|
|
}
|
|
uSessID = GetRTSPSessionID(sess);
|
|
broadcaster->SetSessionID(uSessID);
|
|
broadcaster->SetMountpoint(mountpt);
|
|
broadcaster->AcceptPasswordState();
|
|
OSQueueElem* elem = NEW OSQueueElem(broadcaster);
|
|
if (elem != 0)
|
|
{
|
|
OSMutexLocker locker(&fMutex);
|
|
fQueue.EnQueue(elem);
|
|
DTRACE1("MP3BroadcasterQueue::CreateBroadcaster() Session(%"_S32BITARG_") broadcaster added!\n", uSessID);
|
|
// send the "OK" back to the broadcaster
|
|
theErr = broadcaster->SendOKResponse();
|
|
// If we failed to send the "OK" probably becuase of flow control
|
|
// the only thing to do is to fail the attempt.
|
|
if (theErr != QTSS_NoErr)
|
|
broadcaster->ShutDownState();
|
|
}
|
|
else
|
|
{
|
|
delete broadcaster;
|
|
broadcaster = NULL;
|
|
theErr = QTSS_NotEnoughSpace;
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Dequeue a MP3BroadcasterSession object and delete it.
|
|
QTSS_Error MP3BroadcasterQueue::RemoveBroadcaster(QTSS_RTSPSessionObject sess)
|
|
{
|
|
UInt32 uSessID = 0L;
|
|
if (sess == 0)
|
|
{
|
|
return QTSS_BadArgument;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->GetSession() == sess))
|
|
{
|
|
elem->Remove();
|
|
uSessID = current->GetSessionID();
|
|
DTRACE1("MP3BroadcasterQueue::RemoveBroadcaster() Session(%"_S32BITARG_") broadcaster removed!\n", uSessID);
|
|
delete elem;
|
|
delete current;
|
|
return QTSS_NoErr;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
// Find and dequeue a MP3ClientSession object and delete it.
|
|
QTSS_Error MP3BroadcasterQueue::RemoveClient(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (sess == 0)
|
|
{
|
|
return QTSS_BadArgument;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->IsMyClient(sess)))
|
|
{
|
|
return current->RemoveClient(sess);
|
|
}
|
|
iter.Next();
|
|
}
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
|
|
|
|
// See if a particular Broadcaster RTSP session is in our queue.
|
|
Bool16 MP3BroadcasterQueue::InQueue(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (sess == 0)
|
|
{
|
|
return false;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->GetSession() == sess))
|
|
{
|
|
return true;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// See if a particular Client RTSP session is in any of our broadcasters' queues.
|
|
Bool16 MP3BroadcasterQueue::IsActiveClient(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (sess == 0)
|
|
{
|
|
return false;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->IsMyClient(sess)))
|
|
{
|
|
return true;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Find a MP3BroadcasterSession in the queue by it's mountpoint.
|
|
MP3BroadcasterSession* MP3BroadcasterQueue::FindByMountPoint(char* mountpoint)
|
|
{
|
|
if (mountpoint == NULL || mountpoint[0] == '\0')
|
|
{
|
|
return NULL;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->MountpointEqual(mountpoint)))
|
|
{
|
|
return current;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Find a MP3BroadcasterSession in the queue by it's mountpoint.
|
|
MP3BroadcasterSession* MP3BroadcasterQueue::FindByMountPoint(StrPtrLen& mountpoint)
|
|
{
|
|
if (mountpoint.Ptr == NULL || mountpoint.Len == 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->MountpointEqual(mountpoint)))
|
|
{
|
|
return current;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Find a MP3BroadcasterSession in the queue by it's RTSP Session.
|
|
MP3BroadcasterSession* MP3BroadcasterQueue::FindBySession(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (sess == 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if ((current != 0) && (current->GetSession() == sess))
|
|
{
|
|
return current;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// Mark all broadcast sessions to be terminated when they are next called.
|
|
void MP3BroadcasterQueue::TerminateAllBroadcastSessions()
|
|
{
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueElem* elem = NULL;
|
|
OSQueueIter iter(&fQueue);
|
|
|
|
while ((elem = iter.GetCurrent()) != NULL)
|
|
{
|
|
MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject();
|
|
if (current != NULL)
|
|
{
|
|
current->SetState(MP3BroadcasterSession::kBroadcasterShutDownState);
|
|
}
|
|
iter.Next();
|
|
}
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MP3ClientQueue -- This is a class maintains a queue of MP3Client objects.
|
|
// ****************************************************************************
|
|
MP3ClientQueue::MP3ClientQueue()
|
|
{
|
|
}
|
|
|
|
MP3ClientQueue::~MP3ClientQueue()
|
|
{
|
|
// Make sure all queued client sessions are marked for termination since
|
|
// since they are now orphaned and no longer have an owner.
|
|
TerminateClients();
|
|
}
|
|
|
|
// Create a new MP3ClientSession object and queue it.
|
|
QTSS_Error MP3ClientQueue::AddClient(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream,
|
|
MP3BroadcasterSession* owner)
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
UInt32 uSessID = 0L;
|
|
|
|
if (sess == 0 || stream == 0)
|
|
{
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
if ( InQueue (sess) )
|
|
{
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
MP3ClientSession* client = NEW MP3ClientSession(sess, stream, owner);
|
|
if (client == 0)
|
|
{
|
|
return QTSS_NotEnoughSpace;
|
|
}
|
|
uSessID = GetRTSPSessionID(sess);
|
|
client->SetSessionID(uSessID);
|
|
if (owner != NULL)
|
|
{
|
|
// Get a copy of the x-audiocast headers from the
|
|
// broadcaster's cached copy.
|
|
client->SetHeader(owner->GetHeader());
|
|
// Make sure we have a song name also
|
|
client->SetSongName(owner->GetSongName());
|
|
}
|
|
OSQueueElem* elem = NEW OSQueueElem(client);
|
|
if (elem != 0)
|
|
{
|
|
OSMutexLocker locker(&fMutex);
|
|
fQueue.EnQueue(elem);
|
|
DTRACE1("MP3ClientQueue::AddClient() Session(%"_S32BITARG_") client added!\n", uSessID);
|
|
// send the "OK" back to the broadcaster
|
|
theErr = client->SendResponse();
|
|
}
|
|
else
|
|
{
|
|
delete client;
|
|
theErr = QTSS_NotEnoughSpace;
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Dequeue a MP3ClientSession object and delete it.
|
|
QTSS_Error MP3ClientQueue::RemoveClient(QTSS_RTSPSessionObject sess)
|
|
{
|
|
UInt32 uSessID = 0L;
|
|
if (sess == 0)
|
|
{
|
|
return QTSS_BadArgument;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject();
|
|
if ((curClient != 0) && (curClient->GetSession() == sess))
|
|
{
|
|
elem->Remove();
|
|
uSessID = curClient->GetSessionID();
|
|
DTRACE1("MP3ClientQueue::RemoveClient() Session(%"_S32BITARG_") client removed!\n", uSessID);
|
|
delete elem;
|
|
delete curClient;
|
|
return QTSS_NoErr;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return QTSS_BadArgument;
|
|
}
|
|
|
|
|
|
// See if a particular Client RTSP session is in our queue.
|
|
Bool16 MP3ClientQueue::InQueue(QTSS_RTSPSessionObject sess)
|
|
{
|
|
if (sess == 0)
|
|
{
|
|
return false;
|
|
}
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueIter iter(&fQueue);
|
|
OSQueueElem* elem = NULL;
|
|
while ((elem = iter.GetCurrent()) != 0)
|
|
{
|
|
MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject();
|
|
if ((curClient != 0) && (curClient->GetSession() == sess))
|
|
{
|
|
return true;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Send MP3 data to all queued MP3ClientSession objects.
|
|
QTSS_Error MP3ClientQueue::SendToAllClients(char* buffer, UInt32 bufferlen)
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueElem* elem = NULL;
|
|
OSQueueIter iter(&fQueue);
|
|
|
|
while ((elem = iter.GetCurrent()) != NULL)
|
|
{
|
|
MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject();
|
|
if (curClient != NULL)
|
|
{
|
|
QTSS_Error err;
|
|
|
|
err = curClient->SendMP3Data(buffer, bufferlen);
|
|
if (err != QTSS_NoErr)
|
|
theErr = err;
|
|
}
|
|
iter.Next();
|
|
}
|
|
return theErr;
|
|
}
|
|
|
|
// Update the current song name of queued MP3ClientSession objects.
|
|
// and attempt to resend data thatwas previously blocked.
|
|
void MP3ClientQueue::PreflightClients(char* sn)
|
|
{
|
|
QTSS_Error theErr = QTSS_NoErr;
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueElem* elem = NULL;
|
|
OSQueueIter iter(&fQueue);
|
|
|
|
while ((elem = iter.GetCurrent()) != NULL)
|
|
{
|
|
MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject();
|
|
if (curClient != NULL)
|
|
{
|
|
if (curClient->WasBlocked())
|
|
{
|
|
theErr = curClient->RetrySendData();
|
|
}
|
|
curClient->SetSongName(sn);
|
|
}
|
|
iter.Next();
|
|
}
|
|
}
|
|
|
|
// Mark all clients to be terminated when they are next polled.
|
|
// This should be called by the MP3BroadcasterSession whenever it dies
|
|
// with active client else we'll have a nasty memory leak and clients
|
|
// will pause until they timeout.
|
|
void MP3ClientQueue::TerminateClients()
|
|
{
|
|
OSMutexLocker locker(&fMutex);
|
|
OSQueueElem* elem = NULL;
|
|
OSQueueIter iter(&fQueue);
|
|
|
|
while ((elem = iter.GetCurrent()) != NULL)
|
|
{
|
|
MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject();
|
|
if (curClient != NULL)
|
|
{
|
|
curClient->SetOwner(NULL);
|
|
curClient->SetState(MP3ClientSession::kClientShutDownState);
|
|
}
|
|
iter.Next();
|
|
}
|
|
}
|
|
|
|
// ****************************************************************************
|
|
// MODULE FUNCTION IMPLEMENTATIONS
|
|
// ****************************************************************************
|
|
#if DEBUG_MP3STREAMING_MODULE
|
|
void PrintStringBuffer(StrPtrLen& stringBuffer)
|
|
{
|
|
int len = (int)stringBuffer.Len;
|
|
char localBuffer[256];
|
|
|
|
if (len > 255)
|
|
len = 255;
|
|
::memcpy(localBuffer, stringBuffer.Ptr, len);
|
|
localBuffer[len] = '\0';
|
|
qtss_printf("%s", localBuffer);
|
|
}
|
|
#endif
|
|
|
|
inline void KeepSession(QTSS_RTSPRequestObject theRequest,Bool16 keep)
|
|
{
|
|
(void)QTSS_SetValue(theRequest, qtssRTSPReqRespKeepAlive, 0, &keep, sizeof(keep));
|
|
}
|
|
|
|
QTSS_Error QTSSMP3StreamingModule_Main(void* inPrivateArgs)
|
|
{
|
|
return _stublibrary_main(inPrivateArgs, QTSSMP3StreamingModuleDispatch);
|
|
}
|
|
|
|
// QTSSMP3StreamingModuleDispatch - Dispatch all the API roles that this module handles
|
|
QTSS_Error QTSSMP3StreamingModuleDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParams)
|
|
{
|
|
switch (inRole)
|
|
{
|
|
case QTSS_Register_Role:
|
|
return Register(&inParams->regParams);
|
|
case QTSS_Initialize_Role:
|
|
return Initialize(&inParams->initParams);
|
|
case QTSS_RTSPFilter_Role:
|
|
return FilterRequest(&inParams->rtspFilterParams);
|
|
case QTSS_RereadPrefs_Role:
|
|
return RereadPrefs();
|
|
case QTSS_RTSPSessionClosing_Role:
|
|
return SessionClosing(&inParams->rtspSessionClosingParams);
|
|
case QTSS_StateChange_Role:
|
|
return StateChange(&inParams->stateChangeParams);
|
|
case QTSS_Shutdown_Role:
|
|
return Shutdown();
|
|
}
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
// Register - register all the API roles that this module handles
|
|
QTSS_Error Register(QTSS_Register_Params* inParams)
|
|
{
|
|
// Do role setup
|
|
(void)QTSS_AddRole(QTSS_Initialize_Role);
|
|
(void)QTSS_AddRole(QTSS_RereadPrefs_Role);
|
|
(void)QTSS_AddRole(QTSS_RTSPFilter_Role);
|
|
(void)QTSS_AddRole(QTSS_RTSPSessionClosing_Role);
|
|
(void)QTSS_AddRole(QTSS_StateChange_Role);
|
|
(void)QTSS_AddRole(QTSS_Shutdown_Role);
|
|
|
|
// Tell the server our name!
|
|
static char* sModuleName = "QTSSMP3StreamingModule";
|
|
::strcpy(inParams->outModuleName, sModuleName);
|
|
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
QTSS_Error Initialize(QTSS_Initialize_Params* inParams)
|
|
{
|
|
// Setup module utils
|
|
QTSSModuleUtils::Initialize(inParams->inMessages, inParams->inServer, inParams->inErrorLogStream);
|
|
sServer = inParams->inServer;
|
|
sServerPrefs = inParams->inPrefs;
|
|
sPrefs = QTSSModuleUtils::GetModulePrefsObject(inParams->inModule);
|
|
|
|
RereadPrefs();
|
|
|
|
sOSBufferPoolPtr = NEW OSBufferPool(sBroadcastBufferSize);
|
|
sLogMutex = NEW OSMutex();
|
|
sAtomicMutex = NEW OSMutex();
|
|
|
|
// pre-format the standard HTTP reply headers
|
|
qtss_sprintf(gClientAcceptHeader, kClientAcceptHeader, kVersionString, kBuildString);
|
|
qtss_sprintf(gM3UReplyHeader, kM3UReplyHeader, kVersionString, kBuildString);
|
|
|
|
sMP3AccessLog = NEW QTSSMP3AccessLog();
|
|
|
|
if (sMP3AccessLog != NULL && sLogEnabled)
|
|
sMP3AccessLog->EnableLog();
|
|
WriteStartupMessage();
|
|
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
// RereadPrefs - refread our prefs from the prefs file.
|
|
QTSS_Error RereadPrefs()
|
|
{
|
|
delete [] sDefaultLogDir;
|
|
(void)QTSS_GetValueAsString(sServerPrefs, qtssPrefsErrorLogDir, 0, &sDefaultLogDir);
|
|
|
|
delete [] sLogName;
|
|
sLogName = QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_name", sDefaultLogName);
|
|
|
|
delete [] sLogDir;
|
|
sLogDir = QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_dir", sDefaultLogDir);
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_streaming_enabled", qtssAttrDataTypeBool16,
|
|
&sMP3StreamingEnabled, &sDefaultMP3StreamingEnabled, sizeof(sMP3StreamingEnabled));
|
|
|
|
delete [] sBroadcastPassword;
|
|
sBroadcastPassword = QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_broadcast_password", sDefaultBroadcastPassword);
|
|
|
|
// if broadcast password is blank or empty don't allow MP3 streaming.
|
|
if (sBroadcastPassword == NULL || *sBroadcastPassword == '\0' || *sBroadcastPassword == ' ')
|
|
{
|
|
sMP3StreamingEnabled = false;
|
|
}
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_broadcast_buffer_size", qtssAttrDataTypeUInt32,
|
|
&sBroadcastBufferSize, &sDefaultBroadcastBufferSize, sizeof(sBroadcastBufferSize));
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_max_flow_control_time", qtssAttrDataTypeSInt32,
|
|
&sMaxFlowControlTimeInMSec, &sDefaultFlowControlTimeInMSec, sizeof(sMaxFlowControlTimeInMSec));
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logging", qtssAttrDataTypeBool16,
|
|
&sLogEnabled, &sDefaultLogEnabled, sizeof(sLogEnabled));
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logfile_size", qtssAttrDataTypeUInt32,
|
|
&sMaxLogBytes, &sDefaultMaxLogBytes, sizeof(sMaxLogBytes));
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logfile_interval", qtssAttrDataTypeUInt32,
|
|
&sRollInterval, &sDefaultRollInterval, sizeof(sRollInterval));
|
|
|
|
QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logtime_in_gmt", qtssAttrDataTypeBool16,
|
|
&sLogTimeInGMT, &sDefaultLogTimeInGMT, sizeof(sLogTimeInGMT));
|
|
|
|
UInt32 len = sizeof(SInt32);
|
|
(void) QTSS_GetValue(sServerPrefs, qtssPrefsMaximumConnections, 0, (void*)&sMaximumConnections, &len);
|
|
(void) QTSS_GetValue(sServerPrefs, qtssPrefsMaximumBandwidth, 0, (void*)&sMaximumBandwidth, &len);
|
|
|
|
// handle changing the sLogEnabled state of the access log.
|
|
if (sMP3AccessLog != NULL)
|
|
{
|
|
if (sLogEnabled)
|
|
{
|
|
if (!sMP3AccessLog->IsLogEnabled())
|
|
sMP3AccessLog->EnableLog();
|
|
}
|
|
else
|
|
{
|
|
if (sMP3AccessLog->IsLogEnabled())
|
|
sMP3AccessLog->CloseLog();
|
|
}
|
|
}
|
|
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
// SessionClosing - clean up broadcaster/client connections.
|
|
QTSS_Error SessionClosing(QTSS_RTSPSession_Params* inParams)
|
|
{
|
|
Assert(inParams != NULL);
|
|
#if DEBUG_MP3STREAMING_MODULE
|
|
UInt32 uSessID = 0L;
|
|
uSessID = GetRTSPSessionID(inParams->inRTSPSession);
|
|
DTRACE1("Closing SessionID = %"_S32BITARG_"\n", uSessID);
|
|
#endif
|
|
MP3Session* mp3sess = sMP3SessionTable.Resolve(inParams->inRTSPSession);
|
|
// If Resolve() returns a non-NULL value then we are closing
|
|
// the RTSP session related to either a reflected MP3 broadcast
|
|
// session or we are closing an RTSP session related to an
|
|
// MP3 client session.
|
|
//
|
|
// If Resolve() returns NULL this RTSP session does not belong to
|
|
// this module and we just ignore it by returning QTSS_NoErr.
|
|
if (mp3sess != NULL)
|
|
{
|
|
if (mp3sess->IsA() == kMP3BroadcasterSessionType)
|
|
{
|
|
// Remove the MP3BroadcasterSession from the global
|
|
// server queue and delete its instance.
|
|
return sMP3BroadcasterQueue.RemoveBroadcaster(inParams->inRTSPSession);
|
|
}
|
|
else if (mp3sess->IsA() == kMP3ClientSessionType)
|
|
{
|
|
MP3ClientSession* client = (MP3ClientSession*)mp3sess;
|
|
MP3BroadcasterSession* owner = client->GetOwner();
|
|
if (sLogEnabled)
|
|
LogRequest(inParams->inRTSPSession, client);
|
|
if (owner != NULL)
|
|
{
|
|
// Let the the owner, a MP3BroadcasterSession,
|
|
// remove the client from its queue and delete it.
|
|
return owner->RemoveClient(inParams->inRTSPSession);
|
|
}
|
|
else
|
|
{
|
|
// otherwise, we no longer have an owner for this
|
|
// client session and we just delete the instance.
|
|
delete client;
|
|
return QTSS_NoErr;
|
|
}
|
|
}
|
|
}
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
QTSS_Error Shutdown()
|
|
{
|
|
WriteShutdownMessage();
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
QTSS_Error StateChange(QTSS_StateChange_Params* stateChangeParams)
|
|
{
|
|
if (stateChangeParams->inNewState == qtssIdleState)
|
|
{
|
|
WriteShutdownMessage();
|
|
sMP3BroadcasterQueue.TerminateAllBroadcastSessions();
|
|
}
|
|
else
|
|
{
|
|
WriteStartupMessage();
|
|
}
|
|
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
// IncrementMP3SessionCount - This increments the MP3 session counter in the server's attributes
|
|
void IncrementMP3SessionCount()
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
|
|
// Increment the server's MP3 session count.
|
|
UInt32* ptrValue = NULL;
|
|
UInt32 valueLen = sizeof(ptrValue);
|
|
|
|
OSMutexLocker locker(sAtomicMutex);
|
|
|
|
err = QTSS_GetValuePtr(sServer, qtssMP3SvrCurConn, 0, (void**)&ptrValue, &valueLen);
|
|
if ((err == QTSS_NoErr) && (ptrValue != NULL))
|
|
{
|
|
*ptrValue += 1;
|
|
}
|
|
else
|
|
{
|
|
DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrCurConn failed with err = %"_S32BITARG_"\n", (SInt32)err);
|
|
}
|
|
// bump the total sessions count up also
|
|
err = QTSS_GetValuePtr(sServer, qtssMP3SvrTotalConn, 0, (void**)&ptrValue, &valueLen);
|
|
if ((err == QTSS_NoErr) && (ptrValue != NULL))
|
|
{
|
|
*ptrValue += 1;
|
|
}
|
|
else
|
|
{
|
|
DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrTotalConn failed with err = %"_S32BITARG_"\n", (SInt32)err);
|
|
}
|
|
}
|
|
|
|
// DecrementMP3SessionCount - This decrements the MP3 session counter in the server's attributes
|
|
void DecrementMP3SessionCount()
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
|
|
// Decrement the server's MP3 session count.
|
|
UInt32* ptrValue = NULL;
|
|
UInt32 valueLen = sizeof(ptrValue);
|
|
|
|
err = QTSS_GetValuePtr(sServer, qtssMP3SvrCurConn, 0, (void**)&ptrValue, &valueLen);
|
|
if ((err == QTSS_NoErr) && (ptrValue != NULL))
|
|
{
|
|
OSMutexLocker locker(sAtomicMutex);
|
|
*ptrValue -= 1;
|
|
}
|
|
else
|
|
{
|
|
DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrCurConn failed with err = %"_S32BITARG_"\n", (SInt32)err);
|
|
}
|
|
}
|
|
|
|
// IncrementTotalMP3Bytes - This increments the MP3 byte counter in the server's attributes
|
|
void IncrementTotalMP3Bytes(UInt32 bytes)
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
|
|
// Increment the server's MP3 total byte count.
|
|
UInt64* ptrValue = NULL;
|
|
UInt32 valueLen = sizeof(ptrValue);
|
|
|
|
err = QTSS_GetValuePtr(sServer, qtssMP3SvrTotalBytes, 0, (void**)&ptrValue, &valueLen);
|
|
if ((err == QTSS_NoErr) && (ptrValue != NULL))
|
|
{
|
|
OSMutexLocker locker(sAtomicMutex);
|
|
*ptrValue += bytes;
|
|
}
|
|
else
|
|
{
|
|
DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrTotalBytes failed with err = %"_S32BITARG_"\n", (SInt32)err);
|
|
}
|
|
}
|
|
|
|
// CheckBandwidth - Check and see if we can handle any more bandwidth.
|
|
Bool16 CheckBandwidth(SInt32 bandwidth)
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
|
|
// Get our current bandwidth and check against server's maximum.
|
|
SInt32* ptrValue = NULL;
|
|
UInt32 valueLen = sizeof(ptrValue);
|
|
|
|
err = QTSS_GetValuePtr(sServer, qtssMP3SvrCurBandwidth, 0, (void**)&ptrValue, &valueLen);
|
|
if ((err == QTSS_NoErr) && (ptrValue != NULL))
|
|
{
|
|
OSMutexLocker locker(sAtomicMutex);
|
|
if (((*ptrValue + bandwidth)/1000) <= sMaximumBandwidth)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrCurBandwidth failed with err = %"_S32BITARG_"\n", (SInt32)err);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// GetRTSPSessionID - returns the RTSP Session ID for the given RTSPSession object.
|
|
// return 0 on failure.
|
|
UInt32 GetRTSPSessionID(QTSS_RTSPSessionObject session)
|
|
{
|
|
UInt32 uSessID = 0L;
|
|
UInt32 paramLen = sizeof(uSessID);
|
|
|
|
QTSS_Error err = QTSS_GetValue(session, qtssRTSPSesID, 0, (void*)&uSessID, ¶mLen);
|
|
if (err != QTSS_NoErr)
|
|
{
|
|
uSessID = 0L;
|
|
}
|
|
return uSessID;
|
|
}
|
|
|
|
// IsActiveBroadcastSession - This is true when the specified session belongs to
|
|
// a current broadcast session. We use this to let us know if we were called back
|
|
// in our FilterRequest() role because a read or write was blocked previously.
|
|
inline Bool16 IsActiveBroadcastSession(QTSS_RTSPSessionObject session)
|
|
{
|
|
return (FindBroadcastSession(session) != NULL);
|
|
}
|
|
|
|
// FindBroadcastSession - Given a RTSP session find the MP3BroadcasterSession
|
|
// object that handles this session. returns NULL pointer if none.
|
|
MP3BroadcasterSession* FindBroadcastSession(QTSS_RTSPSessionObject session)
|
|
{
|
|
MP3Session* mp3sess = sMP3SessionTable.Resolve(session);
|
|
if (mp3sess != NULL && mp3sess->IsA() == kMP3BroadcasterSessionType)
|
|
return (MP3BroadcasterSession*) mp3sess;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
// This determines if an incoming request is an broadcaster sending
|
|
// us his password. It will be in the format of:
|
|
// "SOURCE <password> <mountpoint>\n".
|
|
Bool16 IsBroadcastPassword(StrPtrLen& theRequest)
|
|
{
|
|
if (IsShoutcastPassword(theRequest))
|
|
return true;
|
|
StrPtrLen token = theRequest;
|
|
token.Len = 6;
|
|
return token.Equal(StrPtrLen("SOURCE"));
|
|
}
|
|
|
|
// This determines if an incoming request is a Shoutcast broadcaster sending
|
|
// us his password. It will be in the format of:
|
|
// <password>\n".
|
|
Bool16 IsShoutcastPassword(StrPtrLen& theRequest)
|
|
{
|
|
char* bp = (char*)theRequest.Ptr;
|
|
int i;
|
|
int n = (int)theRequest.Len;
|
|
|
|
for (i=0; i<n; i++)
|
|
{
|
|
// any request with an embeded whitespace cannot be a
|
|
// Shoutcast password request.
|
|
if (*bp == ' ' || *bp == '\t')
|
|
return false;
|
|
}
|
|
// otherwise we assume it is.
|
|
return true;
|
|
}
|
|
|
|
Bool16 IsProtocolString(StrPtrLen& source, char *protocol)
|
|
{
|
|
|
|
UInt32 protocolLen = ::strlen(protocol);
|
|
if (protocolLen <= source.Len) // must be smaller or same
|
|
{
|
|
source.Len = protocolLen; // trim off the /version from protocol/version
|
|
if (source.EqualIgnoreCase(protocol))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
Bool16 IsProtocol(StrPtrLen& theRequest, char* protocol)
|
|
{
|
|
StringParser reqParse(&theRequest);
|
|
StrPtrLen line;
|
|
reqParse.GetThruEOL(&line);
|
|
StringParser lineParse(&line);
|
|
|
|
StrPtrLen lastWord;
|
|
StrPtrLen currentWord;
|
|
|
|
lineParse.ConsumeUntilWhitespace(¤tWord);
|
|
lineParse.ConsumeWhitespace();
|
|
|
|
if (IsProtocolString(currentWord, protocol))//test for line starting with protocol/vers in a response from the client
|
|
return true;
|
|
|
|
if (line.Len > 0) do
|
|
{
|
|
lastWord = currentWord;
|
|
lineParse.ConsumeUntilWhitespace(¤tWord);
|
|
lineParse.ConsumeWhitespace();
|
|
} while (currentWord.Len > 0);
|
|
|
|
if (IsProtocolString(lastWord, protocol))// test for line ending with protocol/version as in a request from the client
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
Bool16 IsHTTP(StrPtrLen& theRequest)
|
|
{
|
|
return IsProtocol(theRequest, "HTTP");
|
|
}
|
|
|
|
Bool16 IsRTSP(StrPtrLen& theRequest)
|
|
{
|
|
return IsProtocol(theRequest, "RTSP");
|
|
}
|
|
|
|
|
|
// This determines if an incoming request is an HTTP GET
|
|
// request.
|
|
Bool16 IsHTTPGet(StrPtrLen& theRequest)
|
|
{
|
|
StrPtrLen token = theRequest;
|
|
token.Len = 3;
|
|
Bool16 found = false;
|
|
if (token.EqualIgnoreCase(StrPtrLen("GET")) && IsHTTP(theRequest) )
|
|
found = true;
|
|
return found;
|
|
}
|
|
|
|
// Is this URL a *.m3u file request.
|
|
Bool16 IsA_m3u_URL(char* theURL)
|
|
{
|
|
if (::strstr(theURL, ".m3u") != 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// Is this URL a metdata xfer.
|
|
Bool16 IsMetaDataURL(char* theURL)
|
|
{
|
|
if (::strncmp(theURL, "/admin.cgi?mode=updinfo", 23) == 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// Parse this URL and extract metdata.
|
|
void ParseMetaDataURL(char* theURL)
|
|
{
|
|
char URLBuffer[kURLBufferSize];
|
|
|
|
if (theURL == NULL)
|
|
{
|
|
Assert(0);
|
|
return;
|
|
}
|
|
else if (::strlen(theURL) == 0)
|
|
{
|
|
Assert(0);
|
|
return;
|
|
}
|
|
DTRACE1("##ParseMetaDataURL() - meta-data URL <%s>\n", theURL);
|
|
|
|
StrPtrLen urlPtr(theURL);
|
|
StrPtrLen token;
|
|
StringParser urlParser(&urlPtr);
|
|
|
|
urlParser.ConsumeUntil(&token, '&');
|
|
if ( !token.Equal(StrPtrLen("/admin.cgi?mode=updinfo")) )
|
|
{
|
|
// expected '/admin.cgi?mode=updinfo' in URL!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
if ( !urlParser.Expect('&') )
|
|
{
|
|
// expected '&' in URL after '/admin.cgi?mode=updinfo'
|
|
Assert(0);
|
|
return;
|
|
}
|
|
urlParser.ConsumeUntil(&token, '=');
|
|
if ( !token.Equal(StrPtrLen("pass")) )
|
|
{
|
|
// expected 'pass=' in URL!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
if ( !urlParser.Expect('=') )
|
|
{
|
|
// expected '=' in URL after 'pass'!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
urlParser.ConsumeUntil(&token, '&');
|
|
if ( !token.Equal((const char*)sBroadcastPassword) )
|
|
{
|
|
DTRACE1("##ParseMetaDataURL() - expected Password of '%s' in URL!\n", sBroadcastPassword);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if ( !urlParser.Expect('&') )
|
|
{
|
|
// expected '&' in URL after 'pass=xxx'!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
urlParser.ConsumeUntil(&token, '=');
|
|
if ( !token.Equal(StrPtrLen("mount")) )
|
|
{
|
|
// expected 'mount=' in URL!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
|
|
if ( !urlParser.Expect('=') )
|
|
{
|
|
// expected '=' in URL after 'mount'!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
|
|
urlParser.ConsumeUntil(&token, '&');
|
|
if ( token.Len < 1 )
|
|
{
|
|
// expected mountpoint in URL!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if ( !urlParser.Expect('&') )
|
|
{
|
|
// expected '&' in URL after 'mount=xxx'!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
}
|
|
::memset(URLBuffer, 0, kURLBufferSize);
|
|
::memcpy(URLBuffer, token.Ptr, token.Len);
|
|
|
|
DTRACE1("##ParseMetaDataURL() - got mountpoint of '%s' in URL.\n", URLBuffer);
|
|
MP3BroadcasterSession* xs = sMP3BroadcasterQueue.FindByMountPoint(token);
|
|
if (xs == NULL)
|
|
{
|
|
DTRACE("##ParseMetaDataURL() - no matching mountpoint found!\n");
|
|
return;
|
|
}
|
|
|
|
urlParser.ConsumeUntil(&token, '=');
|
|
if ( !token.Equal(StrPtrLen("song")) )
|
|
{
|
|
// expected 'song=' in URL!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
|
|
if ( !urlParser.Expect('=') )
|
|
{
|
|
// expected '=' in URL after 'song'!
|
|
Assert(0);
|
|
return;
|
|
}
|
|
xs->SetSongName(urlParser.GetCurrentPosition());
|
|
}
|
|
|
|
void SendBroadcastAuthErr(QTSS_Filter_Params* inParams, char * errormessage)
|
|
{
|
|
if (errormessage != NULL)
|
|
{
|
|
UInt32 len = ::strlen(errormessage);
|
|
(void)QTSS_Write(inParams->inRTSPRequest,errormessage ,len, NULL, 0);
|
|
(void)QTSS_SetValue(inParams->inRTSPRequest, qtssRTSPReqRespMsg, 0, errormessage, len);
|
|
}
|
|
|
|
QTSS_SessionStatusCode inStatusCode = qtssClientUnAuthorized;
|
|
(void)QTSS_SetValue(inParams->inRTSPRequest, qtssRTSPReqStatusCode, 0, &inStatusCode, sizeof(inStatusCode));
|
|
|
|
const Bool16 sFalse = false;
|
|
(void)QTSS_SetValue(inParams->inRTSPRequest, qtssRTSPReqRespKeepAlive, 0, &sFalse, sizeof(sFalse));
|
|
|
|
}
|
|
|
|
// This parses and checks the password in the broadcaster's
|
|
// incoming request.
|
|
Bool16 CheckPassword(QTSS_Filter_Params* inParams, StrPtrLen& theRequest, StrPtrLen& mountpoint)
|
|
{
|
|
Bool16 passwordOK = false;
|
|
StrPtrLen strPtr;
|
|
|
|
mountpoint.Ptr = NULL;
|
|
mountpoint.Len = 0;
|
|
StringParser reqParse(&theRequest);
|
|
reqParse.ConsumeUntilWhitespace(&strPtr);
|
|
char* tmp;
|
|
|
|
if ( strPtr.Equal(StrPtrLen("SOURCE")) )
|
|
{
|
|
|
|
//it's an x-audio broadcast request
|
|
// PARSE X-AUDIO HERE!!!!
|
|
reqParse.ConsumeWhitespace();
|
|
reqParse.ConsumeUntilWhitespace(&strPtr);
|
|
tmp = strPtr.GetAsCString();
|
|
// Let's see if the password matches...
|
|
DTRACE1("CheckPassword() - #Got X-AUDIO password: %s\n", tmp);
|
|
if (strcmp(tmp, sBroadcastPassword) != 0)
|
|
{
|
|
SendBroadcastAuthErr(inParams, "ERROR - Bad Password\r\n"); //ice cast style error
|
|
delete[] tmp;
|
|
return false;
|
|
}
|
|
delete[] tmp;
|
|
// get the mount point
|
|
reqParse.ConsumeWhitespace();
|
|
reqParse.ConsumeUntilWhitespace(&strPtr);
|
|
mountpoint.Ptr = strPtr.Ptr;
|
|
mountpoint.Len = strPtr.Len;
|
|
tmp = strPtr.GetAsCString();
|
|
DTRACE1("CheckPassword() - #Got X-AUDIO mountpoint: %s\n", tmp);
|
|
delete[] tmp;
|
|
passwordOK = true;
|
|
}
|
|
else if ( IsShoutcastPassword(strPtr) )
|
|
{
|
|
tmp = strPtr.GetAsCString();
|
|
if (strPtr.Len > 1)
|
|
tmp[strPtr.Len] = '\0';
|
|
// Let's see if the password matches...
|
|
DTRACE1("CheckPassword() - #Got Shoutcast password: %s\n", tmp);
|
|
if (strcmp(tmp, sBroadcastPassword) != 0)
|
|
{
|
|
SendBroadcastAuthErr(inParams, "invalid password\r\n"); // shoutcast style error
|
|
delete[] tmp;
|
|
return false;
|
|
}
|
|
delete[] tmp;
|
|
mountpoint.Ptr = "/";
|
|
mountpoint.Len = 1;
|
|
passwordOK = true;
|
|
}
|
|
return passwordOK;
|
|
}
|
|
|
|
|
|
Bool16 GetHeaderAndValueFromeLine(StrPtrLen *line, StrPtrLen* header, char separator, StrPtrLen* value)
|
|
{
|
|
StringParser lineParser(line);
|
|
lineParser.ConsumeWhitespace();
|
|
|
|
Bool16 foundHeader = lineParser.GetThru(header, separator);
|
|
|
|
lineParser.ConsumeWhitespace();
|
|
lineParser.GetThruEOL(value);
|
|
|
|
return foundHeader;
|
|
}
|
|
|
|
|
|
// Find out if the HTTP header wants meta-data.
|
|
Bool16 NeedsMetaData(StrPtrLen& theRequest)
|
|
{
|
|
|
|
|
|
StrPtrLen line;
|
|
StrPtrLen header;
|
|
StrPtrLen value;
|
|
StringParser requestParser(&theRequest);
|
|
|
|
requestParser.GetThruEOL(NULL); // strip off command line
|
|
while ( requestParser.GetThruEOL(&line) || line.Len > 0 ) // pull out each line
|
|
{
|
|
if (!GetHeaderAndValueFromeLine(&line, &header, ':', &value))
|
|
return false; // no separator found
|
|
|
|
if (header.EqualIgnoreCase("icy-metadata") && (StringParser(&value).ConsumeInteger() > 0) )
|
|
return true; // icy-metadata: (non zero value)
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
/* NeedsMetaData Test cases
|
|
StrPtrLen temp;
|
|
temp.Set("\r\nIcy-metadatA: 1\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 1-1\n");
|
|
|
|
temp.Set("\ricy-metadata: 1\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 1-2\n");
|
|
|
|
temp.Set("\nicy-metadata: 1\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 1 -3\n");
|
|
|
|
temp.Set("\r\nicy-metadata:1\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata:1-4\n");
|
|
|
|
temp.Set("\r\nicy-metadata:\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata:-5\n");
|
|
|
|
temp.Set("\r\nicy-metadata 1\n");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata 1-6\n");
|
|
|
|
temp.Set("\r\nicy-metadata: 0\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 0-7\n");
|
|
|
|
temp.Set("\r\nicy-metadata: 3\r");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 3-8\n");
|
|
|
|
temp.Set("\r\nIcy-metadatA: 1");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 1-0\n");
|
|
|
|
temp.Set("\nIcy-metadatA: 1");
|
|
if ( NeedsMetaData(temp) )
|
|
printf("icy-metadata: 1-10\n");
|
|
*/
|
|
|
|
|
|
// Parse out the URL from the HTTP GET line.
|
|
Bool16 ParseURL(StrPtrLen& theRequest, char* outURL, UInt16 maxlen)
|
|
{
|
|
StringParser reqParse(&theRequest);
|
|
StrPtrLen strPtr;
|
|
|
|
::memset(outURL, 0, maxlen);
|
|
reqParse.ConsumeWord(&strPtr);
|
|
|
|
if ( !strPtr.Equal(StrPtrLen("GET")) )
|
|
{
|
|
return false;
|
|
}
|
|
reqParse.ConsumeWhitespace();
|
|
reqParse.ConsumeUntilWhitespace(&strPtr);
|
|
if (strPtr.Len == 0)
|
|
return false;
|
|
else if ((UInt16)strPtr.Len > maxlen-1)
|
|
strPtr.Len = maxlen-1;
|
|
::memcpy(outURL, strPtr.Ptr, strPtr.Len);
|
|
|
|
return true;
|
|
}
|
|
|
|
void WriteStartupMessage()
|
|
{
|
|
if (!sServerIdle)
|
|
return;
|
|
sServerIdle = false;
|
|
|
|
//format a date for the startup time
|
|
char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes];
|
|
Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, false);
|
|
|
|
char tempBuffer[1024];
|
|
if (result)
|
|
qtss_sprintf(tempBuffer, "#Remark: Streaming beginning STARTUP %s\n", theDateBuffer);
|
|
|
|
// log startup message to error log as well.
|
|
if ((result) && (sMP3AccessLog != NULL))
|
|
sMP3AccessLog->WriteToLog(tempBuffer, kAllowLogToRoll);
|
|
}
|
|
|
|
void WriteShutdownMessage()
|
|
{
|
|
if (sServerIdle)
|
|
return;
|
|
sServerIdle = true;
|
|
|
|
//log shutdown message
|
|
//format a date for the shutdown time
|
|
char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes];
|
|
Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, false);
|
|
|
|
char tempBuffer[1024];
|
|
if (result)
|
|
qtss_sprintf(tempBuffer, "#Remark: Streaming beginning SHUTDOWN %s\n", theDateBuffer);
|
|
|
|
if ( result && sMP3AccessLog != NULL )
|
|
sMP3AccessLog->WriteToLog(tempBuffer, kAllowLogToRoll);
|
|
}
|
|
|
|
QTSS_Error LogRequest(QTSS_RTSPSessionObject inRTSPSession, MP3ClientSession* client)
|
|
{
|
|
char logbuffer[1024];
|
|
char reqBuffer[kRequestBufferSize];
|
|
char userAgent[kUserAgentBufferSize];
|
|
char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes];
|
|
UInt16 reqResult = 200;
|
|
UInt32 byteCount = 54000000;
|
|
UInt32 duration = 0;
|
|
QTSS_Error theError = QTSS_NoErr;
|
|
|
|
// Sanity check...
|
|
if (sMP3AccessLog == NULL)
|
|
return QTSS_NoErr;
|
|
|
|
// LogRequest() is not re-entrant. Hold a mutex until done.
|
|
OSMutexLocker locker(sLogMutex);
|
|
|
|
// Construct the timestamp for the entry
|
|
Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, sLogTimeInGMT);
|
|
// this should never happen, but just in case...
|
|
if (!result)
|
|
theDateBuffer[0] = '\0';
|
|
|
|
// Get the IP address of the client who made the request
|
|
char remoteAddress[kRemoteAddressSize] = {0};
|
|
StrPtrLen theClientIPAddressStr;
|
|
theClientIPAddressStr.Set(remoteAddress,kRemoteAddressSize);
|
|
QTSS_Error err = QTSS_GetValue( inRTSPSession,
|
|
qtssRTSPSesRemoteAddrStr,
|
|
0,
|
|
(void*)theClientIPAddressStr.Ptr,
|
|
&theClientIPAddressStr.Len);
|
|
if (err != QTSS_NoErr)
|
|
::strcpy(remoteAddress, "127.0.0.1");
|
|
|
|
// Pull the actual HTTP request line from the MP3 client session.
|
|
::strcpy(reqBuffer, client->GetRequest());
|
|
|
|
// Get the user agent's name or "unknown user agent" if none was specified
|
|
// in the original HTTP request.
|
|
userAgent[0] = '\0';
|
|
::strcpy(userAgent, client->GetUserAgent());
|
|
if (userAgent[0] == '\0')
|
|
::strcpy(userAgent, "unknown user agent");
|
|
|
|
// Determine how may bytes this client was streamed.
|
|
byteCount = client->GetTotalCount();
|
|
|
|
// Calculate the total connect duration in seconds.
|
|
duration = (UInt32)((OS::Milliseconds() - client->GetConnectTime())/1000);
|
|
|
|
// Finally, get the HTTP result code.
|
|
reqResult = client->GetResult();
|
|
|
|
// Format the access log entry parameters here...
|
|
qtss_sprintf(logbuffer, "%s \"%s\" [%s] \"%s\" %d %"_S32BITARG_" %"_S32BITARG_"\n",
|
|
remoteAddress,
|
|
userAgent,
|
|
theDateBuffer,
|
|
reqBuffer,
|
|
reqResult,
|
|
byteCount,
|
|
duration
|
|
);
|
|
|
|
// Commit it to disk...
|
|
sMP3AccessLog->WriteToLog(logbuffer, kAllowLogToRoll);
|
|
return theError;
|
|
}
|
|
|
|
// url_strcpy - works like strcpy except that it handles URL escape
|
|
// conversions as it copies the string.
|
|
void url_strcpy(char* dest, const char* src)
|
|
{
|
|
int c1, c2;
|
|
while(*src)
|
|
{
|
|
if (*src == '%')
|
|
{
|
|
src++;
|
|
c1 = *src++;
|
|
if (c1 >= '0' && c1 <= '9')
|
|
c1 -= '0';
|
|
else if (c1 >= 'A' && c1 <= 'F')
|
|
c1 -= 'A' + 10;
|
|
else if (c1 >= 'a' && c1 <= 'f')
|
|
c1 -= 'a' + 10;
|
|
else
|
|
c1 = 0;
|
|
c2 = *src;
|
|
if (c2 >= '0' && c2 <= '9')
|
|
c2 -= '0';
|
|
else if (c2 >= 'A' && c2 <= 'F')
|
|
c2 -= 'A' + 10;
|
|
else if (c2 >= 'a' && c2 <= 'f')
|
|
c2 -= 'a' + 10;
|
|
else
|
|
c2 = 32;
|
|
*dest = (c1 * 16) + c2;
|
|
}
|
|
else
|
|
{
|
|
*dest = *src;
|
|
}
|
|
// we need to replace single quotes with
|
|
// a tick so that the client can parse meta-data.
|
|
if (*dest == '\'')
|
|
{
|
|
*dest = '`';
|
|
}
|
|
dest++;
|
|
src++;
|
|
}
|
|
*dest = '\0';
|
|
}
|
|
|
|
// ReEnterFilterRequest - This function is called whenever we
|
|
// have called back our module in the QTSS_FilterRequest role
|
|
// after calling it the first time.
|
|
// This will happen in one of two ways:
|
|
// 1. A MP3BroadcasterSession posted a read request event and data
|
|
// became available on its stream.
|
|
// 2. The Idle timer called us back to poll a MP3ClientSession to
|
|
// see if it needs to die.
|
|
QTSS_Error ReEnterFilterRequest(QTSS_Filter_Params* inParams, MP3Session* mp3Session)
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
QTSS_RTSPRequestObject theRequest = inParams->inRTSPRequest;
|
|
|
|
if (mp3Session != NULL)
|
|
{
|
|
UInt8 sessTyp = mp3Session->IsA();
|
|
switch(sessTyp)
|
|
{
|
|
case kMP3BroadcasterSessionType:
|
|
// this is a MP3 broadcaster session. Invoke it's state machine.
|
|
return ((MP3BroadcasterSession*)mp3Session)->ExecuteState();
|
|
case kMP3ClientSessionType:
|
|
// this is a MP3 client session. If we are active schedule ourself
|
|
// to be called back again in kClientPollInterval milliseconds.
|
|
if (mp3Session->GetState() == MP3ClientSession::kClientSendDataState)
|
|
{
|
|
MP3ClientSession* client = (MP3ClientSession*)mp3Session;
|
|
if (client->WasBlocked())
|
|
client->RetrySendData();
|
|
|
|
KeepSession(theRequest, true);
|
|
err = QTSS_SetIdleTimer(kClientPollInterval);
|
|
return err;
|
|
}
|
|
else
|
|
{
|
|
KeepSession(theRequest, false);
|
|
}
|
|
return QTSS_NoErr;
|
|
default:
|
|
Assert(0);
|
|
return QTSS_NoErr;
|
|
}
|
|
}
|
|
return err;
|
|
}
|
|
|
|
// FilterRequest - filter the incoming HTTP/Broadcast request.
|
|
QTSS_Error FilterRequest(QTSS_Filter_Params* inParams)
|
|
{
|
|
QTSS_Error err = QTSS_NoErr;
|
|
QTSS_RTSPRequestObject theRequest = inParams->inRTSPRequest;
|
|
|
|
// Pull the actual request data from this RTSP session's attributes.
|
|
|
|
StrPtrLen theFullRequest;
|
|
err = QTSS_GetValuePtr(theRequest, qtssRTSPReqFullRequest, 0, (void**)&theFullRequest.Ptr, &theFullRequest.Len);
|
|
if (err != QTSS_NoErr)
|
|
{
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
if (IsRTSP(theFullRequest)) // don't process rtsp requests
|
|
return QTSS_NoErr;
|
|
|
|
if (!sMP3StreamingEnabled || sServerIdle)
|
|
{
|
|
// This allows MP3 streaming to be disabled through a pref setting
|
|
// or ignore requests when the server is in the idle state.
|
|
|
|
if (NeedsMetaData(theFullRequest)) // this is an HTTP icy request
|
|
return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssServerUnavailable,true, NULL);
|
|
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
// See if this RTSP session is an existing MP3 Session.
|
|
// If it is then we have re-entered FilterRequest() from a previous
|
|
// invokation and we will call ReEnterFilterRequest() to do its thing.
|
|
MP3Session* mp3Session = sMP3SessionTable.Resolve(inParams->inRTSPSession);
|
|
if (mp3Session != NULL)
|
|
{
|
|
err = ReEnterFilterRequest(inParams, mp3Session);
|
|
return err;
|
|
}
|
|
|
|
// Check and see if this request is a broadcaster sending it's password or
|
|
// an HTTP GET. If it's not, it's assumed to be a RTSP request and we ignore
|
|
// it. In that case, some other module must handle this request.
|
|
#if DEBUG_MP3STREAMING_MODULE
|
|
DTRACE("####\n");
|
|
PrintStringBuffer(theFullRequest);
|
|
#endif
|
|
if (!IsBroadcastPassword(theFullRequest) && !IsHTTPGet(theFullRequest))
|
|
{
|
|
return QTSS_NoErr;
|
|
}
|
|
|
|
MP3BroadcasterSession* xs = NULL;
|
|
|
|
#if DEBUG_MP3STREAMING_MODULE
|
|
// Debugging info - Retrieve the RTSP session ID associated with this session.
|
|
UInt32 uSessID = 0L;
|
|
uSessID = GetRTSPSessionID(inParams->inRTSPSession);
|
|
DTRACE1("####\n###Filtering SessionID = %"_S32BITARG_"\n", uSessID);
|
|
#endif
|
|
|
|
// See if we have exceeded our maximum number of client connections
|
|
if (MP3Session::GetTotalNumMP3Sessions() >= sMaximumConnections)
|
|
{
|
|
// we are currently at our connection limit. return a 400 error for compatibility with icecast style error.
|
|
return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssClientBadRequest,true, NULL);
|
|
}
|
|
else if (!CheckBandwidth(kBandwidthToAddEstimate))
|
|
{
|
|
// we are currently at our bandwidth limit. return a 400 error for compatibility with icecast style error.
|
|
return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssClientBadRequest,true, NULL);
|
|
}
|
|
|
|
StrPtrLen mountpoint;
|
|
if (!IsHTTP(theFullRequest) && IsBroadcastPassword(theFullRequest) && CheckPassword(inParams, theFullRequest, mountpoint))
|
|
{
|
|
err = sMP3BroadcasterQueue.CreateBroadcaster(inParams->inRTSPSession, inParams->inRTSPRequest, mountpoint);
|
|
if (err != QTSS_NoErr)
|
|
{
|
|
// CreateBroadcaster returned an error but it has already sent the reject reply
|
|
// so there's nothing left to do.
|
|
return QTSS_NoErr;
|
|
}
|
|
// If we're here this is a new connection request from a broadcaster.
|
|
// Read the broadcast headers from the broadcaster
|
|
xs = FindBroadcastSession(inParams->inRTSPSession);
|
|
if (xs != NULL)
|
|
err = xs->GetBroadcastHeaders();
|
|
else
|
|
err = QTSS_NoErr;
|
|
return err;
|
|
}
|
|
else if ( IsHTTPGet(theFullRequest) )
|
|
{
|
|
char theURL[kURLBufferSize];
|
|
|
|
MP3BroadcasterSession* xs = NULL;
|
|
if (!ParseURL(theFullRequest, theURL, kURLBufferSize))
|
|
{
|
|
return QTSS_NoErr;
|
|
}
|
|
// if this was a '*.m3u' file URL then handle it here
|
|
// sendind back a reply containing the URL.
|
|
// This effectively accomplishes a redirect.
|
|
if ( IsA_m3u_URL(theURL) )
|
|
{
|
|
char tmpbuf[1024];
|
|
tmpbuf[sizeof(tmpbuf) -1] = 0;
|
|
char tmp[1024];
|
|
tmp[sizeof(tmp) -1] = 0;
|
|
// remove the '.m3u' prefix from the URL
|
|
UInt32 ulen = ::strlen(theURL) - 4;
|
|
UInt32 serverIPAddr;
|
|
UInt8 x1, x2, x3, x4;
|
|
theURL[ulen] = '\0';
|
|
// Get the server IP address for building the reply playlist path
|
|
ulen = sizeof(serverIPAddr);
|
|
err = QTSS_GetValue(sServer, qtssSvrDefaultIPAddr, 0, &serverIPAddr, &ulen);
|
|
if (err == QTSS_NoErr)
|
|
{
|
|
x1 = (UInt8)((serverIPAddr >> 24) & 0xff);
|
|
x2 = (UInt8)((serverIPAddr >> 16) & 0xff);
|
|
x3 = (UInt8)((serverIPAddr >> 8) & 0xff);
|
|
x4 = (UInt8)((serverIPAddr) & 0xff);
|
|
}
|
|
else
|
|
{
|
|
x1 = 127; x2 = x3 = 0; x4 = 1;
|
|
}
|
|
|
|
// construct the reply string for the client.
|
|
qtss_snprintf(tmp,sizeof(tmp) -1, "http://%d.%d.%d.%d:8000%s", x1,x2,x3,x4, theURL);
|
|
ulen = ::strlen(tmp);
|
|
qtss_snprintf(tmpbuf,sizeof(tmpbuf) -1, "%s %"_U32BITARG_"\r\n\r\n%s\r\n", gM3UReplyHeader, ulen, tmp);
|
|
ulen = ::strlen(tmpbuf);
|
|
// send the reply to the client.
|
|
err = QTSS_Write(inParams->inRTSPRequest, tmpbuf, ulen, NULL, qtssWriteFlagsBufferData);
|
|
return QTSS_NoErr;
|
|
}
|
|
// if this was a meta-data transfer URL then handle it here
|
|
if ( IsMetaDataURL(theURL) )
|
|
{
|
|
// Parse meta-data request here
|
|
ParseMetaDataURL(theURL);
|
|
// tell the broadcaster we got it.
|
|
err = QTSS_Write(inParams->inRTSPRequest, kOKHeader, ::strlen(kOKHeader), NULL, qtssWriteFlagsBufferData);
|
|
return QTSS_NoErr;
|
|
}
|
|
// Since this URL was not a metadata transfer let's assume it is
|
|
// a broadcast mount point and search for it.
|
|
xs = sMP3BroadcasterQueue.FindByMountPoint(theURL);
|
|
if (xs == NULL)
|
|
{
|
|
if (NeedsMetaData(theFullRequest)) // this is an HTTP icy request
|
|
return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssClientNotFound,true, NULL);
|
|
|
|
// Let some other module handle this URL.
|
|
return QTSS_NoErr;
|
|
}
|
|
// if we're here this is a new MP3 connection request from a client.
|
|
// Start the clients stream up...
|
|
err = xs->AddClient(inParams->inRTSPSession, inParams->inRTSPRequest);
|
|
return err;
|
|
}
|
|
return QTSS_NoErr;
|
|
}
|
|
|