安卓音視頻播放-AwesomePlayer

系列文章:

音視頻播放基礎流程

在講具體的實現(xiàn)之前我們看一下音視頻播放的基礎流程:

1.png

流程很簡單,就是將復用的音視頻流解復用出編碼后的音頻流和編碼后的視頻流。然后通過音頻解碼解出PCM數(shù)據(jù)給音頻設備去播放,通過視頻解碼解出YUV數(shù)據(jù)給視頻設備去播放笔宿。

StagefrightPlayer

上一篇文章有講到MediaPlayerService會通過MediaPlayerFactory創(chuàng)建Player,其中一個創(chuàng)建的就是StagefrightPlayer.但它實際上是一個空殼,只是簡單的調用AwesomePlayer的實現(xiàn)而已:

//StagefrightPlayer.h
class StagefrightPlayer : public MediaPlayerInterface {
    ...
private:
    AwesomePlayer *mPlayer;
    ...
}

//StagefrightPlayer.cpp
status_t StagefrightPlayer::pause() {
    ALOGV("pause");

    return mPlayer->pause();
}

bool StagefrightPlayer::isPlaying() {
    ALOGV("isPlaying");
    return mPlayer->isPlaying();
}

status_t StagefrightPlayer::seekTo(int msec) {
    ALOGV("seekTo %.2f secs", msec / 1E3);

    status_t err = mPlayer->seekTo((int64_t)msec * 1000);

    return err;
}
...

所以我們直接看AwesomePlayer的實現(xiàn)惜索。

多線程架構

音視頻的處理一般都很耗時,所以AwesomePlayer開了一個子線程去工作,防止阻塞住MediaPlayerService的主線程。

