一
jellybean 的多媒体跟以前的版本,通过对比没啥变化,最大的变化是google终于舍得给multimedia建个独立的git了(framework/av),等你好久了!也体现了media 在整个android系统中的重要性!framework/av下都是些C/C++代码(libmedia,libmediaplayerservice,libstagefright),jni和 java api 还是保留在原来的位置,改革还不够彻底,但还是迈出了这一步,以后维护能更好的进行了!但是对于从ics往jellybean升级就得费点劲了,打patch不好打了!还有一个大的变化时增加了可以直接调用codec的API,不需要通过stagefrigh引擎去调用,就像我们直接调用FFMPEG的codec一样,方便简单,不用绕那么多弯。具体的往后我们再具体了解吧,毕竟刚有的!
android multimedia Framework 整体架构是一个很庞大的系统,我们该如何划分和去研究呢?大的分法就是video和audio。往细的分呢?也是我接下来要按顺序讲的:
4:audio flinger
可能会以后写的时候有变化吧,但总体方向就如此吧!本周先写写总体框架和video playback。先洗澡鸟。。。。
二
我们学习一种新事物必然首先都要对该事物要有个大体的了解,熟悉它的整体架构,然后进行划分归类,接下来才是各个击破,逐步学习乃至掌握。对于要学习androidMultimedia的人来说也是如此,先来个总括吧 !我打算分三部分来讲解,请听我娓娓道来....
一:多媒体简介
为啥要讲多媒体的概念呢?可能很多人都对这个名称解释不怎么了解,所以在这普及普及。
媒体(Media)就是人与人之间实现信息交流的中介,简单地说,就是信息的载体,也称为媒介。多媒体是计算机和视频技术的结合,实际上它是两个媒体;声音和图像,或者用现在的术语:音响和电视。多媒体本身有两个方面,和所有现代技术一样它是由硬件和软件,或机器和思想混合组成。可以将多媒体技术和功能在概念上区分为控制系统和信息。多媒体之所以能够实现是依靠数字技术。多媒体代表数字控制和数字媒体的汇合,电脑是数字控制系统,而数字媒体是当今音频和视频最先进的存储和传播形式。事实上有人就简单地认为多媒体是电脑和电视的结合。电脑的能力达到实时处理电视和声音数据流的水平,这时多媒体就诞生了。
二:android多媒体框架演变历史
android 的多媒体框架从android诞生以来,发生了天翻地覆的变化,包括引擎的更改,单独处理流媒体的播放器nuplayer的加入,到最新jellybean(android4.1)nuplayer逐步加入stagefrightplayer的功能,可能以后stagefight引擎会被nuplayer取代,那都是后话了。但是openomx(即引擎连接codec的纽带)一直都得到了保持。
在Froyo2.2 以前,multimediaframework 的引擎是一直都是opencore,但为啥用stagefright替代呢,由于我没有开发过opencore,不便下结论,但从网上一些言论来看,估计是opencore太过庞大,不太好维护,具体真正原因就得问google了,如果你知道具体原因,可以给我留言,在此多谢了!
Gingerbread android2.3,加入了真正的支持流媒体的播发器nuplayer,如果你下有源码,可以用gitk\nuplayer,从gitk可以看到如下提交:Initialsupport for a true streaming player for mpeg2 transport streams. 2010.12。
android3.0 到android4.0 ,总体框架没有多大变化。
android4.1 (jellybean) 最大的变化是给c/c++部分的多媒体框架单独设立了一个framework/av的目录,给它开辟了一个git库,即从framework/base下的git库分离了出来,总算给多媒体找了个港湾。
三:jellybean多媒体架构
multimedia framework 架构 由三大部分构成:供上层程序调用的javaAPI,连接java和C/C++的jni部分,多媒体引擎(stagefright)和codec接口(openmaxinterface)。前面两部代码在framework/base/media下,后一部分在framework/av文件夹下。如果你修改的是java API接口或加LOG后编译可以用如下命令:makeframework ,JNI部分 makemedia_jni,第三部分有三个libs组成:libmedia ,libmediaplayerservice,libstagefright,命令如下make media ,makestagefright ,make mediaplayerservice. 生成各自的.so文件,用adb push 到system/下就可以调试了。记得重启!讲了好多废话,还是没有看到总体架构,罪过,好吧,上图,更直观。
从上两图,我们可以发现上层APK要播放视频,首先得获得一个player,而这个player的类型根据你媒体文件的类型来决定的,分配的任务由mediaplayerservice来完成,除了获得player外,最主要的是到底选用哪种编码器进行编解码,这个过程由awesomeplayer和omxcodec来完成,至于声音和图像就交由audioflinger和surfaceflinger来完成了。具体的调用实现,下一篇videoplayerback将会慢慢讨论和学习。
三
上一篇我们讲了多媒体的总体框架,本章我们先来讨论媒体文件的本地播放,也是手机的基本功能。现在市面上的手机配置越来越高,支持高清视频(1920x1080P)已不在话下。那现在android主流播放器都支持哪些媒体格式呢?一般来说mp3,mp4,m4a,m4v,amr等大众格式都是支持的,具体支持成什么样这得看手机厂商和芯片厂商了。具体格式大全可以看framework/base/media/java/android/media/MediaFile.java。
我们下面进入正题研究多媒体文件的本地播放(videoplayback),具体用到的工具有sourceinsight,astah(免费的画流程图工具),android 4.1代码。代码如何获取可以到googlesource下下载:http://source.android.com/source/downloading.html。
一般上层应用要本地播放播放一个媒体文件,需要经过如下过程:
MediaPlayer mMediaPlayer = new MediaPlayer( );
mMediaPlayer.setDataSource(mContext,mUri);-
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.prepareAsync();
mMediaPlayer.start();
首先我们先来分析setDataSource方法,这个方法有两个功能:一个是根据文件类型获得相应的player;一个是创建相应文件类型的mediaExtractor,解析媒体文件,记录metadata的主要信息。
代码如下:
framework/av/media/libmedia/MediaPlayer.cpp
status_tMediaPlayer::setDataSource(
const char *url, const KeyedVector<String8, String8> *headers)
{
ALOGV("setDataSource(%s)", url);
status_t err = BAD_VALUE;
if (url != NULL) {
const sp<IMediaPlayerService>& service(getMediaPlayerService());
if (service != 0) {
sp<IMediaPlayer> player(service->create(getpid(),this, mAudioSessionId));
if ((NO_ERROR != doSetRetransmitEndpoint(player)) ||
(NO_ERROR != player->setDataSource(url, headers))) {
player.clear();
}
err = attachNewPlayer(player);
}
}
return err;
}
我们先来看下setDataSource方法中如何获取player。大体的流程图如下图:
我们知道MediaplayerService是负责外部请求,针对每个APPplayer ,mediaplayerservice都会开辟一个client端来专门处理。(这也就是下面client类的重要意义所在)
client 定义如下:
framework/av/media/libmediaplayerservice/MediaPlayerService.h
classClient : public BnMediaPlayer {...
private:
friend class MediaPlayerService;
Client( const sp<MediaPlayerService>& service,
pid_t pid,
int32_t connId,
const sp<IMediaPlayerClient>& client,
int audioSessionId,
uid_t uid);
}
}
(下面这两段话非常重要,其概括了mediaplayer的播放原理)
(virtualsp<IMediaPlayer> create(pid_t pid, constsp<IMediaPlayerClient>& client, int audioSessionId = 0) = 0;)
上面这段code说明了必须先创建IMediaPlayerClient的client端对象,而后才能创建IMediaPlayer的对象即player
sp<IMediaPlayer>player(service->create(getpid(), this, mAudioSessionId));
从代码看就是一个BnMediaplayer的子类(即local binder)。既然有了BnMediaplayer,客户端也应该有相应的BpMediaplayer。获取这个BpMediaplayer要分两步骤走:第一,获取BpMediaplayerService;第二就是在setDataSource方法中的:
sp<IMediaPlayer>player(service->create(getpid(), this, mAudioSessionId)); 这个函数会返回一个BpMediaplayer。(client端)
获取BpMediaplayerService,首先要去ServiceManager获取相应的服务Mediaplayer,里面的流程是这样检查是否存在要找的service,没有就创建,有就返回BpXX。
有了BpMediaplayerService,我们就可以跟MediaplayerService通信了,自然就可以创建对应的client端来服务对应的BpMediaplayer(客户端):
framework/av/media/libmediaplayerservice/MediaPlayerService.cpp
sp<IMediaPlayer>MediaPlayerService::create(pid_t pid, const sp<IMediaPlayerClient>&client,
int audioSessionId)
{
int32_t connId = android_atomic_inc(&mNextConnId);
sp<Client> c = new Client(
this, pid, connId, client, audioSessionId,
IPCThreadState::self()->getCallingUid());
ALOGV("Create new client(%d) from pid %d, uid %d, ", connId, pid,
IPCThreadState::self()->getCallingUid());
wp<Client> w = c;
{
Mutex::Autolock lock(mLock);
mClients.add(w);
}
return c;
}
到此我们的sp<IMediaPlayer>player(service->create(getpid(), this, mAudioSessionId));
变成了:sp<IMediaPlayer>player(Client); 那它是如何变成我们需要的BpMediaplayer呢,请看下面的定义原型,INTERFACE就是mediaplayer,大伙把宏取代下就知道了:
frameworks/av/media/include/IMediapalyer.h
classIMediaPlayer: public IInterface
{
public:
DECLARE_META_INTERFACE(MediaPlayer);
DECLARE_META_INTERFACE宏定义如下:
#defineDECLARE_META_INTERFACE(INTERFACE) \
static const android::String16descriptor; \
static android::sp<I##INTERFACE>asInterface( \
const android::sp<android::IBinder>&obj); \
virtual const android::String16& getInterfaceDescriptor()const; \
I##INTERFACE(); \
virtual ~I##INTERFACE();
有了DECLAREIMediaplayer.cpp 必有IMPLEMENT
#defineIMPLEMENT_META_INTERFACE(INTERFACE, NAME) \
const android::String16I##INTERFACE::descriptor(NAME); \
constandroid::String16& \
I##INTERFACE::getInterfaceDescriptor() const{ \
return I##INTERFACE::descriptor; \
} \
android::sp<I##INTERFACE>I##INTERFACE::asInterface( \
constandroid::sp<android::IBinder>& obj) \
{ \
android::sp<I##INTERFACE>intr; \
if (obj != NULL){ \
intr =static_cast<I##INTERFACE*>( \
obj->queryLocalInterface( \
I##INTERFACE::descriptor).get()); \
if (intr == NULL){ \
intr = newBp##INTERFACE(obj); \
} \
} \
returnintr; \
} \
I##INTERFACE::I##INTERFACE() {} \
I##INTERFACE::~I##INTERFACE() { } \
通过如上方法 ,我们获得了BpMediaplayer(remoteBinder),我们就可以通过BpMediaplayer跟BnMediaplayer通信了。两者的交互是IBinder。
BpMediaplayer具体实现在哪呢?
frameworks/av/media/libmedia/IMediaplayer.cpp:
classBpMediaPlayer: public BpInterface<IMediaPlayer>
{
public:
BpMediaPlayer(const sp<IBinder>& impl)
: BpInterface<IMediaPlayer>(impl)
{
}
// disconnect from media player service
void disconnect()
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayer::getInterfaceDescriptor());
remote()->transact(DISCONNECT, data, &reply);
}
status_t setDataSource(const char* url,
const KeyedVector<String8, String8>* headers)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayer::getInterfaceDescriptor());
data.writeCString(url);
if (headers == NULL) {
data.writeInt32(0);
} else {
// serialize the headers
data.writeInt32(headers->size());
for (size_t i = 0; i < headers->size(); ++i) {
data.writeString8(headers->keyAt(i));
data.writeString8(headers->valueAt(i));
}
}
remote()->transact(SET_DATA_SOURCE_URL, data, &reply);
return reply.readInt32();
}
remote就是一个IBinder, IBinder 通过transact 方法中的
IPCThreadState::self()->transact(
mHandle, code, data, reply, flags);
通知相应BnMediaplayer(service端)进行相应的处理。里面的如何打开binder,如何传到MediaplayerService::client就不具体说了,有兴趣可以跟下去看看。
以上我们运用到了Binder的通信机制,如果大家对此不太了解可以看:
Android系统进程间通信(IPC)机制Binder中的Server和Client获得Service Manager接口之路 .
获得了BpMediaplayer,我们就可以通过调用client端的setDataSource创建 player了:
status_tMediaPlayerService::Client::setDataSource(
const char *url, const KeyedVector<String8, String8> *headers)
{….
如果是url 以content://开头要转换为filedescriptor
if (strncmp(url, "content://", 10) == 0) {…
int fd = android::openContentProviderFile(url16);
……….
setDataSource(fd, 0, 0x7fffffffffLL); // thissets mStatus
close(fd);
return mStatus;
} else {
player_type playerType = getPlayerType(url);….createplayer前要判断是哪种类型
LOGV("player type = %d", playerType);
// create the right type of player
sp<MediaPlayerBase> p = createPlayer(playerType);
mStatus = p->setDataSource(url,headers);
…
returnmStatus;
}
}
player_typegetPlayerType(const char* url) ……………. 根据url的后缀名判断属于哪种playerType,默认是stagefright,我们现在研究的是本地播放,自然是stagefrightPlayer了
{
if (TestPlayerStub::canBeUsed(url)) {
return TEST_PLAYER;
}
// use MidiFile for MIDI extensions
int lenURL = strlen(url);
for (int i = 0; i < NELEM(FILE_EXTS); ++i) {
int len = strlen(FILE_EXTS[i].extension);
int start = lenURL - len;
if (start > 0) {
if (!strncasecmp(url + start, FILE_EXTS[i].extension, len)) {
returnFILE_EXTS[i].playertype;
}
}
}
……………….
return getDefaultPlayerType();
}
自此我们获得了想要的player了。这里最主要的知识点就是Binder的通信了,Binder的流程我们可以用下图来解释,大家可以好好琢磨:
player已经取得,接下来就是setDataSource的第二步:获取相应的MediaExtractor并储存相应的数据。
关于这一步,我也画了个时序图:
紧接刚才我们获得player的步骤,我们实例话一个stagefrightPlayer的同时也实例话了一个AwesomePlayer,其实真正干实事的AwesomePlayer,stagefrightPlayer只是个对外的接口,
代码如下:framework/av/media/libmediaplayerservice/StagefrightPlayer.cpp
staticsp<MediaPlayerBase> createPlayer(player_type playerType, void* cookie,
notify_callback_f notifyFunc) {
…..
case STAGEFRIGHT_PLAYER:
ALOGV(" create StagefrightPlayer");
p = new StagefrightPlayer;
break;
…….
}
创建stagefrightplayer实例也new了个AwesomePlayer(mPlayer)
StagefrightPlayer::StagefrightPlayer()
: mPlayer(new AwesomePlayer) {
LOGV("StagefrightPlayer");
mPlayer->setListener(this);
}
既然Awesomeplayer是干实事的,我们直接进去看看吧:
frameworks/av/media/libstagefright/AwesomePlayer.cpp
status_tAwesomePlayer::setDataSource_l(
const sp<DataSource> &dataSource) {
sp<MediaExtractor> extractor = MediaExtractor::Create(dataSource);…….创建对应的extractor
…..
return setDataSource_l(extractor);
}
status_tAwesomePlayer::setDataSource_l(const sp<MediaExtractor> &extractor) {
…
for(size_t i = 0; i < extractor->countTracks(); ++i) {
sp<MetaData> meta = extractor->getTrackMetaData(i);.......获取相应track的元数据
int32_t bitrate;
if (!meta->findInt32(kKeyBitRate, &bitrate)) {
const char *mime;
CHECK(meta->findCString(kKeyMIMEType,&mime));
ALOGV("track of type '%s' does not publish bitrate", mime);
totalBitRate = -1;
break;
}
totalBitRate += bitrate;
}
.........
if (!haveVideo && !strncasecmp(mime, "video/", 6)) {
setVideoSource(extractor->getTrack(i)); ………>mVideoTrack
haveVideo = true;
} else if (!haveAudio && !strncasecmp(mime, "audio/", 6)) {
setAudioSource(extractor->getTrack(i));……….>mAudioTrack
haveAudio = true;
return OK;
}
关于MediaExtractor里面涉及到媒体文件格式的很多内容,比如track的构成,有几种track等等,我们将来在videoRecorder中再详细讲解。这里只有知道提取相关信息就行了。
此方法调用完成意味着player进入了MEDIA_PLAYER_INITIALIZED状态。Player的状态有如下几种:
MEDIA_PLAYER_STATE_ERROR
MEDIA_PLAYER_IDLE
MEDIA_PLAYER_INITIALIZED
MEDIA_PLAYER_PREPARING
MEDIA_PLAYER_PREPARED
MEDIA_PLAYER_STARTED
MEDIA_PLAYER_PAUSED
MEDIA_PLAYER_STOPPED
MEDIA_PLAYER_PLAYBACK_COMPLETE
setDataSource我们已经讲完了,讲流程我们的目的是熟悉它的架构,希望大家好好熟悉熟悉,在项目需要的时候根据我们自己的媒体格式,依葫芦画瓢进行改造,比如说支持多track,切换track,以达到KTV的功能等等。。。
下一篇我们将讲解prepare的过程,这个工程主要是匹配codec,初始化codec等。
三.1
今天同事突然问我了一些多媒体基本概念的问题,感觉好多都是不太清楚,既然不清楚那就补一补吧。
码率:也叫比特率,表示经过压缩编码后的视音频数据每秒需要用多少个比特来表示,即把每秒显示的图像进行压缩后的数据量,一般采用的单位是kbps即千位每秒。一般来说码率越大,处理出来的文件就越接近原始文件,但文件体积与码率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真,围绕这个核心衍生出来的CBR(固定码率)与VBR(动态码率)。
固定码率CBR(ConstantBitrate):指文件从头高位都是一种码率,这是以固定文件大小为前提的压缩方式。
动态码率VBR(Variable Bitrate):指没有固定的码率,压缩时根据视音频数据即时确定使用什么码率,这是以质量为前提兼顾文件大小的压缩方式。
【文件大小】(Byte字节)=【码率】(kbps)/8X【时间】(秒)
1 byte (B) = 8 bits (b),我们计算机上文件的容量K/M,都是指B;
1Kilobyte(K/KB)=2^10 bytes=1,024 bytes 千字节 ;
1Megabyte(M/MB)=2^20 bytes=1,048,576 bytes 兆字节;
所以如果用的bits/s的码流计算容量记得要乘8。
视频分辨率:我们常说的视频多少乘多少,严格来说不是分辨率,而是视频的高/宽像素值。常见的屏幕比例其实只有三种:4:3、16:9和 16:10。
采样率:(也称为采样速度或者采样频率)定义了每秒从连续信号中提取并组成离散信号的采样个数,单位用赫兹(Hz)来表示。采样频率的倒数是采样周期(也称为采样时间),它表示采样之间的时间间隔。
帧率(Framerate):是用于测量显示帧数的量度。所谓的测量单位为每秒显示帧数(Frames per Second,简称:FPS)或“赫兹”(Hz)。高的帧率可以得到更流畅、更逼真的动画。一般来说30fps就是可以接受的,但是将性能提升至60fps则可以明显提升交互感和逼真感,但是一般来说超过75fps一般就不容易察觉到有明显的流畅度提升了。
刷新频率:即屏幕刷新的速度。刷新频率越低,图像闪烁和抖动的就越厉害,眼睛疲劳得就越快。
采用70Hz以上的刷新频率时才能基本消除闪烁,显示器最好稳定工作在允许的最高频率下,一般是85Hz。
先记录这些吧,以后再慢慢补上。。。。
四
上一篇我们讲了mediaplayer播放的第一步骤setdataSource,下面我们来讲解preparesync的流程,在prepare前我们还有setDisplay这一步,即获取surfacetexture来进行画面的展示
setVideoSurface(JNIEnv*env, jobject thiz, jobject jsurface, jboolean mediaPlayerMustBeAlive)
{
sp<MediaPlayer> mp = getMediaPlayer(env, thiz);
………
sp<ISurfaceTexture> new_st;
if (jsurface) {
sp<Surface> surface(Surface_getSurface(env, jsurface));
if (surface != NULL) {
new_st= surface->getSurfaceTexture();
---通过surface获取surfaceTexture
new_st->incStrong(thiz);
……….
}………….
mp->setVideoSurfaceTexture(new_st);
}
为什么用surfaceTexture不用surface来展示呢?ICS之前都用的是surfaceview来展示video或者openGL的内容,surfacaview render在surface上,textureviewrender在surfaceTexture,textureview和surfaceview 这两者有什么区别呢?surfaceview跟应用的视窗不是同一个视窗,它自己new了一个window来展示openGL或者video的内容,这样做有一个好处就是不用重绘应用的视窗,本身就可以不停的更新,但这也带来一些局限性,surfaceview不是依附在应用视窗中,也就不能移动、缩放、旋转,应用ListView或者 ScrollView就比较费劲。Textureview就很好的解决了这些问题。它拥有surfaceview的一切特性外,它也拥有view的一切行为,可以当个view使用。
获取完surfaceTexture,我们就可以prepare/prepareAsync了,先给大伙看个大体时序图吧:
JNI的部分我们跳过,直接进入libmedia下的mediaplayer.cpp的 prepareAsync_l方法,prepare是个同步的过程,所以要加锁,prepareAsync_l后缀加_l就是表面是同步的过程。
status_tMediaPlayer::prepareAsync_l()
{
if ( (mPlayer != 0) && ( mCurrentState & ( MEDIA_PLAYER_INITIALIZED| MEDIA_PLAYER_STOPPED) ) ) {
mPlayer->setAudioStreamType(mStreamType);
mCurrentState = MEDIA_PLAYER_PREPARING;
return mPlayer->prepareAsync();
}
ALOGE("prepareAsync called in state %d", mCurrentState);
return INVALID_OPERATION;
}
在上面的代码中,我们看到有个mPlayer,看过前一章的朋友都会记得,就是我们从Mediaplayerservice获得的BpMediaplayer.通过BpMediaplayer我们就可以长驱直入,直捣Awesomeplayer这条干实事的黄龙,前方的mediaplayerservice:client和stagefrightplayer都是些通风报信的料,不值得我们去深入研究,无非是些接口而已。进入了prepareAsync_l方法,我们的播放器所处的状态就是MEDIA_PLAYER_PREPARING了。好了,我们就来看看Awesomeplayer到底做了啥吧.
代码定位于:frameworks/av/media/libstagefright/Awesomeplayer.cpp
先看下prepareAsync_l吧:
status_tAwesomePlayer::prepareAsync_l() {
if (mFlags & PREPARING) {
return UNKNOWN_ERROR; // async prepare already pending
}
if (!mQueueStarted) {
mQueue.start();
mQueueStarted= true;
}
modifyFlags(PREPARING, SET);
mAsyncPrepareEvent= new AwesomeEvent(
this,&AwesomePlayer::onPrepareAsyncEvent);
mQueue.postEvent(mAsyncPrepareEvent);
return OK;
}
这里我们涉及到了TimeEventQueue,即时间事件队列模型,Awesomeplayer里面类似Handler的东西,它的实现方式是把事件响应时间和事件本身封装成一个queueItem,通过postEvent插入队列,时间到了就会根据事件id进行相应的处理。
首先我们来看下TimeEventQueue的start(mQueue.start();)方法都干了什么:
frameworks/av/media/libstagefright/TimedEventQueue.cpp
voidTimedEventQueue::start() {
if (mRunning) {
return;
}
……..
//在c++中线程的应用方式:
pthread_create(&mThread,&attr, ThreadWrapper, this);
………
}
目的很明显就是在主线程创建一个子线程,可能很多没有写过C/C++的人对ptread_create这个创建线程的方法有点陌生,我们就来分析下:
intpthread_create(pthread_t *thread, pthread_addr_t *arr,
void*(*start_routine)(void *), void *arg);
thread :用于返回创建的线程的ID
arr : 用于指定的被创建的线程的属性
start_routine : 这是一个函数指针,指向线程被创建后要调用的函数
arg : 用于给线程传递参数
分析完了,我们就看下创建线程后调用的函数ThreadWrapper吧:
//static
void*TimedEventQueue::ThreadWrapper(void *me) {
……
static_cast<TimedEventQueue*>(me)->threadEntry();
return NULL;
}
跟踪到threadEntry:
frameworks/av/media/libstagefright/TimedEventQueue.cpp
voidTimedEventQueue::threadEntry() {
prctl(PR_SET_NAME, (unsigned long)"TimedEventQueue", 0, 0, 0);
for (;;) {
int64_t now_us = 0;
sp<Event> event;
{
Mutex::Autolock autoLock(mLock);
if (mStopped) {
break;
}
while(mQueue.empty()) {
mQueueNotEmptyCondition.wait(mLock);
}
event_id eventID = 0;
for (;;) {
if (mQueue.empty()) {
// The only event in the queue could have been cancelled
// while we were waiting for its scheduled time.
break;
}
List<QueueItem>::iterator it = mQueue.begin();
eventID = (*it).event->eventID();
……………………………
staticint64_t kMaxTimeoutUs = 10000000ll; // 10 secs
……………..
status_terr = mQueueHeadChangedCondition.waitRelative(
mLock,delay_us * 1000ll);
if (!timeoutCapped && err == -ETIMEDOUT) {
// We finally hit the time this event issupposed to
// trigger.
now_us = getRealTimeUs();
break;
}
}
……………………….
event = removeEventFromQueue_l(eventID);
}
if (event != NULL) {
// Fire event with the lock NOT held.
event->fire(this, now_us);
}
}
}
从代码我们可以了解到,主要目的是检查queue是否为空,刚开始肯定是为空了,等待队列不为空时的条件成立,即有queueIten进入进入队列中。这个事件应该就是
mQueue.postEvent(mAsyncPrepareEvent);
在讲postEvent前,我们先来看看mAsyncPrepareEvent这个封装成AwesomeEvent的Event。
structAwesomeEvent : public TimedEventQueue::Event {
AwesomeEvent(
AwesomePlayer *player,
void(AwesomePlayer::*method)())
: mPlayer(player),
mMethod(method) {
}
从这个结构体我们可以知道当这个event被触发时将会执行Awesomeplayer的某个方法,我们看下mAsyncPrepareEvent:
mAsyncPrepareEvent= new AwesomeEvent(
this,&AwesomePlayer::onPrepareAsyncEvent);
mAsyncPrepareEvent被触发时也就触发了onPrepareAsyncEvent方法。
好了,回到我们的postEvent事件,我们开始说的TimeEventQueue,即时间事件队列模型,刚刚我们说了Event, 但是没有看到delay time啊?会不会在postEvent中加入呢?跟下去看看:
TimedEventQueue::event_idTimedEventQueue::postEvent(const sp<Event> &event) {
// Reserve an earlier timeslot an INT64_MIN to be able to post
// the StopEvent to the absolute head of the queue.
return postTimedEvent(event,INT64_MIN + 1);
}
终于看到delay时间了INT64_MIN + 1。重点在postTimedEvent,它把post过来的event和时间封装成queueItem加入队列中,并通知Queue为空的条件不成立,线程解锁,允许thread继续进行,经过delay time后pullevent_id所对应的event。
frameworks/av/media/libstagefright/TimedEventQueue.cpp
TimedEventQueue::event_idTimedEventQueue::postTimedEvent(
const sp<Event> &event, int64_t realtime_us) {
Mutex::Autolock autoLock(mLock);
event->setEventID(mNextEventID++);
………………….
QueueItemitem;
item.event= event;
item.realtime_us= realtime_us;
if (it == mQueue.begin()) {
mQueueHeadChangedCondition.signal();
}
mQueue.insert(it,item);
mQueueNotEmptyCondition.signal();
return event->eventID();
}
到此,我们的TimeEventQueue,即时间事件队列模型讲完了。实现机制跟handle的C/C++部分类似。
在我们setdataSource实例化Awesomeplayer的时候,我们还顺带创建了如下几个event
sp<TimedEventQueue::Event> mVideoEvent;
sp<TimedEventQueue::Event> mStreamDoneEvent;
sp<TimedEventQueue::Event> mBufferingEvent;
sp<TimedEventQueue::Event> mCheckAudioStatusEvent;
sp<TimedEventQueue::Event> mVideoLagEvent;
具体都是实现了什么功能呢?我们在具体调用的时候再深入讲解。
接下来我们就来讲讲onPrepareAsyncEvent方法了。
frameworks/av/media/libstagefight/AwesomePlayer.cpp
voidAwesomePlayer::onPrepareAsyncEvent() {
Mutex::Autolock autoLock(mLock);
…………………………
if (mUri.size() > 0) {
status_terr = finishSetDataSource_l();----这个不会走了,如果是本地文件的话
…………………………
if (mVideoTrack != NULL && mVideoSource == NULL) {
status_terr = initVideoDecoder();-----------如果有videotrack初始化video的解码器
…………………………
if (mAudioTrack != NULL && mAudioSource == NULL) {
status_terr = initAudioDecoder();---------------如果有audiotrack初始化audio解码器
……………………..
modifyFlags(PREPARING_CONNECTED, SET);
if (isStreamingHTTP()) {
postBufferingEvent_l(); ------一般不会走了
} else {
finishAsyncPrepare_l();----------对外宣布prepare完成,并从timeeventqueue中移除该queueitem,mAsyncPrepareEvent=null
}
}
我们终于知道prepare主要目的了,根据类型找到解码器并初始化对应的解码器。那我们首先就来看看有videotrack的媒体文件是如何找到并初始化解码器吧。
先看图吧,了解大概步骤:
看完图就开讲了:
iniVideoDecoder目的是初始化解码器,取得与解码器的联系,解码数据输出格式等等。
frameworks/av/media/libstagefright/Awesomeplayer.cpp
status_tAwesomePlayer::initVideoDecoder(uint32_t flags) {
…………
mVideoSource= OMXCodec::Create(
mClient.interface(), mVideoTrack->getFormat(),
false, // createEncoder
mVideoTrack,
NULL, flags, USE_SURFACE_ALLOC ? mNativeWindow : NULL);
…………..
status_t err = mVideoSource->start();
}
我们先来看create函数到底干了啥吧:
frameworks/av/media/libstagefright/OMXCodec.cpp
sp<MediaSource>OMXCodec::Create(
const sp<IOMX> &omx,
const sp<MetaData> &meta, bool createEncoder,
const sp<MediaSource> &source,
const char *matchComponentName,
uint32_t flags,
const sp<ANativeWindow> &nativeWindow) {
…………..
boolsuccess = meta->findCString(kKeyMIMEType, &mime);
……………
(1) findMatchingCodecs(
mime, createEncoder, matchComponentName, flags,
&matchingCodecs, &matchingCodecQuirks);
……….
(2) sp<OMXCodecObserver> observer = newOMXCodecObserver;
(3) status_terr = omx->allocateNode(componentName, observer, &node);
……….
(4) sp<OMXCodec>codec = new OMXCodec(
omx, node, quirks, flags,
createEncoder, mime, componentName,
source, nativeWindow);
(5) observer->setCodec(codec);
(6)err = codec->configureCodec(meta);
…………
}
首先看下findMatchingCodecs,原来是根据mimetype找到匹配的解码组件,android4.1的寻找组件有了很大的变化,以前都是把codecinfo都写在代码上了,现在把他们都放到media_codec.xml文件中,full build 后会保存在“/etc/media_codecs.xml”,这个xml由各个芯片厂商来提供,这样以后添加起来就很方便,不用改代码了。一般是原生态的代码都是软解码。解码器的匹配方式是排名制,因为一般厂商的配置文件都有很多的同类型的编码器,谁排前面就用谁的。
frameworks/av/media/libstagefright/OMXCodec.cpp
voidOMXCodec::findMatchingCodecs(
const char *mime,
bool createEncoder, const char *matchComponentName,
uint32_t flags,
Vector<String8> *matchingCodecs,
Vector<uint32_t> *matchingCodecQuirks) {
…………
const MediaCodecList *list = MediaCodecList::getInstance();
………
for(;;) {
ssize_t matchIndex =
list->findCodecByType(mime,createEncoder, index);
………………..
matchingCodecs->push(String8(componentName));
…………….
}
frameworks/av/media/libstagefright/MediaCodecList.cpp
onstMediaCodecList *MediaCodecList::getInstance() {
..
if (sCodecList == NULL) {
sCodecList= new MediaCodecList;
}
return sCodecList->initCheck() == OK ? sCodecList : NULL;
}
MediaCodecList::MediaCodecList()
: mInitCheck(NO_INIT) {
FILE *file = fopen("/etc/media_codecs.xml", "r");
if (file == NULL) {
ALOGW("unable to open media codecs configuration xml file.");
return;
}
parseXMLFile(file);
}
有了匹配的componentName,我们就可以创建ComponentInstance,这由allocateNode方法来实现。
frameworks/av/media/libstagefright/omx/OMX.cpp
status_tOMX::allocateNode(
const char *name, const sp<IOMXObserver> &observer, node_id *node) {
……………………
OMXNodeInstance *instance = new OMXNodeInstance(this, observer);
OMX_COMPONENTTYPE *handle;
OMX_ERRORTYPE err = mMaster->makeComponentInstance(
name, &OMXNodeInstance::kCallbacks,
instance, &handle);
……………………………
*node = makeNodeID(instance);
mDispatchers.add(*node,new CallbackDispatcher(instance));
instance->setHandle(*node, handle);
mLiveNodes.add(observer->asBinder(), instance);
observer->asBinder()->linkToDeath(this);
return OK;
}
在allocateNode,我们要用到mMaster来创建component,但是这个mMaster什么时候初始化了呢?我们看下OMX的构造函数:
OMX::OMX()
: mMaster(new OMXMaster),-----------原来在这呢!
mNodeCounter(0) {
}
但是我们前面没有讲到OMX什么时候构造的啊?我们只能往回找了,原来我们在初始化Awesomeplayer的时候忽略掉了,罪过啊:
AwesomePlayer::AwesomePlayer()
: mQueueStarted(false),
mUIDValid(false),
mTimeSource(NULL),
mVideoRendererIsPreview(false),
mAudioPlayer(NULL),
mDisplayWidth(0),
mDisplayHeight(0),
mVideoScalingMode(NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW),
mFlags(0),
mExtractorFlags(0),
mVideoBuffer(NULL),
mDecryptHandle(NULL),
mLastVideoTimeUs(-1),
mTextDriver(NULL) {
CHECK_EQ(mClient.connect(),(status_t)OK) 这个就是创建的地方
mClient是OMXClient,
status_tOMXClient::connect() {
sp<IServiceManager> sm = defaultServiceManager();
sp<IBinder> binder =sm->getService(String16("media.player"));
sp<IMediaPlayerService> service =interface_cast<IMediaPlayerService>(binder);---很熟悉吧,获得BpMediaplayerservice
CHECK(service.get() != NULL);
mOMX =service->getOMX();
CHECK(mOMX.get() != NULL);
if (!mOMX->livesLocally(NULL /* node */, getpid())) {
ALOGI("Using client-side OMX mux.");
mOMX = new MuxOMX(mOMX);
}
return OK;
}
好了,我们直接进入mediaplayerservice.cpp看个究竟吧:
sp<IOMX>MediaPlayerService::getOMX() {
Mutex::Autolock autoLock(mLock);
if (mOMX.get() == NULL) {
mOMX = newOMX;
}
return mOMX;
}
终于看到了OMX的创建了,哎以后得注意看代码才行!!!
我们搞了那么多探究OMXMaster由来有什么用呢?
OMXMaster::OMXMaster()
: mVendorLibHandle(NULL) {
addVendorPlugin();
addPlugin(new SoftOMXPlugin);
}
voidOMXMaster::addVendorPlugin() {
addPlugin("libstagefrighthw.so");
}
原来是用来加载各个厂商的解码器(libstagefrighthw.so),还有就是把google本身的软解码器(SoftOMXPlugin)也加载了进来。那么这个libstagefrighthw.so在哪?我找了半天终于找到了,每个芯片厂商对应自己的libstagefrighthw
hardware/XX/media/libstagefrighthw/xxOMXPlugin
如何实例化自己解码器的component?我们以高通为例:
voidOMXMaster::addPlugin(const char *libname) {
mVendorLibHandle = dlopen(libname, RTLD_NOW);
…………………………….
if (createOMXPlugin) {
addPlugin((*createOMXPlugin)());-----创建OMXPlugin,并添加进我们的列表里
}
}
hardware/qcom/media/libstagefrighthw/QComOMXPlugin.cpp
OMXPluginBase*createOMXPlugin() {
return new QComOMXPlugin;
}
QComOMXPlugin::QComOMXPlugin()
: mLibHandle(dlopen("libOmxCore.so",RTLD_NOW)),----载入自己的omxAPI
mInit(NULL),
mDeinit(NULL),
mComponentNameEnum(NULL),
mGetHandle(NULL),
mFreeHandle(NULL),
mGetRolesOfComponentHandle(NULL) {
if (mLibHandle != NULL) {
mInit = (InitFunc)dlsym(mLibHandle, "OMX_Init");
mDeinit = (DeinitFunc)dlsym(mLibHandle, "OMX_DeInit");
mComponentNameEnum =
(ComponentNameEnumFunc)dlsym(mLibHandle, "OMX_ComponentNameEnum");
mGetHandle = (GetHandleFunc)dlsym(mLibHandle, "OMX_GetHandle");
mFreeHandle = (FreeHandleFunc)dlsym(mLibHandle, "OMX_FreeHandle");
mGetRolesOfComponentHandle =
(GetRolesOfComponentFunc)dlsym(
mLibHandle, "OMX_GetRolesOfComponent");
(*mInit)();
}
}
以上我们就可以用高通的解码器了。我们在创建component的时候就可以创建高通相应的component实例了:
OMX_ERRORTYPEOMXMaster::makeComponentInstance(
const char *name,
const OMX_CALLBACKTYPE *callbacks,
OMX_PTR appData,
OMX_COMPONENTTYPE **component) {
Mutex::Autolock autoLock(mLock);
*component = NULL;
ssize_tindex = mPluginByComponentName.indexOfKey(String8(name)); ----根据我们在media_codec.xml的解码器名字,在插件列表找到其索引
OMXPluginBase*plugin = mPluginByComponentName.valueAt(index); --根据索引找到XXOMXPlugin
OMX_ERRORTYPEerr =
plugin->makeComponentInstance(name,callbacks, appData, component);
-----创建组件
mPluginByInstance.add(*component, plugin);
return err;
}
hardware/qcom/media/libstagefrighthw/QComOMXPlugin.cpp
OMX_ERRORTYPEQComOMXPlugin::makeComponentInstance(
const char *name,
const OMX_CALLBACKTYPE *callbacks,
OMX_PTR appData,
OMX_COMPONENTTYPE **component) {
if (mLibHandle == NULL) {
return OMX_ErrorUndefined;
}
String8 tmp;
RemovePrefix(name, &tmp);
name = tmp.string();
return(*mGetHandle)(
reinterpret_cast<OMX_HANDLETYPE*>(component),
const_cast<char*>(name),
appData,const_cast<OMX_CALLBACKTYPE *>(callbacks));
}
哈哈,我们终于完成了app到寻找到正确解码器的工程了!!!
ComponentInstance,OMXCodecObserver,omxcodec,omx的关系和联系,我写了篇文章,可以到链接进去看看:
http://blog.csdn.net/tjy1985/article/details/7397752
OMXcodec通过binder(IOMX)跟omx建立了联系,解码器则通过注册的几个回调事件OMX_CALLBACKTYPEOMXNodeInstance::kCallbacks = {
&OnEvent,&OnEmptyBufferDone, &OnFillBufferDone
}往OMXNodeInstance这个接口上报消息,OMX通过消息分发机制往OMXCodecObserver发消息,它再给注册进observer的omxcodec(observer->setCodec(codec);)进行最后的处理!
stagefright通过OpenOMX联通解码器的过程至此完毕。
create最后一步就剩下configureCodec(meta),主要是设置下输出的宽高和initNativeWindow。
忘了个事,就是OMXCOdec的状态:
enumState {
DEAD,
LOADED,
LOADED_TO_IDLE,
IDLE_TO_EXECUTING,
EXECUTING,
EXECUTING_TO_IDLE,
IDLE_TO_LOADED,
RECONFIGURING,
ERROR
};
在我们实例化omxcodec的时候该状态处于LOADED状态。
LOADER后应该就是LOADER_TO_IDLE,那什么时候进入该状态呢,就是我们下面讲的start方法:
status_terr = mVideoSource->start();
mVideoSource就是omxcodec,我们进入omxcodec.cpp探个究竟:
status_tOMXCodec::start(MetaData *meta) {
….
returninit();
}
status_tOMXCodec::init() {
……..
err = allocateBuffers();
err = mOMX->sendCommand(mNode, OMX_CommandStateSet, OMX_StateIdle);
setState(LOADED_TO_IDLE);
……………………
}
start原来做了三件事啊,
1:allocateBuffers给输入端放入缓存的数据,给输出端准备匹配的nativewindow
status_tOMXCodec::allocateBuffers() {
status_t err = allocateBuffersOnPort(kPortIndexInput);
if (err != OK) {
return err;
}
return allocateBuffersOnPort(kPortIndexOutput);
}
2:分配完后通知解码器器端进入idle状态,sendCommand的流程可以参考http://blog.csdn.net/tjy1985/article/details/7397752
emptyBuffer过程
3:本身也处于IDLE。
到此我们的initVideoDecoder就完成了,initAudioDecoder流程也差不多一致,这里就不介绍了,有兴趣的可以自己跟进去看看。
prepare的最后一步finishAsyncPrepare_l(),对外宣布prepare完成,并从timeeventqueue中移除该queueitem,mAsyncPrepareEvent=null。
费了很多的口舌和时间,我们终于完成了prepare的过程,各路信息通道都打开了,往下就是播放的过程了。
五
前面两篇文章,我们分别讲了setdataSource和prepare的过程,获得了mVideoTrack,mAudioTrack,mVideoSourc,mAudioSource,前两个来自于setdataSource过程,后面两是prepare。
status_tAwesomePlayer::setDataSource_l(const sp<MediaExtractor> &extractor){…
if (!haveVideo &&!strncasecmp(mime.string(), "video/", 6)) {
setVideoSource(extractor->getTrack(i));}
else if (!haveAudio &&!strncasecmp(mime.string(), "audio/", 6)) {
setAudioSource(extractor->getTrack(i));
……………..
}
}
voidAwesomePlayer::setVideoSource(sp<MediaSource> source) {
CHECK(source != NULL);
mVideoTrack =source;
}
void AwesomePlayer::setAudioSource(sp<MediaSource>source) {
CHECK(source != NULL);
mAudioTrack =source;
}
mVideoSource = OMXCodec::Create(
mClient.interface(), mVideoTrack->getFormat(),
false, // createEncoder
mVideoTrack,
NULL, flags, USE_SURFACE_ALLOC ? mNativeWindow : NULL);
mAudioSource = OMXCodec::Create(
mClient.interface(), mAudioTrack->getFormat(),
false, // createEncoder
mAudioTrack);
通过mVideoTrack,mAudioTrack我们找到了相应的解码器,并初始化了,下面我们就开讲mediaplayer如何播放了。前面的一些接口实现,我们就不讲了,不懂的可以回到setdataSource这一篇继续研究,我们直接看Awesomeplayer的实现。先看大体的时序图吧:
status_t AwesomePlayer::play_l() {
modifyFlags(SEEK_PREVIEW, CLEAR);
…………
modifyFlags(PLAYING, SET);
modifyFlags(FIRST_FRAME, SET); ---设置PLAYING和FIRST_FRAME的标志位
…………………..
if (mAudioSource !=NULL) {-----mAudioSource不为空时初始化Audioplayer
if (mAudioPlayer == NULL) {
if (mAudioSink != NULL) {
(1) mAudioPlayer = new AudioPlayer(mAudioSink, allowDeepBuffering, this);
mAudioPlayer->setSource(mAudioSource);
seekAudioIfNecessary_l();
}
}
CHECK(!(mFlags & AUDIO_RUNNING));
if (mVideoSource == NULL) {-----如果单是音频,直接播放
….
(2) status_terr = startAudioPlayer_l(
false /*sendErrorNotification */);
modifyFlags((PLAYING | FIRST_FRAME), CLEAR);
…………..
return err;
}
}
}
……
if (mVideoSource !=NULL) {-----有视频时,发送event到queue,等待处理
// Kick off video playback
(3)postVideoEvent_l();
if (mAudioSource != NULL && mVideoSource != NULL) {----有视频,音频时,检查他们是否同步
(4) postVideoLagEvent_l();
}
}
}
…………..
return OK;
}
在playe_l方法里,我们可以看到首先是实例化一个audioplayer来播放音频,如果单单是音频直接就播放,现在我们是本地视频播放,将不会走第二步,直接走第三和第四步。我们看下postVideoEvent_l()方法,跟我们在讲prepareAsync_l的类似:
voidAwesomePlayer::postVideoEvent_l(int64_t delayUs) {
……………
mVideoEventPending =true;
mQueue.postEventWithDelay(mVideoEvent, delayUs < 0? 10000 : delayUs);
}
mVideoEvent在我们构造awesomeplayer时已经定义:
mVideoEvent = new AwesomeEvent(this, &AwesomePlayer::onVideoEvent);
所以我们看onVideoEvent方法:
void AwesomePlayer::onVideoEvent() {
if (!mVideoBuffer) {
for (;;) {
(1) status_t err =mVideoSource->read(&mVideoBuffer, &options); ---mVideoSource(omxcodec)
options.clearSeekTo();
++mStats.mNumVideoFramesDecoded;
}
(2) status_t err = startAudioPlayer_l();
if ((mNativeWindow != NULL)
&& (mVideoRendererIsPreview || mVideoRenderer == NULL)) {
mVideoRendererIsPreview = false;
(3) initRenderer_l();
}
if (mVideoRenderer !=NULL) {
mSinceLastDropped++;
(4) mVideoRenderer->render(mVideoBuffer);
}
(5) postVideoEvent_l();
}
我们看到通过read方法去解码一个个sample,获取videobuffer,然后render到surfaceTexture。
read 方法:
status_t OMXCodec::read(
MediaBuffer **buffer, const ReadOptions *options) {
if (mInitialBufferSubmit) {
mInitialBufferSubmit = false;
if (seeking) {
CHECK(seekTimeUs >= 0);
mSeekTimeUs = seekTimeUs;
mSeekMode = seekMode;
// There's no reason to trigger the code below, there's
// nothing to flush yet.
seeking = false;
mPaused = false;
}
drainInputBuffers();---对应emptybuffer,输入端
if (mState == EXECUTING) {
// Otherwise mState == RECONFIGURING and this code will trigger
// after the output port is reenabled.
fillOutputBuffers();--对应fillbuffer,输出端
}
}
….
size_t index =*mFilledBuffers.begin();
mFilledBuffers.erase(mFilledBuffers.begin());
BufferInfo *info =&mPortBuffers[kPortIndexOutput].editItemAt(index);
CHECK_EQ((int)info->mStatus, (int)OWNED_BY_US);
info->mStatus =OWNED_BY_CLIENT;
info->mMediaBuffer->add_ref();
if (mSkipCutBuffer !=NULL) {
mSkipCutBuffer->submit(info->mMediaBuffer);
}
*buffer =info->mMediaBuffer;
}
在讲read之前我们先来回顾下prepare时候的omxcodec::start方法,因为跟我们讲read有千丝万缕的关系,start方法:
status_t OMXCodec::start(MetaData *meta) {
Mutex::AutolockautoLock(mLock);
……….
sp<MetaData>params = new MetaData;
if (mQuirks &kWantsNALFragments) {
params->setInt32(kKeyWantsNALFragments, true);
}
if (meta) {
int64_t startTimeUs = 0;
int64_t timeUs;
if (meta->findInt64(kKeyTime, &timeUs)) {
startTimeUs = timeUs;
}
params->setInt64(kKeyTime, startTimeUs);
}
status_t err = mSource->start(params.get()); ---我们以mp4为例,就是mpeg4source
if (err != OK) {
return err;
}
mCodecSpecificDataIndex= 0;
mInitialBufferSubmit =true;
mSignalledEOS = false;
mNoMoreOutputData =false;
mOutputPortSettingsHaveChanged = false;
mSeekTimeUs = -1;
mSeekMode =ReadOptions::SEEK_CLOSEST_SYNC;
mTargetTimeUs = -1;
mFilledBuffers.clear();
mPaused = false;
return init();
}
status_t OMXCodec::init() {
….
err = allocateBuffers();
if (mQuirks &kRequiresLoadedToIdleAfterAllocation) {
err = mOMX->sendCommand(mNode,OMX_CommandStateSet, OMX_StateIdle);
CHECK_EQ(err,(status_t)OK);
setState(LOADED_TO_IDLE); -------发送命令到component,让component处于Idle状态,经过两次回调后使component处于OMX_StateExecuting
}
….
}
由于我们以MP4为例,所以mSource就是MPEG4Source,MPEG4Source在MPEG4Extractor.cpp,我们看下start方法做了什么:
status_t MPEG4Source::start(MetaData*params) {
Mutex::AutolockautoLock(mLock);
…………..
mGroup = newMediaBufferGroup;
int32_t max_size;
CHECK(mFormat->findInt32(kKeyMaxInputSize, &max_size));
mGroup->add_buffer(new MediaBuffer(max_size));
mSrcBuffer = newuint8_t[max_size];
mStarted = true;
return OK;
}
原来是设定输入的最大buffer.
我再看看allocateBuffers();
status_t OMXCodec::allocateBuffers() {
status_t err = allocateBuffersOnPort(kPortIndexInput);----配置输入端的buffer总量,大小等OMX_PARAM_PORTDEFINITIONTYPE
if (err != OK) {
return err;
}
return allocateBuffersOnPort(kPortIndexOutput);---配置输出端,并dequeuebuffer到OMX端
}
OMX_PARAM_PORTDEFINITIONTYPE 是component的配置信息。
typedef structOMX_PARAM_PORTDEFINITIONTYPE {
OMX_U32 nSize; /**< Size of the structure in bytes */
OMX_VERSIONTYPEnVersion; /**< OMX specification versioninformation */
OMX_U32nPortIndex; /**< Port number the structure applies to */
OMX_DIRTYPEeDir; /**< Direction (input or output) of this port */
OMX_U32nBufferCountActual; /**< The actual number of buffersallocated on this port */
OMX_U32nBufferCountMin; /**< The minimum numberof buffers this port requires */
OMX_U32 nBufferSize; /**< Size, in bytes, for buffers to be used for this channel */
OMX_BOOLbEnabled; /**< Ports default to enabled and are enabled/disabled by
OMX_CommandPortEnable/OMX_CommandPortDisable.
When disabled a port is unpopulated. A disabled port
is not populated with buffers on a transition to IDLE. */
OMX_BOOLbPopulated; /**<Port is populated with all of its buffers as indicated by
nBufferCountActual. A disabled port is always unpopulated.
An enabled port is populated on a transition to OMX_StateIdle
and unpopulated on a transition to loaded. */
OMX_PORTDOMAINTYPEeDomain; /**< Domain of the port. Determines the contentsof metadata below. */
union {
OMX_AUDIO_PORTDEFINITIONTYPE audio;
OMX_VIDEO_PORTDEFINITIONTYPE video;
OMX_IMAGE_PORTDEFINITIONTYPE image;
OMX_OTHER_PORTDEFINITIONTYPE other;
} format;
OMX_BOOLbBuffersContiguous;
OMX_U32nBufferAlignment;
} OMX_PARAM_PORTDEFINITIONTYPE;
OMX_PARAM_PORTDEFINITIONTYPE的参数从哪里来呢?原来来自解码器端,包括输入输出端的buffer大小,总数等信息。
status_tOMXCodec::allocateBuffersOnPort(OMX_U32 portIndex) {
if (mNativeWindow !=NULL && portIndex == kPortIndexOutput) {
return allocateOutputBuffersFromNativeWindow();------当输出的时候走这里,给输出端分配内存空间,并dequeue buffer 到OMX。
}
OMX_PARAM_PORTDEFINITIONTYPE def;
InitOMXParams(&def);
def.nPortIndex =portIndex;
err =mOMX->getParameter(
mNode, OMX_IndexParamPortDefinition, &def, sizeof(def));---从component获取OMX_PARAM_PORTDEFINITIONTYPE相关配置,具体哪些可以看上面的结构体
if (err != OK) {
return err;
}
size_t totalSize =def.nBufferCountActual * def.nBufferSize; ---从getParameter获得的每个输入/输出端的buffer大小和总数
mDealer[portIndex] =new MemoryDealer(totalSize, "OMXCodec");
for (OMX_U32 i = 0; i< def.nBufferCountActual; ++i) {
sp<IMemory> mem = mDealer[portIndex]->allocate(def.nBufferSize);
CHECK(mem.get() != NULL);
BufferInfo info;
info.mData = NULL;
info.mSize =def.nBufferSize;
IOMX::buffer_idbuffer;
if (portIndex == kPortIndexInput
&& ((mQuirks & kRequiresAllocateBufferOnInputPorts)
|| (mFlags & kUseSecureInputBuffers))) {
if (mOMXLivesLocally) {
mem.clear();
err = mOMX->allocateBuffer(
mNode, portIndex,def.nBufferSize, &buffer,
&info.mData);-----给输入端分配内存空间,并使info.mData指向mNode的header
…………….
info.mBuffer = buffer;
info.mStatus =OWNED_BY_US;
info.mMem = mem;
info.mMediaBuffer= NULL;
mPortBuffers[portIndex].push(info); ---BufferInfo 放到Vector<BufferInfo>mPortBuffers[2] mPortBuffers进行管理,到read的时候用,0是输入,1是输出。
………………………….
}
复习完start方法,我们就来讲reader方法了:
status_t OMXCodec::read(
MediaBuffer **buffer, const ReadOptions *options) {
if (mInitialBufferSubmit) {
mInitialBufferSubmit = false;
………….
drainInputBuffers();
if (mState == EXECUTING) {
// Otherwise mState == RECONFIGURING and this code will trigger
// after the output port is reenabled.
fillOutputBuffers();
}
…………………..
size_t index =*mFilledBuffers.begin();
mFilledBuffers.erase(mFilledBuffers.begin());
BufferInfo *info =&mPortBuffers[kPortIndexOutput].editItemAt(index);
CHECK_EQ((int)info->mStatus, (int)OWNED_BY_US);
info->mStatus =OWNED_BY_CLIENT;
info->mMediaBuffer->add_ref();
if (mSkipCutBuffer !=NULL) {
mSkipCutBuffer->submit(info->mMediaBuffer);
}
*buffer = info->mMediaBuffer;
}
先看drainInputBuffers方法,主要是从mediasource读取数据元,
void OMXCodec::drainInputBuffers() {
CHECK(mState ==EXECUTING || mState == RECONFIGURING);
if (mFlags &kUseSecureInputBuffers) {
Vector<BufferInfo> *buffers = &mPortBuffers[kPortIndexInput];---mPortBuffers是我们allocateBuffersOnPort方法存下来的对应的输入/输出bufferinfo数据
for (size_t i = 0; i < buffers->size(); ++i) {---循环每次输入端能填充数据的buffer总数,这是由component的结构决定的,各个厂商的解码器配置不一样
if (!drainAnyInputBuffer()-----往buffer里面填元数据,给解码器解码
|| (mFlags & kOnlySubmitOneInputBufferAtOneTime)) {
break;
}
}
}
………………
}
bool OMXCodec::drainAnyInputBuffer() {
returndrainInputBuffer((BufferInfo *)NULL);
}
bool OMXCodec::drainInputBuffer(BufferInfo*info) {
for (;;) {
MediaBuffer *srcBuffer;
if (mSeekTimeUs >= 0) {
if (mLeftOverBuffer) {
mLeftOverBuffer->release();
mLeftOverBuffer = NULL;
}
MediaSource::ReadOptions options;
options.setSeekTo(mSeekTimeUs, mSeekMode);
mSeekTimeUs = -1;
mSeekMode = ReadOptions::SEEK_CLOSEST_SYNC;---seek模式
mBufferFilled.signal();
err = mSource->read(&srcBuffer, &options);---读mediasource,我们以mpeg4为例,它的实现就在MPEG4Extrator.cpp(),根据seek模式和seek时间从sampletable里面找到meta_data。存到srcBuffer。
if (mFlags & kUseSecureInputBuffers) {
info = findInputBufferByDataPointer(srcBuffer->data());---让bufferinfo的mData指向元数据的data
CHECK(info != NULL);
}
err =mOMX->emptyBuffer(
mNode,info->mBuffer, 0, offset,
flags,timestampUs); ----对应component的方法是OMX_EmptyThisBuffer,回调消息为:EmptyBufferDone。
if (err != OK) {
setState(ERROR);
return false;
}
info->mStatus =OWNED_BY_COMPONENT;----设置状态为OWNED_BY_COMPONENT
}
从上面的分析,我们得知emtyBuffer后在5msec之内会有个EmptyBufferDone回调,我们看下omxcodec对该回调的处理:
void OMXCodec::on_message(constomx_message &msg) {
case omx_message::EMPTY_BUFFER_DONE:
………………
IOMX::buffer_id buffer =msg.u.extended_buffer_data.buffer;
CODEC_LOGV("EMPTY_BUFFER_DONE(buffer: %p)", buffer);
Vector<BufferInfo> *buffers = &mPortBuffers[kPortIndexInput];
size_t i = 0;
while (i < buffers->size() && (*buffers)[i].mBuffer != buffer) {
++i;
}
BufferInfo* info =&buffers->editItemAt(i);
-------------通过buffer_id找到Vector<BufferInfo>bufferInfo
info->mStatus = OWNED_BY_US;-------设置info的状态为OWNED_BY_US
info->mMediaBuffer->release();-----释放mediabuffer
info->mMediaBuffer = NULL;
…………….
if (mState != ERROR
&& mPortStatus[kPortIndexInput] != SHUTTING_DOWN) {
CHECK_EQ((int)mPortStatus[kPortIndexInput], (int)ENABLED);
if (mFlags & kUseSecureInputBuffers) {
drainAnyInputBuffer();----下一片段buffer移交给component
} else {
drainInputBuffer(&buffers->editItemAt(i));
}
}
emptybuffer后应该就是fillOutputBuffer:
void OMXCodec::fillOutputBuffer(BufferInfo*info) {
CHECK_EQ((int)info->mStatus, (int)OWNED_BY_US);
if (mNoMoreOutputData){
CODEC_LOGV("There is no more output data available, not "
"calling fillOutputBuffer");--------------没有数据了退出
return;
}
if(info->mMediaBuffer != NULL) {
sp<GraphicBuffer> graphicBuffer =info->mMediaBuffer->graphicBuffer();
if (graphicBuffer != 0) {
// When using a native buffer we need to lock the buffer before
// giving it to OMX.
CODEC_LOGV("Calling lockBuffer on %p", info->mBuffer);
int err = mNativeWindow->lockBuffer(mNativeWindow.get(),
graphicBuffer.get()); -------锁定该buffer,准备render图像
if (err != 0) {
CODEC_LOGE("lockBuffer failed w/ error 0x%08x", err);
setState(ERROR);
return;
}
}
}
CODEC_LOGV("Calling fillBuffer on buffer %p", info->mBuffer);
status_t err =mOMX->fillBuffer(mNode, info->mBuffer);---------填充输出端buffer
……….
info->mStatus = OWNED_BY_COMPONENT;
}
fillbuffer后获得mVideoBuffer就可以在Awesomeplayer的onvideoEvent方法中的mVideoRenderer->render(mVideoBuffer);进行图像的显示了。
-------------------------------------gulinuxvideoPlayerLocal------------------------------------
以上我们就是播放的过程了。到此多媒体本地播放流程全部讲完了,里面很多细节的东西,还得大伙自己深入理解,往后有什么需要补充和添加的,我会再次补充上。
六
我们讲多媒体,涉及到的最多的就是MP4文件和MP3文件了,但是我们对这两个文件的格式了解多少呢,它的由有哪些部分部分组成呢?它的核心部件是哪些?它哪些部分是供解码器去解析的呢?带着这些疑问,我们首先来探索下MP4文件。
我们首先用MP4Info这个工具来看下MP4的大貌:
从上图我们可以看到MP4文件中的所有数据都装在box中,也就是说MP4文件由若干个box组成,每个box有类型和长度,可以将box理解为一个数据对象块。box中可以包含另一个box,这种box称为container box。一个MP4文件首先会有且只有一个“ftyp”类型的box,作为MP4格式的标志并包含关于文件的一些信息;之后会有且只有一个“moov”类型的box(Movie Box),它是一种container box,子box包含了媒体的metadata信息;一个moov可以由多个tracks组成。每个track就是一个随时间变化的媒体序列,例如,视频帧序列。track里的每个时间单位是一个sample,它可以是一帧视频,或者音频。sample按照时间顺序排列。注意,一帧音频可以分解成多个音频sample,所以音频一般用sample作为单位,而不用帧。MP4文件的媒体数据包含在“mdat”类型的box(Midia Data Box)中,该类型的box也是container box,可以有多个,也可以没有(当媒体数据全部引用其他文件时),媒体数据的结构由metadata进行描述。“free”类型的box,就是一些自由的信息,可以写,也可以不写。
box中的字节序为网络字节序,也就是大端字节序(Big-Endian),简单的说,就是一个32位的4字节整数存储方式为高位字节在内存的低端。Box由header和body组成,其中header统一指明box的大小和类型,body根据类型有不同的意义和格式。
BOX
标准的box开头的4个字节(32位)为box size,该大小包括box header和box body整个box的大小,这样我们就可以在文件中定位各个box。如果size为1,则表示这个box的大小为large size,真正的size值要在largesize域上得到。(实际上只有“mdat”类型的box才有可能用到large size。)如果size为0,表示该box为文件的最后一个box,文件结尾即为该box结尾。(同样只存在于“mdat”类型的box中。)
size后面紧跟的32位为box type,一般是4个字符,如“ftyp”、“moov”等,这些box type都是已经预定义好的,分别表示固定的意义。如果是“uuid”,表示该box为用户扩展类型。如果box type是未定义的,应该将其忽略。
对应的代码片段为:framework/av/media/libstagefright/MPEG4Extrator.cpp
status_tMPEG4Extractor::parseChunk(off64_t *offset, int depth) {
ALOGV("enteringparseChunk %lld/%d", *offset, depth);
uint32_t hdr[2];
static const char*mQTMajorBrand = "qt ";
if(mDataSource->readAt(*offset, hdr, 8) < 8) {
return ERROR_IO;
}
uint64_t chunk_size = ntohl(hdr[0]);---box size
uint32_t chunk_type = ntohl(hdr[1]);---boxtype
off64_t data_offset =*offset + 8;
if (chunk_size == 1) {
if (mDataSource->readAt(*offset + 8,&chunk_size, 8) < 8) {---读取box size的大小
return ERROR_IO;
}
chunk_size = ntoh64(chunk_size);---将64位的网络字节转换为主机字节
data_offset += 8;
……….
char chunk[5];
MakeFourCCString(chunk_type, chunk); ----FOURCC全称Four-Character Codes,是在编程
中非常常用的东西,一般用作标示符。它是一个32位的标示符,其实就是typedef unsigned long FOURCC
}
…………
}
static void MakeFourCCString(uint32_t x,char *s) {
s[0] = x >> 24;
s[1] = (x >> 16)& 0xff;
s[2] = (x >> 8)& 0xff;
s[3] = x & 0xff;
s[4] = '\0';
}
File Type Box(ftyp)
File Type Box(ftyp):该box有且只有1个,并且只能被包含在文件层,而不能被其他box包含。该box应该被放在文件的最开始,指示该MP4文件应用的相关信息。 “ftyp” body依次包括1个32位的major brand(4个字符),1个32位的minor version(整数)和1个以32位(4个字符)为单位元素的数组compatible brands。这些都是用来指示文件应用级别的信息。该box的字节实例如下:
对应的的代码如下:
framework/av/media/libstagefright/MPEG4Extrator.cpp
status_tMPEG4Extractor::parseChunk(off64_t *offset, int depth) {
switch(chunk_type) {
case FOURCC('f', 't', 'y', 'p'):
{
if (chunk_data_size < 4) {
return ERROR_MALFORMED;
}
uint32_t ftype;
if (mDataSource->readAt(data_offset, &ftype, 4) < 4) {
return ERROR_IO;
}
MakeFourCCString(ntohl(ftype), mMajorBrand); -----majorbrand
*offset += chunk_size;
break;
}
}
Movie Box(moov)
该box包含了文件媒体的metadata信息,“moov”是一个container box,具体内容信息由子box诠释。同File Type Box一样,该box有且只有一个,且只被包含在文件层。一般情况下,
“moov”会紧随“ftyp”出现。一般情况下, “moov”中会包含1个“mvhd”和若干个“trak”。其中“mvhd”为header box,一般作为“moov”的第一个子box出现(对于其他container box来说,header box都应作为首个子box出现)。“trak”包含了一个track的相关信息,是一个container box。结构如下图:
Movie Header Box(mvhd)
字段 | 字节数 | 意义 |
box size | 4 | box大小 |
box type | 4 | box类型 |
version | 1 | box版本,0或1,一般为0。(以下字节数均按version=0) |
flags | 3 |
|
creation time | 4 | 创建时间(相对于UTC时间1904-01-01零点的秒数) |
modification time | 4 | 修改时间 |
time scale | 4 | 文件媒体在1秒时间内的刻度值,可以理解为1秒长度的时间单元数 |
duration | 4 | 该track的时间长度,用duration和time scale值可以计算track时长,比如audio track的time scale = 8000, duration = 560128,时长为70.016,video track的time scale = 600, duration = 42000,时长为70 |
rate | 4 | 推荐播放速率,高16位和低16位分别为小数点整数部分和小数部分,即[16.16] 格式,该值为1.0(0x00010000)表示正常前向播放 |
volume | 2 | 与rate类似,[8.8] 格式,1.0(0x0100)表示最大音量 |
reserved | 10 | 保留位 |
matrix | 36 | 视频变换矩阵 |
pre-defined | 24 |
|
next track id | 4 | 下一个track使用的id号 |
status_tMPEG4Extractor::parseChunk(off64_t *offset, int depth) {
ALOGV("enteringparseChunk %lld/%d", *offset, depth);
uint32_t hdr[2];
static const char* mQTMajorBrand= "qt ";
if(mDataSource->readAt(*offset, hdr, 8) < 8) {
return ERROR_IO;
}
uint64_t chunk_size = ntohl(hdr[0]);---boxsize
uint32_t chunk_type = ntohl(hdr[1]);---boxtype
………………….
case FOURCC('m', 'v', 'h', 'd'):
{
if (chunk_data_size < 12) { //increase to 16?---
return ERROR_MALFORMED;
}
uint8_t header[16];
if (mDataSource->readAt(
data_offset, header, sizeof(header))
< (ssize_t)sizeof(header)) {
return ERROR_IO;
}
int64_t creationTime;
if (header[0] == 1) {
creationTime = U64_AT(&header[4]);
mFileMetaData->setInt64(kKeyEditOffset, 0 );
} else if (header[0] != 0) {
return ERROR_MALFORMED;
} else {
creationTime = U32_AT(&header[4]);-------创建时间,4个字节
int32_t mvTimeScale = U32_AT(&header[12]);---时间刻度,4个字节
mFileMetaData->setInt32(kKeyEditOffset, mvTimeScale );
}
String8 s;
convertTimeToDate(creationTime, &s);
mFileMetaData->setCString(kKeyDate, s.string());
*offset += chunk_size;
break;
}
Track Box(trak)
“trak”也是一个container box,其子box包含了该track的媒体数据引用和描述(hint track除外)。一个MP4文件中的媒体可以包含多个track,且至少有一个track,这些track之间彼此独立,有自己的时间和空间信息。“trak”必须包含一个“tkhd”和一个“mdia”,此外还有很多可选的box其中“tkhd”为track header box,“mdia”为media box,该box是一个包含一些track媒体数据信息box的container box。
status_tMPEG4Extractor::parseChunk(off64_t *offset, int depth) {
ALOGV("enteringparseChunk %lld/%d", *offset, depth);
uint32_t hdr[2];
static const char*mQTMajorBrand = "qt ";
if(mDataSource->readAt(*offset, hdr, 8) < 8) {
return ERROR_IO;
}
uint64_t chunk_size = ntohl(hdr[0]);---box size
uint32_t chunk_type =ntohl(hdr[1]);---box type
……………………..
if (chunk_type == FOURCC('t', 'r', 'a','k')) {
isTrack = true;
Track *track = new Track; --- 如果是Track,new 个track
track->next = NULL;
if (mLastTrack) {
mLastTrack->next = track;
} else {
mFirstTrack = track;
}
mLastTrack = track;
track->meta = new MetaData;
track->includes_expensive_metadata = false;
track->skipTrack = false;
track->timescale = 0;
track->meta->setCString(kKeyMIMEType,"application/octet-stream");
}
off64_t stop_offset = *offset + chunk_size;
*offset = data_offset;
while (*offset < stop_offset) {
if (stop_offset - *offset >= 8) {
status_t err = parseChunk(offset, depth + 1);
if (err != OK) {
if(chunk_type == FOURCC('u', 'd', 't', 'a')){
ALOGW("error in udta atom, ignoring %llu bytes",stop_offset -*offset);
*offset = stop_offset;
} else {
return err;
}
}
}
………….
}
七
SampleTable Box(stbl)
“stbl”几乎是普通的MP4文件中最复杂的一个box了。sample是媒体数据存储的单位,存储在media的chunk中,chunk和sample的长度均可互不相同。chunk是几个sample的集合。“stbl”包含了关于track中sample所有时间和位置的信息,以及sample的编解码等信息。利用这个表,可以解释sample的时序、类型、大小以及在各自存储容器中的位置。“stbl”是一个containerbox,其子box包括:sampledescription box(stsd)、time tosample box(stts)、samplesize box(stsz或stz2)、sample tochunk box(stsc)、chunkoffset box(stco或co64)、compositiontime to sample box(ctts)、syncsample box(stss)等。“stsd”必不可少,且至少包含一个条目,该box包含了datareference box进行sample数据检索的信息。没有“stsd”就无法计算mediasample的存储位置。“stsd”包含了编码的信息,其存储的信息随媒体类型不同而不同。
if(chunk_type == FOURCC('s', 't', 'b', 'l')) {
ALOGV("sampleTable chunk is %d bytes long.", (size_t)chunk_size);
if (mDataSource->flags()
& (DataSource::kWantsPrefetching
| DataSource::kIsCachingDataSource)) {
sp<MPEG4DataSource> cachedSource =
new MPEG4DataSource(mDataSource);
if (cachedSource->setCachedRange(*offset, chunk_size) == OK) {
mDataSource =cachedSource;
}
}
mLastTrack->sampleTable= new SampleTable(mDataSource);----创建sampletable,每个track对应一个sampletable
}
SampleDescription Box(stsd)
boxheader和version字段后会有一个entry count字段,根据entry的个数,每个entry会有type信息,如“vide”、“sund”等,根据type不同sampledescription会提供不同的信息,例如对于videotrack,会有“VisualSampleEntry”类型信息,对于audio track会有“AudioSampleEntry”类型信息。视频的编码类型、宽高、长度,音频的声道、采样等信息都会出现在这个box中。
caseFOURCC('s', 't', 's', 'd'):
{
,…………………………….
uint32_t entry_count = U32_AT(&buffer[4]);
off64_t stop_offset = *offset + chunk_size;
*offset = data_offset + 8;
if (entry_count > 1) {----针对3GPP,有可能有多个entry_count,但目前我们每个track支持单类型的media
// For 3GPP timed text, there could be multiple tx3g boxes contain
// multiple text display formats. These formats will be used to
// display the timed text.
const char *mime;
CHECK(mLastTrack->meta->findCString(kKeyMIMEType, &mime));
if (!strcasecmp(mime, MEDIA_MIMETYPE_TEXT_3GPP)) {
ALOGV("Text track found");
for (uint32_t i = 0; i < entry_count; ++i) {
status_t err = parseChunk(offset, depth + 1);
if (err != OK) {
return err;
}
}
// For now we only support a single type of media per track.
}
else {
status_t err = mLastTrack->sampleTable->setSampleDescParams(entry_count,*offset, chunk_data_size);
if (err != OK) {
return ERROR_IO;
}
//视频的编码类型、宽高、长度,音频的声道、采样等信息
mHasVideo = true;
uint8_t avc1[86];//(avc1-avcc) which is fixed
if (mDataSource->readAt(*offset, avc1, sizeof(avc1)) <(ssize_t)sizeof(avc1)) {
return ERROR_IO;
}
uint32_t chunk_type = U32_AT(&avc1[4]);
uint16_t data_ref_index = U16_AT(&avc1[14]);
uint16_t width = U16_AT(&avc1[32]);
uint16_t height = U16_AT(&avc1[34]);
mLastTrack->meta->setCString(kKeyMIMEType, FourCC2MIME(chunk_type));
mLastTrack->meta->setInt32(kKeyWidth, width);
mLastTrack->meta->setInt32(kKeyHeight, height);
uint8_t *avcc;
uint32_t avccSize;
mLastTrack->sampleTable->getSampleDescAtIndex(1, &avcc,&avccSize);
mLastTrack->meta->setData(kKeyAVCC, kTypeAVCC, avcc, avccSize);
*offset = stop_offset;
}
} else {
for (uint32_t i = 0; i < entry_count; ++i) {
status_t err = parseChunk(offset, depth + 1);
if (err != OK) {
return err;
}
} // end of for
}//end of entry count 1
if (*offset != stop_offset) {
return ERROR_MALFORMED;
}
break;
}
TimeTo Sample Box(stts)
“stts”存储了sample的duration,描述了sample时序的映射方法,我们通过它可以找到任何时间的sample。“stts”可以包含一个压缩的表来映射时间和sample序号,用其他的表来提供每个sample的长度和指针。表中每个条目提供了在同一个时间偏移量里面连续的sample序号,以及samples的偏移量。递增这些偏移量,就可以建立一个完整的timeto sample表。
caseFOURCC('s', 't', 't', 's'):
{
status_t err =
mLastTrack->sampleTable->setTimeToSampleParams(---该方法在SampleTable.cpp,映射时间和sample序号
data_offset, chunk_data_size);
if (err != OK) {
return err;
}
*offset += chunk_size;
break;
}
Sample Size Box(stsz)
“stsz”定义了每个sample的大小,包含了媒体中全部sample的数目和一张给出每个sample大小的表。这个box相对来说体积是比较大的。
caseFOURCC('s', 't', 's', 'z'):
case FOURCC('s', 't', 'z', '2'):
{
status_t err =
mLastTrack->sampleTable->setSampleSizeParams(-----该方法在SampleTable.cpp,设置sample大小
chunk_type, data_offset, chunk_data_size);
if (err != OK) {
return err;
}
size_t max_size;
err = mLastTrack->sampleTable->getMaxSampleSize(&max_size);
if (err != OK) {
return err;
}
// Assume that a given buffer only contains at most 10 fragments,
// each fragment originally prefixed with a 2 byte length will
// have a 4 byte header (0x00 0x00 0x00 0x01) after conversion,
// and thus will grow by 2 bytes per fragment.
mLastTrack->meta->setInt32(kKeyMaxInputSize, max_size + 10 * 2);
*offset += chunk_size;
// Calculate average frame rate.
const char *mime;
CHECK(mLastTrack->meta->findCString(kKeyMIMEType, &mime));
if (!strncasecmp("video/", mime, 6)) {
size_t nSamples = mLastTrack->sampleTable->countSamples();
int64_t durationUs;
if (mLastTrack->meta->findInt64(kKeyDuration, &durationUs)) {
if (durationUs > 0) {
int32_t frameRate = (nSamples * 1000000LL +
(durationUs >> 1)) / durationUs;
mLastTrack->meta->setInt32(kKeyFrameRate, frameRate);
}
}
}
break;
}
SampleTo Chunk Box(stsc)
用chunk组织sample可以方便优化数据获取,一个chunk包含一个或多个sample。“stsc”中用一个表描述了sample与chunk的映射关系,查看这张表就可以找到包含指定sample的chunk,从而找到这个sample。
caseFOURCC('s', 't', 's', 'c'):
{
status_t err =
mLastTrack->sampleTable->setSampleToChunkParams(该方法在SampleTable.cpp,映射sample和chunk的关系,一个或多个sample组成一个chunk
data_offset, chunk_data_size);
if (err != OK) {
return err;
}
*offset += chunk_size;
break;
}
SyncSample Box(stss)
“stss”确定media中的关键帧。对于压缩媒体数据,关键帧是一系列压缩序列的开始帧,其解压缩时不依赖以前的帧,而后续帧的解压缩将依赖于这个关键帧。“stss”可以非常紧凑的标记媒体内的随机存取点,它包含一个sample序号表,表内的每一项严格按照sample的序号排列,说明了媒体中的哪一个sample是关键帧。如果此表不存在,说明每一个sample都是一个关键帧,是一个随机存取点。
如何查找关键帧呢?
1:确定给定时间的sample序号检查sync sampleatom来发现这个sample序号之后的key frame
2:检查sample-to-chunkatom来发现对应该sample的chunk
3:从chunkoffset atom中提取该chunk的偏移量
4:利用sample sizeatom找到sample在trunk内的偏移量和sample的大小
caseFOURCC('s', 't', 's', 's'):
{
status_t err =
mLastTrack->sampleTable->setSyncSampleParams(----设置关键帧
data_offset, chunk_data_size);
if (err != OK) {
return err;
}
*offset += chunk_size;
break;
}
ChunkOffset Box(stco)
“stco”定义了每个chunk在媒体流中的位置。位置有两种可能,32位的和64位的,后者对非常大的电影很有用。在一个表中只会有一种可能,这个位置是在整个文件中的,而不是在任何box中的,这样做就可以直接在文件中找到媒体数据,而不用解释box。需要注意的是一旦前面的box有了任何改变,这张表都要重新建立,因为位置信息已经改变了。
caseFOURCC('s', 't', 'c', 'o'):
case FOURCC('c', 'o', '6', '4'):
{
status_t err =
mLastTrack->sampleTable->setChunkOffsetParams(---设置chunk的偏移量
chunk_type,data_offset, chunk_data_size);
if (err != OK) {
return err;
}
*offset += chunk_size;
break;
}
FreeSpace Box(free或skip)
“free”中的内容是无关紧要的,可以被忽略。该box被删除后,不会对播放产生任何影响。
MeidaData Box(mdat)
该box包含于文件层,可以有多个,也可以没有(当媒体数据全部为外部文件引用时),用来存储媒体数据。
下图为总的概括:
参考资料:http://mpeg.chiariglione.org/standards/mpeg-4/mpeg-4.htm
具体源码:frameworks/av/media/libstagefright/MPEG4Extractor.cpp
frameworks/av/media/libstagefright/sampletable.cpp
好了,MP4文件格式已经介绍完了,videorecoder也会用到这些知识,望大家好好研究研究。
八
---------------------------------------------------流媒体 gulinux StreamingMedia---------------------------
从这篇开始我们将进入流媒体的环节,流媒体在android中有nuplayer来实现的,在开始讲解android流媒体前,我们先来讲讲流媒体传输协议,了解了基本协议,我们在看代码的过程中,就会有事半功倍的效果。我们将主要讲解RTSP,HTTP,HTTPS,SDP四种协议。
一:RTSP协议简介(Real Time Streaming Protocol)
实时流协议RTSP是一个应用层协议,用于控制具有实时特性的数据(例如多媒体流)的传送。
RTSP协议一般与RTP/RTCP和RSVP等底层协议一起协同工作,提供基于Internet的整套的流服务。它可以选择发送通道(例如:UDP、组播UDP和TCP)和基于RTP的发送机制。它可以应用于组播和点播。RTP,RTCP,RSVP 定义如下:
1. 实时传输协议RTP(Real-timeTransport protocol)
2. 实时传输控制协议RTCP(Real-timeTransport Control protocol)
3. 实时流协议RTSP(RealTime Streaming protocol)
4. 资源预留协议RSVP(ResourceReserve Protocol)
RTSP协议机理:
客户机在向视频服务器请求视频服务之前,首先通过HTTP协议从Web服务器获取所请求视频服务的演示描述(Presentationdescription )文件,在RTSP中,每个演示(Presentation)及其所对应的媒体流都由一个RTSP URL标识。整个演示及媒体特性都在一个演示描述(Presentationdescription )文件中定义,该文件可能包括媒体编码方式、语言、RTSPURLs、目标地址、端口及其它参数。用户在向服务器请求某个连续媒体流的服务之前,必须首先从服务器获得该媒体流的演示描述(Presentationdescription )文件以得到必需的参数,演示描述文件的获取可采用HTTP、email或其他方法。利用该文件提供的信息定位视频服务地址(包括视频服务器地址和端口号)及视频服务的编码方式等信息。然后客户机根据上述信息向视频服务器请求视频服务。视频服务初始化完毕,视频服务器为该客户建立一个新的视频服务流,客户端与服务器运行实时流控制协议RTSP,以对该流进行各种VCR控制信号的交换,如播放(PLAY)、停止(PAUSE)、快进、快退等。当服务完毕,客户端提出拆线(TEARDOWN)请求。服务器使用RTP/UDP协议将媒体数据传输给客户端,一旦数据抵达客户端,客户端应用程序即可播放输出。在流式传输中,使用RTP/RTCP/UDP和RTSP/TCP两种不同的通信协议在客户端和服务器间建立联系。如下图:
RTSP中的所有的操作都是通过服务器和客户方的消息应答来完成的,其消息包括请求(Request)和响应(Response)两种,RTSP正是通过服务器和客户端的消息应答来完成媒体流的创建、初始化(SETUP)、VCR控制(PLAY、PAUSE)以及拆线(TEARDOWN)等操作的。如下图:
RSTP一些基本方法及用途:
OPTIONS 获得有效方法
SETUP 建立传输
ANNOUNCE 改变媒体文件的类型
DESCRIBE 获得媒体文件的类型
PLAY 播放
RECORD 刻录
REDIRECT 转换客户端到新的服务器
PAUSE 暂停
SETPARAMETER 设置设备,编码等参数
TEARDOWN 移除状态
完整的播放过程:
GET过程:
C->W:GET /twister.sdp HTTP/1.1
Host:www.example.com
Accept:application/sdp
W->C:HTTP/1.0 200 OK
Content-Type:application/sdp
v=0
o=-2890844526 2890842807 IN IP4 192.16.24.202
s=RTSPSession
m=audio0 RTP/AVP 0
a=control:rtsp://audio.com/twister/audio.en
m=video0 RTP/AVP 31
a=control:rtsp://video.com/twister/video
SETUP过程:
C->A(audio): SETUPrtsp://audio.com/twister/audio.en RTSP/1.0
CSeq:1
Transport:RTP/AVP/UDP;unicast
;client_port=3056-3057
A->C:RTSP/1.0 200 OK
CSeq:1
Session:12345678
Transport:RTP/AVP/UDP;unicast
;client_port=3056-3057;
;server_port=5000-5001
C->V(video): SETUPrtsp://video.com/twister/video RTSP/1.0
CSeq:1
Transport:RTP/AVP/UDP;unicast
;client_port=3058-3059
V->C:RTSP/1.0 200 OK
CSeq:1
Session:23456789
Transport:RTP/AVP/UDP;unicast
;client_port=3058-3059
;server_port=5002-5003
PLAY过程:
C->V:PLAY rtsp://video.com/twister/video RTSP/1.0
CSeq:2
Session:23456789
Range:smpte=0:10:00-
V->C:RTSP/1.0 200 OK
CSeq:2
Session:23456789
Range:smpte=0:10:00-0:20:00
RTP-Info:url=rtsp://video.com/twister/video
;seq=12312232;rtptime=78712811
C->A:PLAY rtsp://audio.com/twister/audio.en RTSP/1.0
CSeq:2
Session:12345678
Range:smpte=0:10:00-
A->C:RTSP/1.0 200 OK
CSeq:2
Session:12345678
Range:smpte=0:10:00-0:20:00
RTP-Info:url=rtsp://audio.com/twister/audio.en
;seq=876655;rtptime=1032181
close 过程:
C->A:TEARDOWN rtsp://audio.com/twister/audio.en RTSP/1.0
CSeq:3
Session:12345678
A->C:RTSP/1.0 200 OK
CSeq:3
C->V:TEARDOWN rtsp://video.com/twister/video RTSP/1.0
CSeq:3
Session:23456789
V->C:RTSP/1.0 200 OK
CSeq: 3
关于RTSP的一些时间概念:
normalplay time (NPT):seconds, microseconds
MPTEtimestamps (seconds,frames)
absolutetime (forlive events)
二 HTTP协议简介(超文本传送协议 (HTTP-Hypertext transfer protocol) )
HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HTTP-NG(NextGeneration of HTTP)的建议已经提出。
1:HTTP协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。
由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
2:HTTP协议的几个重要概念
1.连接(Connection):一个传输层的实际环流,它是建立在两个相互通讯的应用程序之间。
2.消息(Message):HTTP通讯的基本单位,包括一个结构化的八元组序列并通过连接传输。
3.请求(Request):一个从客户端到服务器的请求信息包括应用于资源的方法、资源的标识符和协议的版本号
4.响应(Response):一个从服务器返回的信息包括HTTP协议的版本号、请求的状态(例如“成功”或“没找到”)和文档的MIME类型。
5.资源(Resource):由URI标识的网络数据对象或服务。
6.实体(Entity):数据资源或来自服务资源的回映的一种特殊表示方法,它可能被包围在一个请求或响应信息中。一个实体包括实体头信息和实体的本身内容。
7.客户机(Client):一个为发送请求目的而建立连接的应用程序。
8.用户代理(Useragent):初始化一个请求的客户机。它们是浏览器、编辑器或其它用户工具。
9.服务器(Server):一个接受连接并对请求返回信息的应用程序。
10.源服务器(Originserver):是一个给定资源可以在其上驻留或被创建的服务器。
11.代理(Proxy):一个中间程序,它可以充当一个服务器,也可以充当一个客户机,为其它客户机建立请求。请求是通过可能的翻译在内部或经过传递到其它的服务器中。一个代理在发送请求信息之前,必须解释并且如果可能重写它。
代理经常作为通过防火墙的客户机端的门户,代理还可以作为一个帮助应用来通过协议处理没有被用户代理完成的请求。
12.网关(Gateway):一个作为其它服务器中间媒介的服务器。与代理不同的是,网关接受请求就好象对被请求的资源来说它就是源服务器;发出请求的客户机并没有意识到它在同网关打交道。
网关经常作为通过防火墙的服务器端的门户,网关还可以作为一个协议翻译器以便存取那些存储在非HTTP系统中的资源。
13.通道(Tunnel):是作为两个连接中继的中介程序。一旦激活,通道便被认为不属于HTTP通讯,尽管通道可能是被一个HTTP请求初始化的。当被中继的连接两端关闭时,通道便消失。当一个门户(Portal)必须存在或中介(Intermediary)不能解释中继的通讯时通道被经常使用。
14.缓存(Cache):反应信息的局域存储。
3:建立连接的方式
HTTP支持2中建立连接的方式:非持久连接和持久连接(HTTP1.1默认的连接方式为持久连接)。
1) 非持久连接
让我们查看一下非持久连接情况下从服务器到客户传送一个Web页面的步骤。假设该贝面由1个基本HTML文件和10个JPEG图像构成,而且所有这些对象都存放在同一台服务器主机中。再假设该基本HTML文件的URL为:gpcuster.cnblogs.com/index.html。
下面是具体步骡:
1.HTTP客户初始化一个与服务器主机gpcuster.cnblogs.com中的HTTP服务器的TCP连接。HTTP服务器使用默认端口号80监听来自HTTP客户的连接建立请求。
2.HTTP客户经由与TCP连接相关联的本地套接字发出—个HTTP请求消息。这个消息中包含路径名/somepath/index.html。
3.HTTP服务器经由与TCP连接相关联的本地套接字接收这个请求消息,再从服务器主机的内存或硬盘中取出对象/somepath/index.html,经由同一个套接字发出包含该对象的响应消息。
4.HTTP服务器告知TCP关闭这个TCP连接(不过TCP要到客户收到刚才这个响应消息之后才会真正终止这个连接)。
5.HTTP客户经由同一个套接字接收这个响应消息。TCP连接随后终止。该消息标明所封装的对象是一个HTML文件。客户从中取出这个文件,加以分析后发现其中有10个JPEG对象的引用。
6.给每一个引用到的JPEG对象重复步骡1-4。
上述步骤之所以称为使用非持久连接,原因是每次服务器发出一个对象后,相应的TCP连接就被关闭,也就是说每个连接都没有持续到可用于传送其他对象。每个TCP连接只用于传输一个请求消息和一个响应消息。就上述例子而言,用户每请求一次那个web页面,就产生11个TCP连接。
2) 持久连接
非持久连接有些缺点。首先,客户得为每个待请求的对象建立并维护一个新的连接。对于每个这样的连接,TCP得在客户端和服务器端分配TCP缓冲区,并维持TCP变量。对于有可能同时为来自数百个不同客户的请求提供服务的web服务器来说,这会严重增加其负担。其次,如前所述,每个对象都有2个RTT的响应延长——一个RTT用于建立TCP连接,另—个RTT用于请求和接收对象。最后,每个对象都遭受TCP缓启动,因为每个TCP连接都起始于缓启动阶段。不过并行TCP连接的使用能够部分减轻RTT延迟和缓启动延迟的影响。
在持久连接情况下,服务器在发出响应后让TCP连接继续打开着。同一对客户/服务器之间的后续请求和响应可以通过这个连接发送。整个Web页面(上例中为包含一个基本HTMLL文件和10个图像的页面)自不用说可以通过单个持久TCP连接发送:甚至存放在同一个服务器中的多个web页面也可以通过单个持久TCP连接发送。通常,HTTP服务器在某个连接闲置一段特定时间后关闭它,而这段时间通常是可以配置的。持久连接分为不带流水线(withoutpipelining)和带流水线(withpipelining)两个版本。如果是不带流水线的版本,那么客户只在收到前一个请求的响应后才发出新的请求。这种情况下,web页面所引用的每个对象(上例中的10个图像)都经历1个RTT的延迟,用于请求和接收该对象。与非持久连接2个RTT的延迟相比,不带流水线的持久连接已有所改善,不过带流水线的持久连接还能进一步降低响应延迟。不带流水线版本的另一个缺点是,服务器送出一个对象后开始等待下一个请求,而这个新请求却不能马上到达。这段时间服务器资源便闲置了。
HTTP/1.1的默认模式使用带流水线的持久连接。这种情况下,HTTP客户每碰到一个引用就立即发出一个请求,因而HTTP客户可以一个接一个紧挨着发出各个引用对象的请求。服务器收到这些请求后,也可以一个接一个紧挨着发出各个对象。如果所有的请求和响应都是紧挨着发送的,那么所有引用到的对象一共只经历1个RTT的延迟(而不是像不带流水线的版本那样,每个引用到的对象都各有1个RTT的延迟)。另外,带流水线的持久连接中服务器空等请求的时间比较少。与非持久连接相比,持久连接(不论是否带流水线)除降低了1个RTT的响应延迟外,缓启动延迟也比较小。其原因在于既然各个对象使用同一个TCP连接,服务器发出第一个对象后就不必再以一开始的缓慢速率发送后续对象。相反,服务器可以按照第一个对象发送完毕时的速率开始发送下一个对象。
4: 缓存的机制
HTTP/1.1中缓存的目的是为了在很多情况下减少发送请求,同时在许多情况下可以不需要发送完整响应。前者减少了网络回路的数量;HTTP利用一个“过期(expiration)”机制来为此目的。后者减少了网络应用的带宽;HTTP用“验证(validation)”机制来为此目的。具体可以参考:
http://www.chedong.com/tech/cache_docs.html
三 RTSP协议与HTTP协议的联系与区别
RTSP协议负责在服务器和客户端之间建立并控制一个或多个时间上同步的连续流媒体,其目标是象HTTP协议为用户提供文字和图形服务那样为用户提供连续媒体服务。因此,RTSP协议的设计在语法和操作上与HTTP协议很相似,这样,对于HTTP的大部分扩展也适用于RTSP。
但是RTSP协议和HTTP协议在很多方面有着区别:
1. HTTP是一个无状态协议,而RTSP协议是有状态的。
2. HTTP本质上是一个非对称协议,客户端提出请求而服务器响应;而RTSP是对称的,服务器和客户端都可发送和响应请求。
四 HTTPS传输协议
HTTPS(SecureHypertext Transfer Protocol)安全超文本传输协议,它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版。
它是由Netscape开发并内置于其浏览器中,用于对数据进行压缩和解压操作,并返回网络上传送回的结果。HTTPS实际上应用了Netscape的安全全套接字层(SSL)作为HTTP应用层的子层。(HTTPS使用端口443,而不是象HTTP那样使用端口80来和TCP/IP进行通信。)SSL使用40 位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。HTTPS和SSL支持使用X.509数字认证,如果需要的话用户可以确认发送者是谁。
HTTPS和HTTP的区别:
1:http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
2:https协议需要到ca申请证书,一般免费证书很少,需要交费。
3:http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议
4:http的连接很简单,是无状态的,而HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全
HTTPS解决的问题:
1 . 信任主机的问题.采用https 的server 必须从CA 申请一个用于证明服务器用途类型的证书.改证书只有用于对应的server的时候,客户度才信任次主机. 所以目前所有的银行系统网站,关键部分应用都是https 的. 客户通过信任该证书,从而信任了该主机. 其实这样做效率很低,但是银行更侧重安全. 这一点对我们没有任何意义,我们的server ,采用的证书不管自己issue 还是从公众的地方issue, 客户端都是自己人,所以我们也就肯定信任该server.
2 . 通讯过程中的数据的泄密和被窜改
1. 一般意义上的https, 就是 server 有一个证书.
a) 主要目的是保证server 就是他声称的server. 这个跟第一点一样.
b) 服务端和客户端之间的所有通讯,都是加密的.
i. 具体讲,是客户端产生一个对称的密钥,通过server 的证书来交换密钥. 一般意义上的握手过程.
ii. 加下来所有的信息往来就都是加密的.第三方即使截获,也没有任何意义.因为他没有密钥. 当然窜改也就没有什么意义了.
2. 少许对客户端有要求的情况下,会要求客户端也必须有一个证书.
a) 这里客户端证书,其实就类似表示个人信息的时候,除了用户名/密码, 还有一个CA 认证过的身份. 应为个人证书一般来说上别人无法模拟的,所有这样能够更深的确认自己的身份.
b) 目前少数个人银行的专业版是这种做法,具体证书可能是拿U盘作为一个备份的载体.
HTTPS 一定是繁琐的.
a) 本来简单的http协议,一个get一个response. 由于https 要还密钥和确认加密算法的需要.单握手就需要6/7 个往返.
i. 任何应用中,过多的round trip 肯定影响性能.
b) 接下来才是具体的http协议,每一次响应或者请求, 都要求客户端和服务端对会话的内容做加密/解密.
i. 尽管对称加密/解密效率比较高,可是仍然要消耗过多的CPU,为此有专门的SSL 芯片. 如果CPU 信能比较低的话,肯定会降低性能,从而不能serve 更多的请求.
ii. 加密后数据量的影响.所以,才会出现那么多的安全认证提示。
五 SDP协议
SDP会话描述协议:为会话通知、会话邀请和其它形式的多媒体会话初始化等目的提供了多媒体会话描述。会话目录用于协助多媒体会议的通告,并为会话参与者传送相关设置信息。SDP 即用于将这种信息传输到接收端。SDP 完全是一种会话描述格式――它不属于传输协议 ――它只使用不同的适当的传输协议,包括会话通知协议 (SAP) 、会话初始协议(SIP)、实时流协议 (RTSP)、 MIME 扩展协议的电子邮件以及超文本传输协议 (HTTP)。SDP 的设计宗旨是通用性,它可以应用于大范围的网络环境和应用程序,而不仅仅局限于组播会话目录。
SDP是会话描述协议的缩写,是描述流媒体初始化参数的格式,由IETF作为RFC 4566颁布。流媒体是指在传输过程中看到或听到的内容,SDP包通常包括以下信息:
(1)会话信息· 会话名和目的
·会话活动时间
由于参与会话的资源是受限制的,因此包括以下附加信息是非常有用的
· 会话使用的带宽信息
· 会话负责人的联系信息
(2)媒体信息
· 媒体类型,例如视频和音频
· 传输协议,例如RTP/UDP/IP和H.320。
· 多播地址和媒体传输端口(IP多播会话)
· 用于联系地址的媒体和传输端口的远端地址(IP单播会话)
SDP描述由许多文本行组成,文本行的格式为<类型>=<值>,<类型>是一个字母,<值>是结构化的文本串,其格式依<类型>而定。
SDP格式(带*为可选):
Sessiondescription
v= (protocol version) //该行指示协议的版本
o= (owner/creator and session identifier)
例如: o=mhandley 2890844526 2890842807 IN IP4 126.16.64.4 //o行中包含与会话所有者有关的参数(1:第一个参数表明会话发起者的名称,该参数可不填写,如填写和SIP消息中,from消息头的内容一致:2:第二个参数为主叫方的会话标识符:3:第三个参数为主叫方会话的版本,会话数据有改变时,版本号递增:4:第四个参数定义了网络类型,IN表示Internet网络类型,目前仅定义该网络类型:5:第五个参数为地址类型,目前支持IPV4和IPV6两种地址类型:6:第六个参数为地址:表明会话发起者的IP地址,该地址为信令面的IP地址,信令PDP激活时为手机分配。)
s= (session name) //表明本次会话的标题,或会话的名称
i=* (session information)
u=* (URI of description)
e=* (email address)
p=* (phone number)
c=* (connection information - not required if included in all media)
b=* (zero or more bandwidth information lines)
One or more time descriptions ("t=" and "r=" lines, seebelow)
z=* (time zone adjustments)
k=* (encryption key)
a=* (zero or more session attribute lines)
Zero or more media descriptions
Time description
t= (time the session is active)
r=* (zero or more repeat times)
Media description, if present
m= (media name and transport address)
例如:m=audio 3458 RTP/AVP 0 96 97 // m行又称媒体行,描述了发送方所支持的媒体类型等信息(1: 第一个参数为媒体名称:表明支持音频类型。2: 第二个参数为端口号,表明UE在本地端口为3458上发送音频流。3: 第三个参数为传输协议,一般为RTP/AVP协议。4:四-七参数为所支持的四种净荷类型编号)
m=video 3400 RTP/AVP 98 99//m行又称媒体行,描述了发送方所支持的媒体类型等信息
i=* (media title)
c=*(connection information - optional if included at
session-level)
b=* (zero or more bandwidth information lines)
k=* (encryption key)
a=* (zero or more media attribute lines)
参考文档:
http://www.cnblogs.com/tuyile006/archive/2011/02/22/1961679.html
http://www.chedong.com/tech/cache_docs.html