这是一个在SYMBIAN 2/3版实现HTTP网络广播流媒体播放的example,其中本地音频播放采用CMdaAudioPlayerUtility类,流媒体播放采用CMdaAduioOutputStream类,目前支持mp3和AAC+解码,同时支持绝大部分的音频本地文件回放(MP3, AAC, eAAC+, MP4, M4A, WMA, 3GPP, AMR, and WAV),总之很强大的例子。下面转wertcsdn博友的《shoutcast例子分析》
程序源代码及自带文档下载
Internet Radio是一个为S60第三版设备开发的声音应用参考实现,支持SHOUTcast (America Online) 协议。
目的:为开发更多新颖的S60声音应用提供一个学习工具。
两个主要组成部分,UI和Engine。(一般比较好的程序都是这么设计的,以保证更好的模块化)。
在多处使用了Adapter设计模式:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
普通的单view架构,类图如下所示,这是标准的Symbian图形化应用程序框架之一(共有三种),大多数Symbian应用程序都是用的这个框架。
AppUi实现了MAdapterObserver接口,其作用是由CPlayerAdapter和Engine(通过CPluginAdapter)回调显示元数据及状态信息。
class CS60InternetRadioAppUi : public MAdapterObserver, public CAknAppUi
另外该类又包含了如下类。
private: // Data
CPlayerAdapter* iPlayerAdapter; // the audio player adapter 播放本地文件
MAudioAdapter* iAudioAdapter; // the current audio adapter 一个统一的接口
CS60InternetRadioView* iAppView; //the application view 界面显示部分
RPointerArray<CPluginAdapter> iAudioPlugins; // list of plugin adapters 播放shoutcast流
CPlayerAdapter封装了系统的CMdaAudioPlayerUtility类进行本地文件播放,并提供相应接口给AppUi调用。
CPluginAdapter提供了实现EComPlugin接口,实现shoutcast流数据的播放。
MAudioAdapter抽象出了播放本地文件和shoutcast流内容的所需接口,以及一些界面显示接口。
CPlayerAdapter和CPluginAdapter都实现MAudioAdapter接口,UI可以使用同一个接口处理播放本地文件或者播放Shoutcast流数据。
class CPlayerAdapter : public CBase, public MAudioAdapter, public MMdaAudioPlayerCallback
class CPluginAdapter : public CBase, public MAudioAdapter
MAudioAdapter* iAudioAdapter;
if ( iAudioPlugins.Count() ) // If plugin adapters were found, use the first one on the list.
{
iAudioAdapter = iAudioPlugins[0];
}
else // otherwise, we use the file player adapter
{
iAudioAdapter = iPlayerAdapter;
}
AppView主要处理界面上的Label的显示以及进行按键处理,如左右键为调低、调高音量。
程序引擎由两部分组成,但使用同一个接口MAudioAdapter来播放本地文件和shoutcast流:
1. Shoutcast stream播放功能采用ECom Plugin实现,具体通过CShoutcastAdapter类实现(下一节介绍)。
2. 本地文件播放功能通过CPlayerAdapter类实现,封装了系统的CMdaAudioPlayerUtility(Adapter模式)。
CPlayerAdapter类图
封装了系统的CMdaAudioPlayerUtility类进行声音文件的本地播放,并提供相应接口给AppUi调用。其完成的工作基本上就是对CMdaAudioPlayerUtility功能上的封装,以及播放状态控制,音量控制和回调UI显示播放信息。以下函数基本上可以说明所有问题:(注意颜色上的对应)
void CPlayerAdapter::PlayL()
{
if ( iState == EReadyToPlay )
{
iMdaAudioPlayerUtility->Play();
iState = EPlaying;
iTempMetadata.Copy(KPALocalPlayback);
iObserver.SetMetadata(KMetadataServer, iTempMetadata, ETrue);
ShowMetadata();
}
}
SHOUTCast流播放功能是通过EComPlugin实现的,该机制使程序引擎具有良好的可扩展性。它使用CMdaAduioOutputStream来播放流数据,而音频解码功能是通过系统自带的Audio Subsystem来完成的。
CShoutcastAdapter类图如下:
该类封装了网络数据处理,流数据缓冲,元数据抽取,声音播放等功能。关键类的具体实现及处理过程如下:
每一帧信息的结构体,定义如下:
class TAudioFrameInfo
{
public:
TInt iMode; // encoding mode
TInt iBitRate; // bitrate (bit/s)
TInt iSamplingRate; // sampling frequency (Hz)
TInt iChannels; // number of channels
TInt iFrameSize; // encoded size (bytes)
TInt iFrameSamples; // decoded size (samples per channel)
TInt iSamplingRateOut; // sampling frequency after conversion (Hz)
TInt iChannelsOut; // number of audio channels after conversion (1 or 2)
TInt iFrameSamplesOut; // decoded size after conversion (samples per channel)
TInt iPadding; // padding flag (TRUE or FALSE, TRUE if p slot exists)
TInt iId; // id of algorithm (1 MPEG-1, 0 MPEG-2)
};
元数据的基本类,主要有name和value两个元素,及提供一系列操作这两个元素的方法。
HBufC* iName;
HBufC* iValue;
在CShoutcastStream类中有如下定义:
RPointerArray<CMetaDataEntry> iMetadata;
继承自Active object,使得回调可以异步实现,来防止客户端的可重入代码(re-entrant code)。
static CEventDispatcher* NewL(MShoutcastStreamObserver& aObserver);
其中aObserver为指向CShoutcastAdapter的指针。
提供给其它类的接口为SendEvent:在一个context jump后调用事件处理函数(见RunL),防止可重入代码。
void CEventDispatcher::SendEvent(TUid aEvent, TInt aError)
{
iEvent = aEvent;
iError = aError;
if (!IsActive())
{
TRequestStatus* s = &iStatus;
SetActive(); //Context jump
User::RequestComplete(s, KErrNone);
}
}
Active Object运行后调用该函数,回调处理该事件。
void CEventDispatcher::RunL()
{
iObserver.HandleEvent(iEvent, iError);
}
Shoutcast Stream接口类,定义如下: (CShoutcastAdapter实现该接口)
class MShoutcastStreamObserver
{
public:
virtual void HandleEvent(TUid aEvent, TInt aError) = 0;
};
该类继承自Shoutcast plugin并实现了MShoutcastStreamObserver接口。核心成员变量:
// The shoutcast stream object
CShoutcastStream* iStreamSource;
//this is the playlist. Even entries (0, 2, 4 ...)are the URLs, odd entries are the Titles of the URLs
RPointerArray<HBufC8> iURLs;
enum TOp
{
EOpOpen, EOpAddDataSink, EOpAddDataSource, EOpPrime, EOpPlay
};
该类主要实现的功能:通过封装了CShoutcastStream,实现了MAudioAdapter接口。
1。查找并解析playlist文件:
通过void CShoutcastAdapter::FindPlayListsL() 和 ParsePlaylistFileL() 函数实现。
2。打开、关闭URL:
将MShoutcastStreamObserver接口传入CShoutcastStream,并通过MAdapterObserver通知AppUi在界面上显示Metadata。
3。Menu相关处理:
如获取Menu显示的text,动态添加Menu项,更新Menu显示当前Adapter的可选项等
4。通过封装了CShoutcastStream,实现MAudioAdapter接口,并做了一些状态判断和界面显示功能以及播放音量的控制。
5。出来来自CShoutcastStream的事件,通过void CShoutcastAdapter::HandleEvent()函数实现。主要是根据来自CShoutcastStream的事件进行播放状态的控制和界面元数据的显示更新。
Shoutcast engine的核心类。处理Shoutcast协议,与Symbian声音播放系统的接口,以及解析流中的元数据。
类定义: 是一个Active Object,并实现MMdaAudioOutputStreamCallback接口,有AudioSystem回调。
class CShoutcastStream : public CActive, public MMdaAudioOutputStreamCallback
还包含以下主要成员变量:
enum TState
{
ENotConnected, EResolving, EConnecting, ESendingRequest, EReceivingResponse,
EAnswerOK, EAnswerError, EInitMP3Dec, EInitStreamOutput, EData, EDisconnecting
};
TState iState; // Current engine state 当前状态
CEventDispatcher* iDispatcher; // Event dispatcher 事件处理
CMdaAudioOutputStream* iStreamOutput; // Output Stream object 对系统类的封装
TBuf8<URL_SIZE> iURL; //// Buffers for url parsing 解析URL
// stream buffering related 缓冲区相关
TUint8 *iBuffer2Decode; TUint8 *iBuffer2Play;
TPtr8 iPtrBuffer2Decode; TPtr8 iPtrBuffer2Play;
//MetaData related 元数据相关
RPointerArray<CMetaDataEntry> iMetadata;
TBuf<METADATA_BUFFER_SIZE> iTempMetadata;
// Networking related 网络相关
TInetAddr* iAddr; RSocketServ iSocksvr; RSocket iSock;
其构造函数中将MShoutcastStreamObserver放入到CEventDispatcher中,来通知CShoutcastAdapter。
void CShoutcastStream::ConstructL(const TDesC8& aUrl, MShoutcastStreamObserver& aObserver )
{
iURL.Copy(aUrl);
iTempMetadata.Copy(iURL);
GetIPFromURL(); //把aUrl分成IP,PORT和PATH三部分分别放入iIp,port,iPath
iDispatcher = CEventDispatcher::NewL(aObserver);
CActiveScheduler::Add(this);
}
总体播放流程:
用户选中Menu里面Station中的某个连接,AppUi发现是属于Plugin的Menu,便选中某个plugin,然后会自动调用plugin的HandleCommandL(),在该函数中调用OpenUrlL(i);和PlayL();
其中OpenUrlL(i)就是new一个CShoutcastStream类,最终调用到上面这段CShoutcastStream::ConstructL代码。
而PlayL()在第一次播放(而非暂停后再播放)总是会调用iStreamSource->PrimeL()。
所以CShoutcastStream中的代码流程开始展开,PrimeL()完成了以下事情:
{
iBuffer2Decode = new (ELeave) TUint8[BUFFER2DECODE_SIZE];
// allocate the play buffer
iBuffer2Play = new (ELeave) TUint8[BUFFER2PLAY_SIZE];
iMetadataBuffer = new (ELeave) TUint8[METADATA_BUFFER_SIZE];
ResetBufferVars();
//-add metadata fields. Intended: server name, server genre, artist, song, bitrate (constant) + kHz + stereo, bytes by now + cost (total: 6)
CMetaDataEntry *mmfMetadata;
iTempMetadata.Copy(_L(""));
// a total of 6 meta data entries
ConnectToServerL();
}
void CShoutcastStream::ConnectToServerL(TBool aReconnecting )
{
err = iSocksvr.Connect();
err = iSock.Open(iSocksvr, KAfInet, KSockStream, KProtocolInetTcp);
//最终效果为在界面上弹出一个等待的dialog,里面写着连接的URL
iDispatcher->SendEvent(TUid::Uid(KShoutcastStreamUid),
aReconnecting?KShoutcastEvent_Reconnecting:KShoutcastEvent_Connecting);
iState = EConnecting;
iSock.Connect(*iAddr, iStatus);
SetActive();
}
//Active Object的处理引擎,所有状态在这里集中处理。
CShoutcastStream::RunL()
{
switch ( iState )
{
case EConnecting:
SendRequestToServerL();
break;
case ESendingRequest:
ReceiveResponseFromServerL();
break;
case EReceivingResponse:
ParseResponseFromServerL();
break;
case EData:
{
if ( iStatus != KErrNone )
{
CloseAndClean();
Disconnect(iStatus.Int());
return;
}
if ( ReadRequestHelperDone() != KErrNone )
{
CloseAndClean();
Disconnect(iStatus.Int());
return;
}
};
break;
default:
ASSERT(0);
}
void CShoutcastStream::SendRequestToServerL()
{
//We use iPtrBuffer2Decode to build the HTTP request
ResetBufferVars();
//first line of the request
iPtrBuffer2Decode.Copy(KSCGet);
iPtrBuffer2Decode.Append(iPath);
iPtrBuffer2Decode.Append(KSCHttp10);
//next lines of the request
iPtrBuffer2Decode.Append(KSCUserAgent);
iPtrBuffer2Decode.Append(KSCAccept);
//Indicate to the server we want to receive metadata
iPtrBuffer2Decode.Append(KSCIcyMetadata);
iPtrBuffer2Decode.Append(KSCConnectionClose);
//send it off
iState = ESendingRequest;
iSock.Write(iPtrBuffer2Decode, iStatus);
SetActive();
}
void CShoutcastStream::ReceiveResponseFromServerL()
{
//receive the HTTP answer
iState = EReceivingResponse;
iPtrBuffer2Decode.SetLength(0);
iSock.Read(iPtrBuffer2Decode,iStatus);
SetActive();
}
void CShoutcastStream::ParseResponseFromServerL()
{
//获取下列字段的值,并初始化相应的参数:HTTP Response
_LIT8(KSCOKHeader, "200 OK");
_LIT8(KSCContentTypeTag, "content-type");
_LIT(KSCMimeTypeAudioMpeg, "audio/mpeg");
_LIT(KSCMimeTypeAudioAacp, "audio/aacp");
_LIT(KSCMimeTypeAudioAac, "audio/aac");
_LIT8(KSCIcyMetaintTag, "icy-metaint");
_LIT8(KSCIcyNameTag, "icy-name");
_LIT8(KSCIcyGenreTag, "icy-genre");
_LIT8(KSCIcyBrTag, "icy-br");
GetAudioSettings(); //得到iAudioSettings.iSampleRate和iAudioSettings.iChannels
iState = EInitStreamOutput;
// Notify the client that metadata is available
iDispatcher->SendEvent(TUid::Uid(KShoutcastStreamUid),0x01FF);
// Initialize the output stream
InitStreamOutputL();
}
void CShoutcastStream::InitStreamOutputL()
{
iStreamOutput = CMdaAudioOutputStream::NewL(*this);
iStreamOutput->SetPriority(80 , EMdaPriorityPreferenceTimeAndQuality);
iStreamOutput->Open(&iSettings);
}
//Once the stream is opened, we can set audio properties, volume, and priority.
void CShoutcastStream::MaoscOpenComplete(TInt aError )
{
if ( aError == KErrNone )
{
TRAP(err, iStreamOutput->SetAudioPropertiesL(iAudioSettings.iSampleRate, iAudioSettings.iChannels));
TRAP(err, iStreamOutput->SetDataTypeL(iDataType.FourCC()));
iStreamOutput->SetVolume(iVolume);
//announce the client that we are connected
iDispatcher->SendEvent(TUid::Uid(KShoutcastStreamUid),KShoutcastEvent_Connected);
iState = EData;//this should be the only place where iState gets assigned the EData value!
}
else
{
iDispatcher->SendEvent(TUid::Uid(KShoutcastStreamUid), aError);
}
}
当从网上收到声音数据后,就会调用:
void CShoutcastStream::MaoscBufferCopied(TInt aError, const TDesC8& /*aBuffer*/)
{
iWritingToStream = EFalse;
// if no error, write more data to the output stream
if ( !aError )
{
TRAPD(err, FillBufferL());
}
}
// Fill the play buffer with encoded data and send it to the stream output.
void CShoutcastStream::FillBufferL()
{
if ( iWritingToStream )
{
// Still busy writing to stream. We have not received MaoscBufferCopied.
return;
}
else
{
iWritingToStream = ETrue;
}
TInt ldTotal;
iLenBuffer2Play=iPtrBuffer2Play.MaxLength();
if ( iLenBuffer2Decode+iReadData < iLenBuffer2Play )
{
//not enough data to play
iDispatcher->SendEvent(TUid::Uid(KShoutcastStreamUid),KShoutcastEvent_BufferEmpty);
iLenBuffer2Play=0;
iPausedForBuffering = ETrue;
iWritingToStream = EFalse;
}
else
{
//there is enough data to be played!
if ( iLenBuffer2Decode >= iLenBuffer2Play )
{
//copy all the data into the buffer
iPtrBuffer2Play.Copy(iBuffer2Decode+iPosBuffer2Decode,iLenBuffer2Play);
iPosBuffer2Decode+=iLenBuffer2Play;
iLenBuffer2Decode-=iLenBuffer2Play;
}
else
{
//we have to copy the entire end of the buffer, then also the beginning of the buffer
//copy the end of the buffer
iPtrBuffer2Play.Copy(iBuffer2Decode+iPosBuffer2Decode, iLenBuffer2Decode);
TInt dataLeft = iLenBuffer2Play-iLenBuffer2Decode;
//we need to go from case 2 to case 1 (when there is no buffer to copy)
ASSERT(iReadData > 0);
iPosBuffer2Decode=0;
iLenBuffer2Decode=iReadData;
iReadData=-1;//going from case 2 to case 1
//append the remaining data
ASSERT(dataLeft <= iLenBuffer2Decode);//due to the first if from the HwDevice else
iPtrBuffer2Play.Append(iBuffer2Decode+iPosBuffer2Decode,dataLeft);
iPosBuffer2Decode+=dataLeft;
iLenBuffer2Decode-=dataLeft;
}
}
ASSERT(iLenBuffer2Play >= 0);
ldTotal = iLenBuffer2Play;
if ( iLenBuffer2Play > 0 )
{
LOG1("FillBufferL: Calling WriteL for %d bytes",iLenBuffer2Play);
iStreamOutput->WriteL(iPtrBuffer2Play); //真正播放出声音的函数
}
//buffering stuff
if(iPosBuffer2Decode>BUFFER2DECODE_SIZE-iMaxFrameSize-READ_EPSILON && iLenBuffer2Decode<iMaxFrameSize)
{
//we passed the upper safety limit!
memmove(iBuffer2Decode+iMaxFrameSize-iLenBuffer2Decode,
iBuffer2Decode+iPosBuffer2Decode,iLenBuffer2Decode);
iPosBuffer2Decode=iMaxFrameSize-iLenBuffer2Decode;
iLenBuffer2Decode+=iReadData;
iReadData=-1;//going from case 2 to case 1
};
//buffering fill
TInt bufferFill;
if(iReadData==-1)
{
bufferFill=(TInt)(100.0*iLenBuffer2Decode/(BUFFER2DECODE_SIZE-iMaxFrameSize));
}
else
{
bufferFill=(TInt)(100.0*(iLenBuffer2Decode+iReadData)/(BUFFER2DECODE_SIZE-iMaxFrameSize));
}
//fill buffering metadata
iTempMetadata.Format(KSCBufFormat, bufferFill);
iMetadata[6]->SetValueL(iTempMetadata);
//update metadata on the client/controller
if ( !iPausedForBuffering )
{
iDispatcher->SendEvent(TUid::Uid(KShoutcastStreamUid),0x0140);
}
//restarting buffering, if needed!
if(!iReadingActive && ldTotal>0)
{
//reading is stopped and we need to restart it!
ReadRequest();
};
LOG2("FillBufferL OK (pos=%d, len=%d)",iPosBuffer2Decode, iLenBuffer2Decode);
}
1。字符串定义
界面上显示的字符串、HTTP GET和Response消息以及元数据都在该类中定义。
2。常量定义:
MP3,AAC的采样频率、码率以及头大小的定义。
3。声音处理:
增大、减少音量,返回当前音量。
请求方式是标准的HTTP请求,就像浏览器请求Web服务器一样。
回复消息中包含了配置plugin播放流的参数,如bitrate、content-type等信息。
接着就是音频数据流。