具體的架構如下(這幅圖是在這篇博客抄來的,這篇文章寫得的確不錯,大家感興趣可以去仔細讀一下:

2.png

首先AwesomePlayer內部有個TimedEventQueue對象,所有的操作都會封裝成一個個的Event,丟到這個隊列里磅废。然后TimedEventQueue創(chuàng)建了一個子線程,不斷從隊列中拿出Event來執(zhí)行却汉。

例如prepare操作最后會調到prepareAsync_l,這里面就是創(chuàng)建了個Event,通過postEvent丟到隊列里:

status_t AwesomePlayer::prepareAsync_l() {
    ...

    if (!mQueueStarted) {
        mQueue.start();
        mQueueStarted = true;
    }

    ...
    mAsyncPrepareEvent = new AwesomeEvent(
            this, &AwesomePlayer::onPrepareAsyncEvent);

    mQueue.postEvent(mAsyncPrepareEvent);

    return OK;
}

AwesomeEvent繼承TimedEventQueue::Event,實現(xiàn)了fire方法,回調了注冊的方法:

struct AwesomeEvent : public TimedEventQueue::Event {
    AwesomeEvent(
            AwesomePlayer *player,
            void (AwesomePlayer::*method)())
        : mPlayer(player),
          mMethod(method) {
    }
    ...
    virtual void fire(TimedEventQueue *queue, int64_t /* now_us */) {
        (mPlayer->*mMethod)();
    }
    ...
};

TimedEventQueue::start創(chuàng)建了一個子線程,調用TimedEventQueue::threadEntry方法,這里面有個死循環(huán)一直在從Event隊列中拿出Event,執(zhí)行fire方法:

void TimedEventQueue::start() {
    if (mRunning) {
        return;
    }

    mStopped = false;

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

    pthread_create(&mThread, &attr, ThreadWrapper, this);

    pthread_attr_destroy(&attr);

    mRunning = true;
}

void *TimedEventQueue::ThreadWrapper(void *me) {

    androidSetThreadPriority(0, ANDROID_PRIORITY_FOREGROUND);

    static_cast<TimedEventQueue *>(me)->threadEntry();

    return NULL;
}

void TimedEventQueue::threadEntry() {
    ...
    for (;;) {
        ...
        event = removeEventFromQueue_l(eventID);

        if (event != NULL) {
            // Fire event with the lock NOT held.
            event->fire(this, now_us);
        }
    }
}

Demux

我們先來看看prepare回調的時候實際是調用了AwesomePlayer::beginPrepareAsync_l()方法,在這里會實際的去設置數(shù)據(jù)源,然后初始化Demux鸯绿、視頻解碼器和音頻解碼器:

void AwesomePlayer::onPrepareAsyncEvent() {
    Mutex::Autolock autoLock(mLock);
    beginPrepareAsync_l();
}


void AwesomePlayer::beginPrepareAsync_l() {
    ...
    status_t err = finishSetDataSource_l();
    ...
    status_t err = initVideoDecoder();
    ...
    status_t err = initAudioDecoder();
}

先來看看AwesomePlayer::finishSetDataSource_l實際上是為音視頻源找到對應的MediaExtractor,這個MediaExtractor的功能就是實現(xiàn)播放器的基礎流程中的Demux,分解出視頻流和音頻流:

3.png

代碼如下:


status_t AwesomePlayer::finishSetDataSource_l() {
    ...
    extractor = MediaExtractor::Create(dataSource, sniffedMIME.empty() ? NULL : sniffedMIME.c_str());
    ...
    status_t err = setDataSource_l(extractor);
    ...
}


status_t AwesomePlayer::setDataSource_l(const sp<MediaExtractor> &extractor) {
    ...
    for (size_t i = 0; i < extractor->countTracks(); ++i) {
        sp<MetaData> meta = extractor->getTrackMetaData(i);

        const char *_mime;
        CHECK(meta->findCString(kKeyMIMEType, &_mime));

        String8 mime = String8(_mime);
        ...
        if (!haveVideo && !strncasecmp(mime.string(), "video/", 6)) {
            setVideoSource(extractor->getTrack(i));
            ...
        } else if (!haveAudio && !strncasecmp(mime.string(), "audio/", 6)) {
            setAudioSource(extractor->getTrack(i));
            ...
        }
        ...
    }
    ...
}

MediaExtractor::Create的實現(xiàn)也是蠻粗暴的,判斷媒體類型,然后創(chuàng)建不同的MediaExtractor,如MPEG4Extractor满粗、MP3Extractor等:


sp<MediaExtractor> MediaExtractor::Create(const sp<DataSource> &source, const char *mime) {
    ..
    MediaExtractor *ret = NULL;
        if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG4)
                || !strcasecmp(mime, "audio/mp4")) {
            ret = new MPEG4Extractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MPEG)) {
            ret = new MP3Extractor(source, meta);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB)
                || !strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_WB)) {
            ret = new AMRExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)) {
            ret = new FLACExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WAV)) {
            ret = new WAVExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_OGG)) {
            ret = new OggExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MATROSKA)) {
            ret = new MatroskaExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2TS)) {
            ret = new MPEG2TSExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WVM)) {
            // Return now.  WVExtractor should not have the DrmFlag set in the block below.
            return new WVMExtractor(source);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AAC_ADTS)) {
            ret = new AACExtractor(source, meta);
        } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2PS)) {
            ret = new MPEG2PSExtractor(source);
        }
    ...
}
4.png

解碼器

然后AwesomePlayer::initVideoDecoder、AwesomePlayer::initAudioDecoder里面就是調用OMXCodec去做解碼,OMXCodec其實是OpenMax的一層封裝晒喷。OpenMax就是具體的解碼器實現(xiàn)了:


status_t AwesomePlayer::initVideoDecoder(uint32_t flags) {
    ...
    mVideoSource = OMXCodec::Create(
            mClient.interface(), mVideoTrack->getFormat(),
            false, // createEncoder
            mVideoTrack,
            NULL, flags, USE_SURFACE_ALLOC ? mNativeWindow : NULL);
   ...
}


status_t AwesomePlayer::initAudioDecoder() {
    ...
    mOmxSource = OMXCodec::Create(
                mClient.interface(), mAudioTrack->getFormat(),
                false, // createEncoder
                mAudioTrack);
    ...
}

播放流程

應用在java層調用MediaPlayer.start,最終會通過IPC去到MediaPlayerService里調用到StagefrightPlayer::start方法,我們直接從這里開始往下挖:

//從這里開始是StagefrightPlayer.cpp里的代碼
status_t StagefrightPlayer::start() {
    return mPlayer->play();
}

//從這里開始是AwesomePlayer.cpp里的代碼
status_t AwesomePlayer::play() {
    ...
    return play_l();
}

status_t AwesomePlayer::play_l() {
    ...
    createAudioPlayer_l();
    ...
    postVideoEvent_l();
    ...
    return OK;
}

void AwesomePlayer::postVideoEvent_l(int64_t delayUs) {
    ...
    mQueue.postEventWithDelay(mVideoEvent, delayUs < 0 ? 10000 : delayUs);
}

在AwesomePlayer::play_l方法里面調用AwesomePlayer::createAudioPlayer_l創(chuàng)建了一個AudioPlayer,然后調用AwesomePlayer::postVideoEvent_l往mQueue里丟了一個事件孝偎。

還記得這個mVideoEvent嗎?它對應的是AwesomePlayer::onVideoEvent方法,也就是說把這個Event丟到mQueue里面之后AwesomePlayer::onVideoEvent就會在子線程中被調用

mVideoEvent = new AwesomeEvent(this, &AwesomePlayer::onVideoEvent);

讓我們繼續(xù)看看AwesomePlayer::onVideoEvent方法里面干了什么:

void AwesomePlayer::onVideoEvent() {
    ...
    status_t err = mVideoSource->read(&mVideoBuffer, &options);
    ...
    if ((mNativeWindow != NULL)
            && (mVideoRendererIsPreview || mVideoRenderer == NULL)) {
        mVideoRendererIsPreview = false;

        initRenderer_l();
    }
    ...
    if (mAudioPlayer != NULL && !(mFlags & (AUDIO_RUNNING | SEEK_PREVIEW))) {
        startAudioPlayer_l();
    }
    ...
    if (mVideoRenderer != NULL) {
        ...
        mVideoRenderer->render(mVideoBuffer);
        ...
        }
    ...
    postVideoEvent_l();
}

這個方法最重要的就是創(chuàng)建一個VideoRender,從mVideoSource讀取解碼好的視頻幀去渲染,渲染完之后再調AwesomePlayer::postVideoEvent_l再往隊列丟入一個VideoEvent。于是畫面就不斷的刷新了厨埋。

可以看到,這個方法內部也啟動了音頻播放器去播放音頻邪媳。而且其實它還做了一些音視頻同步的工作,但是考慮到邏輯比較啰嗦,我這里就省略了。

VideoRender

最后讓我們來看看VideoRendere是怎么來的

void AwesomePlayer::initRenderer_l() {
    ...
    if (USE_SURFACE_ALLOC
            && !strncmp(component, "OMX.", 4)
            && strncmp(component, "OMX.google.", 11)
            && strcmp(component, "OMX.Nvidia.mpeg2v.decode")) {
        mVideoRenderer =
            new AwesomeNativeWindowRenderer(mNativeWindow, rotationDegrees);
    } else {
        mVideoRenderer = new AwesomeLocalRenderer(mNativeWindow, meta);
    }
}

可以看到,是根據(jù)解碼器類型用mNativeWindow創(chuàng)建了不同的AwesomeNativeWindowRenderer或者AwesomeLocalRenderer荡陷。這個mNativeWindow就是畫面最終需要渲染到的地方

我們看看mNativeWindow是怎么來的:

// AwesomePlayer.cpp
status_t AwesomePlayer::setNativeWindow_l(const sp<ANativeWindow> &native) {
    mNativeWindow = native;
    ...
}

status_t AwesomePlayer::setSurfaceTexture(const sp<IGraphicBufferProducer> &bufferProducer) {
   ...
   err = setNativeWindow_l(new Surface(bufferProducer));
   ...
}

//StagefrightPlayer.cpp
status_t StagefrightPlayer::setVideoSurfaceTexture(
        const sp<IGraphicBufferProducer> &bufferProducer) {
    ALOGV("setVideoSurfaceTexture");

    return mPlayer->setSurfaceTexture(bufferProducer);
}

//MediaPlayerService.cpp
status_t MediaPlayerService::Client::setVideoSurfaceTexture(
    ...
    sp<MediaPlayerBase> p = getPlayer();
    ...
    status_t err = p->setVideoSurfaceTexture(bufferProducer);
    ...
}

//MediaPlayer.cpp
status_t MediaPlayer::setVideoSurfaceTexture(
        const sp<IGraphicBufferProducer>& bufferProducer)
{
    ...
    return mPlayer->setVideoSurfaceTexture(bufferProducer);
}


//android_media_MediaPlayer.cpp
static void setVideoSurface(JNIEnv *env, jobject thiz, jobject jsurface, jboolean mediaPlayerMustBeAlive)
{
    sp<MediaPlayer> mp = getMediaPlayer(env, thiz);
    ...
    sp<Surface> surface(android_view_Surface_getSurface(env, jsurface));
    ...
    new_st = surface->getIGraphicBufferProducer();
    ...
    mp->setVideoSurfaceTexture(new_st);
}

static void android_media_MediaPlayer_setVideoSurface(JNIEnv *env, jobject thiz, jobject jsurface)
{
    setVideoSurface(env, thiz, jsurface, true /* mediaPlayerMustBeAlive */);
}

//android.media.MediaPlayer.java
public class MediaPlayer extends PlayerBase
                         implements SubtitleController.Listener
                                  , VolumeAutomation
                                  , AudioRouting
{
    ...
    private native void _setVideoSurface(Surface surface);
    ...
    public void setDisplay(SurfaceHolder sh) {
        mSurfaceHolder = sh;
        Surface surface;
        if (sh != null) {
            surface = sh.getSurface();
        } else {
            surface = null;
        }
        _setVideoSurface(surface);
        updateSurfaceScreenOn();
    }
    ...
}

可以看到,VideoRendere最終是根據(jù)MediaPlayer.setDisplay這個方法設置的SurfaceHolder創(chuàng)建的到的雨效。這就解釋了畫面是怎么渲染到指定的SurfaceView上的。

完整架構圖

整個渲染的架構如下:

5.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末废赞,一起剝皮案震驚了整個濱河市徽龟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唉地,老刑警劉巖据悔,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耘沼,居然都是意外死亡极颓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門群嗤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來菠隆,“玉大人,你說我怎么就攤上這事狂秘『Ь叮” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵者春,是天一觀的道長破衔。 經常有香客問我,道長钱烟,這世上最難降的妖魔是什么晰筛? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮拴袭,結果婚禮上读第,老公的妹妹穿的比我還像新娘。我一直安慰自己稻扬,他們只是感情好,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布羊瘩。 她就那樣靜靜地躺著泰佳,像睡著了一般盼砍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逝她,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天浇坐,我揣著相機與錄音,去河邊找鬼黔宛。 笑死近刘,一個胖子當著我的面吹牛,可吹牛的內容都是我干的臀晃。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼苇经!你這毒婦竟也來了仓洼?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤险绘,失蹤者是張志新(化名)和其女友劉穎踢京,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宦棺,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡瓣距,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了代咸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹈丸。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖侣背,靈堂內的尸體忽然破棺而出白华,到底是詐尸還是另有隱情,我是刑警寧澤贩耐,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布弧腥,位于F島的核電站,受9級特大地震影響潮太,放射性物質發(fā)生泄漏管搪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一铡买、第九天 我趴在偏房一處隱蔽的房頂上張望更鲁。 院中可真熱鬧,春花似錦奇钞、人聲如沸澡为。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽媒至。三九已至顶别,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拒啰,已是汗流浹背驯绎。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谋旦,地道東北人剩失。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像册着,于是被迫代替她去往敵國和親拴孤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容