Android 13 - Media框架(6)- NuPlayer::Source

Source 在播放器中起着拉流(Streaming)和解复用(demux)的作用,Source 设计的好坏直接影响到播放器的基础功能,我们这一节将会了解 NuPlayer 中的通用 Source(GenericSource)关注本地播放架构,直播流暂时先不研究。

1、NuPlayer::Source

NuPlayer::Source 是一个抽象类,定义了 Source 实现所需要的基本接口,例如 prepareAsync,start,dequeueAccessUnit等,除此之外还包含了一些共有的方法,例如 Callback发送 等。

android 为我们提供了5种 Source 实现,分别为:

  • HTTPLiveSource:url 为 m3u8 结尾的 http 链接时,NuPlayer 将创建 HTTPLiveSource;
  • RTSPSource:url 为 rtsp 开头,或者以sdp结尾时,将创建 RTSPSource;
  • RTPSource
  • GenericSource:通用 Source,当 url 不符合以上创建条件时会创建该 Source,一般用于本地播放;
  • StreamingSource:NuPlayer 的 setDataSource 提供了一个以 IStreamSource 为参数的版本,意为由上层自己实现 Source,参数 IStreamSource 将被封装在 StreamingSource 中供 NuPlayer 使用。如果我们想在 native 自定义 Source,可以实现 IStreamSource 接口,然后调用这个版本的 setDataSource 方法。

NuPlayer::Source 提供有如下基本播放控制接口,具体实现可根据需求覆写这些实现:

    virtual void prepareAsync() = 0;
    virtual void start() = 0;
    virtual void stop() {}
    virtual void pause() {}
    virtual void resume() {}
    virtual void disconnect() {}
    virtual status_t feedMoreTSData() = 0;
    virtual status_t dequeueAccessUnit(bool audio, sp<ABuffer> *accessUnit) = 0;
    virtual status_t seekTo(int64_t /* seekTimeUs */,MediaPlayerSeekMode /* mode */ = MediaPlayerSeekMode::SEEK_PREVIOUS_SYNC) ;

NuPlayer::Source 提供有名为 Flags 的枚举类型,Flags 标记了当前播放码流所支持的操作,执行完 prepareAsync 后会将 Flags 信息上抛,最终打开或关闭上层的一些功能:

    enum Flags {
        FLAG_CAN_PAUSE          = 1,
        FLAG_CAN_SEEK_BACKWARD  = 2,  // the "10 sec back button"
        FLAG_CAN_SEEK_FORWARD   = 4,  // the "10 sec forward button"
        FLAG_CAN_SEEK           = 8,  // the "seek bar"
        FLAG_DYNAMIC_DURATION   = 16,
        FLAG_SECURE             = 32, // Secure codec is required.
        FLAG_PROTECTED          = 64, // The screen needs to be protected (screenshot is disabled).
    };

Source 运行过程中可能会有如下事件上抛给 NuPlayer,上抛所调用的函数实现在 NuPlayer.cpp 中:

  • kWhatPrepared:Source prepare 完成通知 NuPlayer;
  • kWhatFlagsChanged:prepare 过程中获取的码流支持的操作,prepare 过程中上抛给 NuPlayer;
  • kWhatVideoSizeChanged:prepare 过程中获取的码流宽高等信息,prepare 过程中上抛给 NuPlayer;
  • kWhatBufferingUpdate:上抛当前 buffering 的百分比;
  • kWhatPauseOnBufferingStart:上抛 buffering 开始事件;
  • kWhatResumeOnBufferingEnd:上抛 buffering 结束事件;
  • kWhatCacheStats:上抛当前的缓存带宽信息;
  • kWhatInstantiateSecureDecoders:上抛信息创建 secure decoder;

2、GenericSource

GenericSource 名为通用 source,但是往往它会被用来当作本地播放的 Source,由于本地播放的码流文件会有形形色色的封装格式,所以这个 source 会依赖解封装(demux)服务 media.extractor。除了依赖解封装外,source 还需要一个 IO 来读取码流,这个 IO 被封装在 DataSource中。Source、DataSource、Extractor三者的关系如下:

请添加图片描述

2.1、prepareAsync

void NuPlayer::GenericSource::prepareAsync() {
    Mutex::Autolock _l(mLock);
    ALOGV("prepareAsync: (looper: %d)", (mLooper != NULL));

    if (mLooper == NULL) {
        mLooper = new ALooper;
        mLooper->setName("generic");
        mLooper->start();

        mLooper->registerHandler(this);
    }

    sp<AMessage> msg = new AMessage(kWhatPrepareAsync, this);
    msg->post();
}

GenericSource 的 ALooper 包含在它的类内部,在 prepareAsync 中完成创建和注册。prepareAsync 的处理比较长,这里对代码进行精简阅读:

void NuPlayer::GenericSource::onPrepareAsync() {
    mDisconnectLock.lock();
    // delayed data source creation
    if (mDataSource == NULL) {
        mIsSecure = false;

        if (!mUri.empty()) {
            const char* uri = mUri.c_str();
            String8 contentType;
			// ......
          	// 1. 使用 DataSource 工厂创建一个 datasource
            sp<DataSource> dataSource = PlayerServiceDataSourceFactory::getInstance()
                    ->CreateFromURI(mHTTPService, uri, &mUriHeaders, &contentType,
                            static_cast<HTTPBase *>(mHttpSource.get()));
			 // ......
            if (!mDisconnected) {
                mDataSource = dataSource;
            }
        } 
        // ......
    }

	// 这里的 mIsStreaming 表示当前 Source 是不是网络串流的
    if (mDataSource->flags() & DataSource::kIsCachingDataSource) {
        mCachedSource = static_cast<NuCachedSource2 *>(mDataSource.get());
    }
    // For cached streaming cases, we need to wait for enough
    // buffering before reporting prepared.
    mIsStreaming = (mCachedSource != NULL);

	// 2、使用 DataSource 创建 Extractor
    // init extractor from data source
    status_t err = initFromDataSource();

	// 3、获取 Extractor 解析到的 video track 信息
    if (mVideoTrack.mSource != NULL) {
        sp<MetaData> meta = getFormatMeta_l(false /* audio */);
        sp<AMessage> msg = new AMessage;
        err = convertMetaDataToMessage(meta, &msg);
        if(err != OK) {
            notifyPreparedAndCleanup(err);
            return;
        }
        notifyVideoSizeChanged(msg);
    }

	// 4、将source flag 上抛
    notifyFlagsChanged(
            // FLAG_SECURE will be known if/when prepareDrm is called by the app
            // FLAG_PROTECTED will be known if/when prepareDrm is called by the app
            FLAG_CAN_PAUSE |
            FLAG_CAN_SEEK_BACKWARD |
            FLAG_CAN_SEEK_FORWARD |
            FLAG_CAN_SEEK);
	// 5、将prepareAsync完成消息上抛
    finishPrepareAsync();
}

onPrepareAsync 主要做了如下几件事情:

  1. 使用 DataSource 工厂创建合适的 datasource;
  2. 调用 media.extractor 服务,将 DataSource 作为参数传入,创建 Extractor;
  3. 获取 Extractor 解析到的 video track 信息,将信息上抛;
  4. 将source flag 上抛;
  5. 将prepareAsync完成消息上抛。
status_t NuPlayer::GenericSource::initFromDataSource() {
    sp<IMediaExtractor> extractor;
    sp<DataSource> dataSource;
    {
        Mutex::Autolock _l_d(mDisconnectLock);
        dataSource = mDataSource;
    }
    
    // 1、创建 IExtractor
    // This might take long time if data source is not reliable.
    extractor = MediaExtractorFactory::Create(dataSource, NULL);
	// 2、获取码流信息,包含码流时长等信息
    sp<MetaData> fileMeta = extractor->getMetaData();
	// 3、获取 track 数量
    size_t numtracks = extractor->countTracks();

    mFileMeta = fileMeta;
    if (mFileMeta != NULL) {
        int64_t duration;
        if (mFileMeta->findInt64(kKeyDuration, &duration)) {
            mDurationUs = duration;
        }
    }

    int32_t totalBitrate = 0;
    mMimes.clear();

	// 4、遍历所有的 track
    for (size_t i = 0; i < numtracks; ++i) {
        sp<IMediaSource> track = extractor->getTrack(i);
        if (track == NULL) {
            continue;
        }
		// 获取 track 的信息,包含 mime type
        sp<MetaData> meta = extractor->getTrackMetaData(i);

        const char *mime;
        CHECK(meta->findCString(kKeyMIMEType, &mime));
		// 创建一个 Track 来封装获取到的 track source,以及 track buffer pool
        if (!strncasecmp(mime, "audio/", 6)) {
            if (mAudioTrack.mSource == NULL) {
                mAudioTrack.mIndex = i;
                mAudioTrack.mSource = track;
                mAudioTrack.mPackets =
                    new AnotherPacketSource(mAudioTrack.mSource->getFormat());

                if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_VORBIS)) {
                    mAudioIsVorbis = true;
                } else {
                    mAudioIsVorbis = false;
                }
				// 将 mime 加入到 vector 中
                mMimes.add(String8(mime));
            }
        } else if (!strncasecmp(mime, "video/", 6)) {
            if (mVideoTrack.mSource == NULL) {
                mVideoTrack.mIndex = i;
                mVideoTrack.mSource = track;
                mVideoTrack.mPackets =
                    new AnotherPacketSource(mVideoTrack.mSource->getFormat());
                // 将 video mime 放在容器第一个
                mMimes.insertAt(String8(mime), 0);
            }
        }
		// 将 track source 存储到 vector 中
        mSources.push(track);
        int64_t durationUs;
        if (meta->findInt64(kKeyDuration, &durationUs)) {
            if (durationUs > mDurationUs) {
                mDurationUs = durationUs;
            }
        }
		// 获取 bitrate
        int32_t bitrate;
        if (totalBitrate >= 0 && meta->findInt32(kKeyBitRate, &bitrate)) {
            totalBitrate += bitrate;
        } else {
            totalBitrate = -1;
        }
    }
    
    if (mSources.size() == 0) {
        ALOGE("b/23705695");
        return UNKNOWN_ERROR;
    }
    mBitrate = totalBitrate;
    return OK;
}

initFromDataSource 的关键是调用 media.extractor 服务创建 IMediaExtractor 对象来解析 DataSource 读取到的内容。IMediaExtractor 创建完成,解析过程也就完成了,使用 extractor 可以获取到流的基本信息,track数量,创建 IMediaSource 对象。

  1. 使用 DataSource 创建 IMediaExtractor;
  2. 获取 Extractor MetaData,时长信息、track count、track metadata 这类流信息是存储的 extractor 中的,所以需要从 Extractor 中获取到;
  3. 使用 Extractor 为 每个 track 创建 IMediaSource,并存储在 vector 中,后续读取指定 track 的数据通过该 IMediaSource 来完成;
  4. 将 IMediaSource,track id,以及一个 BufferPool 组织成一个 Track 结构体,后续调用就使用该结构体。
void NuPlayer::GenericSource::finishPrepareAsync() {
    ALOGV("finishPrepareAsync");

    status_t err = startSources();
    if (err != OK) {
        ALOGE("Failed to init start data source!");
        notifyPreparedAndCleanup(err);
        return;
    }

    if (mIsStreaming) {
        mCachedSource->resumeFetchingIfNecessary();
        mPreparing = true;
        schedulePollBuffering();
    } else {
        notifyPrepared();
    }

    if (mAudioTrack.mSource != NULL) {
        postReadBuffer(MEDIA_TRACK_TYPE_AUDIO);
    }

    if (mVideoTrack.mSource != NULL) {
        postReadBuffer(MEDIA_TRACK_TYPE_VIDEO);
    }
}

prepareAsync 的最后会调用 finishPrepareAsync,这里首先会调用已选择的 IMediaSource 的 start 方法,接着上抛 prepare 完成的消息,最后调用 postReadBuffer,让 GenericSource 开始使用 IMediaSource 读取 demux 后的数据。

2.2、数据读取

上面说到调用 postReadBuffer 读取数据,GenericSource 只有一个 ALooper 线程,所以不能同时读取音频和视频数据,那我们应该按什么顺序读取呢?

void NuPlayer::GenericSource::postReadBuffer(media_track_type trackType) {
    if ((mPendingReadBufferTypes & (1 << trackType)) == 0) {
        mPendingReadBufferTypes |= (1 << trackType);
        sp<AMessage> msg = new AMessage(kWhatReadBuffer, this);
        msg->setInt32("trackType", trackType);
        msg->post();
    }
}

postReadBuffer 用成员 mPendingReadBufferTypes 来管理当前应该读取的 track。调用 postReadBuffer 后函数会检查 mPendingReadBufferTypes 对应 trackType 位置的值是否为 0 时,如果是就去读取该 track 的码流,如果不是说明当前正在读取该 track 的码流,不对当前调用做任何操作。当前 track 的数据读取完毕,mPendingReadBufferTypes 对应位置的码流会被重置为 0。

我觉得使用以上机制有两种作用:

  • 可以尽量保证在碰到需要同时读取 audio 和 video 数据的情况时,两种数据都能够读取到,数据次数相对均匀;
  • 可以保证消息队列中同时只有一条读取 audio 和 video 数据的 Message,避免其他消息无法被处理。

接下来看 readBuffer 是如何读取数据的,同上面一样,这里会删减掉 Streaming 部分的代码:

void NuPlayer::GenericSource::readBuffer(
        media_track_type trackType, int64_t seekTimeUs, MediaPlayerSeekMode mode,
        int64_t *actualTimeUs, bool formatChange) {
    Track *track;
    size_t maxBuffers = 1;
    // 1、根据 tracktype 获取一次要读取的buffer的数量
    switch (trackType) {
        case MEDIA_TRACK_TYPE_VIDEO:
            track = &mVideoTrack;
            maxBuffers = 8;  // too large of a number may influence seeks
            break;
        case MEDIA_TRACK_TYPE_AUDIO:
            track = &mAudioTrack;
            maxBuffers = 64;
            break;
        case MEDIA_TRACK_TYPE_SUBTITLE:
            track = &mSubtitleTrack;
            break;
        case MEDIA_TRACK_TYPE_TIMEDTEXT:
            track = &mTimedTextTrack;
            break;
        default:
            TRESPASS();
    }

	// 设定本次读取的位置
    if (actualTimeUs) {
        *actualTimeUs = seekTimeUs;
    }

    MediaSource::ReadOptions options;

    bool seeking = false;
    if (seekTimeUs >= 0) {
        options.setSeekTo(seekTimeUs, mode);
        seeking = true;
    }
	// 是否支持一次读取多包数据,默认是支持的
    const bool couldReadMultiple = (track->mSource->supportReadMultiple());

    if (couldReadMultiple) {
        options.setNonBlocking();
    }
	// 2、获取当前 track 的 generation
    int32_t generation = getDataGeneration(trackType);
    // 3、循环读取,直到读到指定数量的 buffer
    for (size_t numBuffers = 0; numBuffers < maxBuffers; ) {
        Vector<MediaBufferBase *> mediaBuffers;
        status_t err = NO_ERROR;

        sp<IMediaSource> source = track->mSource;
        mLock.unlock();
        // 4、读取buffer
        if (couldReadMultiple) {
            err = source->readMultiple(
                    &mediaBuffers, maxBuffers - numBuffers, &options);
        } 
        mLock.lock();

        options.clearNonPersistent();

        size_t id = 0;
        size_t count = mediaBuffers.size();
		// 5、检查 generation 的值,如果不同于之前的 generation 那么就销毁所有数据并退出数据读取
        // in case track has been changed since we don't have lock for some time.
        if (generation != getDataGeneration(trackType)) {
            for (; id < count; ++id) {
                mediaBuffers[id]->release();
            }
            break;
        }
		// 解析读到的每一个buffer
        for (; id < count; ++id) {
            int64_t timeUs;
            MediaBufferBase *mbuf = mediaBuffers[id];
            // 6、查找 buffer 中的 pts 信息
            if (!mbuf->meta_data().findInt64(kKeyTime, &timeUs)) {
                mbuf->meta_data().dumpToLog();
                track->mPackets->signalEOS(ERROR_MALFORMED);
                break;
            }
            if (trackType == MEDIA_TRACK_TYPE_AUDIO) {
                mAudioTimeUs = timeUs;
            } else if (trackType == MEDIA_TRACK_TYPE_VIDEO) {
                mVideoTimeUs = timeUs;
            }
			// 7、将一个不连续的标志位加入到 buffer pool 中
            queueDiscontinuityIfNeeded(seeking, formatChange, trackType, track);
			// 8、将数据转换为 ABuffer
            sp<ABuffer> buffer = mediaBufferToABuffer(mbuf, trackType);
            if (numBuffers == 0 && actualTimeUs != nullptr) {
                *actualTimeUs = timeUs;
            }
            // 9、给buffer附上一些额外信息
            if (seeking && buffer != nullptr) {
                sp<AMessage> meta = buffer->meta();
                if (meta != nullptr && mode == MediaPlayerSeekMode::SEEK_CLOSEST
                        && seekTimeUs > timeUs) {
                    sp<AMessage> extra = new AMessage;
                    extra->setInt64("resume-at-mediaTimeUs", seekTimeUs);
                    meta->setMessage("extra", extra);
                }
            }
			// 10、将buffer加入到对应 track 的buffer pool 中
            track->mPackets->queueAccessUnit(buffer);
            formatChange = false;
            seeking = false;
            ++numBuffers;
        }
        // 11、销毁没有被解析的 buffer 
        if (id < count) {
            // Error, some mediaBuffer doesn't have kKeyTime.
            for (; id < count; ++id) {
                mediaBuffers[id]->release();
            }
            break;
        }

		// 12、当返回值为WOULD_BLOCK时,退出当前track的读取
        if (err == WOULD_BLOCK) {
            break;
        } else if (err == INFO_FORMAT_CHANGED) {
        } else if (err != OK) {
            queueDiscontinuityIfNeeded(seeking, formatChange, trackType, track);
            track->mPackets->signalEOS(err);
            break;
        }
    }
}
  1. 根据 tracktype 决定一次要读取的buffer的数量,video buffer size 可能比较大,所以读取的数量比较小,audio 的情况反之;
  2. 如果需要seek,那么读取的时候需要带下去 seek 信息;
  3. 开始读取之前先获取当前的 generation 信息,generation 的作用是标记当前执行的操作是否已经过时了(对于读取 audio 和 video 是用不到的);
  4. 循环读取,直到读到指定数量的 buffer,这里要注意读取的时候是没有加锁的
    • 检查 generation 的值,如果不同于之前的 generation 那么就销毁所有数据并退出数据读取(对于读取 audio 和 video 是用不到的);
    • 重新选择了 track(会附带 seek动作),向 buffer pool 添加一个不连续信息;
    • 解析每一个 buffer,读取 pts 信息,将 buffer 以 ABuffer 的形式存储,加入到 buffer pool;
    • 销毁未被解析的 buffer(解析过的 buffer 会直接释放);
    • 当返回值为WOULD_BLOCK时,退出当前 track 的读取。

由于读取是一个比较耗时的工作,可能会影响其他 cmd 的执行,所以这里设计在读取过程中没有给 IMediaSource 加锁,我们可以控制 IMediaSource 让它中断读取,返回 WOULD_BLOCK 之类的异常。

Android Media 有很多地方用了 generation 技巧,例如在 Renderer、ACodec 中也用到了。它的作用我认为有两点:

  1. 方便 debug;
  2. 在任务执行过程中检查是否有其他更高优先级的任务需要执行,优先级高的任务会直接修改 generation 的数值,从而中断当前任务的执行。

selectTrack 可能会改变当前正在播放的 mime type,decoder 需要被释放再重新创建,所以需要在 Buffer Pool 中添加一条 Discontinuity 信息,用来侦测当前写入 decoder 的数据是否已经到达 selectTrack 后读取的位置。

所有读到的 buffer 将会拷贝到 ABuffer 当中,ABuffer 存储有buffer data、buffer length、metadata 以及 pts 等信息,这些都是向 decoder 中写入时所需要的。

2.3、start

调用 start 后 GenericSource 会分别去读取一次 audio 和 video data,如上一小节所述,如果当前正在读取,那这里的 postReadBuffer 将不会生效。

start 更重要的是将 mStarted 标志置为 true,有了它才能从 GenericSource 获取数据向 decoder 写入。

void NuPlayer::GenericSource::start() {
    Mutex::Autolock _l(mLock);
    ALOGI("start");

    if (mAudioTrack.mSource != NULL) {
        postReadBuffer(MEDIA_TRACK_TYPE_AUDIO);
    }

    if (mVideoTrack.mSource != NULL) {
        postReadBuffer(MEDIA_TRACK_TYPE_VIDEO);
    }

    mStarted = true;
}

2.4、pause、stop、resume

这里将 pause、stop 和 resume 放在一起,他们核心的作用是修改 mStarted 的值,从而控制是否能从 GenericSource 拿到 demux 后的数据。

void NuPlayer::GenericSource::stop() {
    Mutex::Autolock _l(mLock);
    mStarted = false;
}

void NuPlayer::GenericSource::pause() {
    Mutex::Autolock _l(mLock);
    mStarted = false;
}

void NuPlayer::GenericSource::resume() {
    Mutex::Autolock _l(mLock);
    mStarted = true;
}

2.5、disconnect

disconnect 主要是为了网络连接所设计的,里面调用的是 Streaming Source 的断开方法。

void NuPlayer::GenericSource::disconnect() {
    sp<DataSource> dataSource, httpSource;
    {
        Mutex::Autolock _l_d(mDisconnectLock);
        dataSource = mDataSource;
        httpSource = mHttpSource;
        mDisconnected = true;
    }

    if (dataSource != NULL) {
        // disconnect data source
        if (dataSource->flags() & DataSource::kIsCachingDataSource) {
            static_cast<NuCachedSource2 *>(dataSource.get())->disconnect();
        }
    } else if (httpSource != NULL) {
        static_cast<HTTPBase *>(httpSource.get())->disconnect();
    }
}

2.6、seekTo

调用 seek 方法会等当前读取的工作完成,自己以 seekTime,seekMode 作为参数,去调用 readBuffer 分别读取一次 audio/video data:

status_t NuPlayer::GenericSource::doSeek(int64_t seekTimeUs, MediaPlayerSeekMode mode) {
    if (mVideoTrack.mSource != NULL) {
        ++mVideoDataGeneration;
        int64_t actualTimeUs;
        readBuffer(MEDIA_TRACK_TYPE_VIDEO, seekTimeUs, mode, &actualTimeUs);
        if (mode != MediaPlayerSeekMode::SEEK_CLOSEST) {
            seekTimeUs = std::max<int64_t>(0, actualTimeUs);
        }
        mVideoLastDequeueTimeUs = actualTimeUs;
    }

    if (mAudioTrack.mSource != NULL) {
        ++mAudioDataGeneration;
        readBuffer(MEDIA_TRACK_TYPE_AUDIO, seekTimeUs, MediaPlayerSeekMode::SEEK_CLOSEST);
        mAudioLastDequeueTimeUs = seekTimeUs;
    }
    return OK;
}

读取流程和正常流程大致相同,但是不同的是会调用到 AnotherPacketSource.queueDiscontinuity,这里很好理解,seek 后需要丢弃 buffer pool 里之前的所有数据。queueDiscontinuity 就是在向 bufferPool 写入数据时添加 flag,从而实现清空之前的数据的目的。

void NuPlayer::GenericSource::queueDiscontinuityIfNeeded(
        bool seeking, bool formatChange, media_track_type trackType, Track *track) {
    if ((seeking || formatChange)
            && (trackType == MEDIA_TRACK_TYPE_AUDIO
            || trackType == MEDIA_TRACK_TYPE_VIDEO)) {
        ATSParser::DiscontinuityType type = (formatChange && seeking)
                ? ATSParser::DISCONTINUITY_FORMATCHANGE
                : ATSParser::DISCONTINUITY_NONE;
        track->mPackets->queueDiscontinuity(type, NULL /* extra */, true /* discard */);
    }
}

2.7、dequeueAccessUnit

GenericSource 读取到的 demux 后的数据都存储在 AnotherPacketSource 这个buffer pool 中,decoder 调用 dequeueAccessUnit 实际就时从 AnotherPacketSource 获取 buffer。

status_t NuPlayer::GenericSource::dequeueAccessUnit(
        bool audio, sp<ABuffer> *accessUnit) {
    Mutex::Autolock _l(mLock);
	// 1、如果 start 为 false 直接退出
    if (!mStarted && mIsDrmReleased) {
        return -EWOULDBLOCK;
    }

    Track *track = audio ? &mAudioTrack : &mVideoTrack;

    if (track->mSource == NULL) {
        return -EWOULDBLOCK;
    }
	// 2、判断是 bufferpool 是否为空,如果为空则尝试读取并直接退出
    status_t finalResult;
    if (!track->mPackets->hasBufferAvailable(&finalResult)) {
        if (finalResult == OK) {
            postReadBuffer(
                    audio ? MEDIA_TRACK_TYPE_AUDIO : MEDIA_TRACK_TYPE_VIDEO);
            return -EWOULDBLOCK;
        }
        return finalResult;
    }
	// 3、从 bufferpool 中 dequeue buffer,(阻塞等待)
    status_t result = track->mPackets->dequeueAccessUnit(accessUnit);
	// 4、检查 bufferpool 中的数据,如果不够了就尝试读取
    if (!mIsStreaming) {
        if (track->mPackets->getAvailableBufferCount(&finalResult) < 2) {
            postReadBuffer(audio? MEDIA_TRACK_TYPE_AUDIO : MEDIA_TRACK_TYPE_VIDEO);
        }
    }
    return result;
}
  1. 如果 start 为 false 直接退出;
  2. 判断是 bufferpool 是否为空,如果为空则尝试读取并直接退出;
  3. 从 bufferpool 中 dequeue buffer,如果看 AnotherPacketSource 的源码会发现,如果没有buffer了,AnotherPacketSource.dequeueAccessUnit 会阻塞等待,但是在这之前已经判断了是否为空,所以这里并不会出现阻塞的情况;
  4. 检查 bufferpool 中的数据量,如果不够了就尝试读取。

最后要看读取事件驱动的问题,我们可以发现执行 prepare、start、seek、selectTrack 时都会调用一次 postReadBuffer,难道执行一次 read 就没下文了吗?

看了 dequeueAccessUnit 我们就可以知道,本地文件的读取是依赖 decoder 的需求的,decoder 要多少就读多少;而 streaming 是不一样的,它有一个自己 post 自己的过程,从而实现自身不断去拉流的效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/88449.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode】224. 基本计算器

224. 基本计算器&#xff08;困难&#xff09; 方法&#xff1a;双栈解法 思路 我们可以使用两个栈 nums 和 ops 。 nums &#xff1a; 存放所有的数字ops &#xff1a;存放所有的数字以外的操作&#xff0c;/- 也看做是一种操作 然后从前往后做&#xff0c;对遍历到的字符做…

万字精讲——数据结构栈与队列必会OJ练习

W...Y的主页 &#x1f495; 代码库分享 &#x1f60a; 在之前的博客中&#xff0c;我们学习了栈与队列的基本内容&#xff0c;并且实现了栈与队列。今天我们进行刷题训练&#xff0c;走进栈与队列的世界中去感受一番&#xff01;&#xff01;&#xff01; 目录 括号匹配问题…

redis 7高级篇1 redis的单线程与多线程

一 redis单线程与多线程 1.1 redis单线程&多线程 1.redis的单线程 redis单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的&#xff0c;Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理…

【VsCode】SSH远程连接Linux服务器开发,搭配cpolar内网穿透实现公网访问(1)

文章目录 前言1、安装OpenSSH2、vscode配置ssh3. 局域网测试连接远程服务器4. 公网远程连接4.1 ubuntu安装cpolar内网穿透4.2 创建隧道映射4.3 测试公网远程连接 5. 配置固定TCP端口地址5.1 保留一个固定TCP端口地址5.2 配置固定TCP端口地址5.3 测试固定公网地址远程 前言 远程…

synchronized锁升级

在 Java SE 1.6中&#xff0c; 锁 一共有 4 种状 态 &#xff0c; 级别 从低到高依次是&#xff1a;无 锁 状 态 、偏向 锁 状 态 、 轻 量 级锁 状 态和重量 级锁 状 态 &#xff0c; 这 几个状 态 会随着 竞 争情况逐 渐 升 级 。 锁 可以升 级 但不能降 级 &#xff0c;意味…

mysql的登录与退出

mysql是c/s架构&#xff0c;意味着同时要有客户端和服务端 1 找到客户端。mysql.exe的安装目录 打开命令行 2 输入对应的服务器的ip&#xff0c;如果是本地&#xff0c;就是Localhost&#xff0c;如果是远程服务器&#xff0c;那就输入对应ip/域名。并且指定mysql监听的端口 …

workbench连接MySQL8.0错误 bad conversion 外部组件 异常

阿里云搭建MySQL实用的版本是8.0 本地安装的版本是: workbench 6.3 需要升级到&#xff1a; workbench 8.0 https://dev.mysql.com/downloads/workbench/

elment-ui中使用el-steps案例

el-steps案例 样式 代码 <div class"active-box"><div class"active-title">请完善</div><el-steps :active"active" finish-status"success" align-center><el-step title"第一步" /><…

SpringCloud入门实战(十四)Sentinel微服务流量防卫兵简介

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 &#xff0c;关注我&#xff0c;不迷路 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术…

2023年高教社杯数学建模思路 - 复盘:光照强度计算的优化模型

文章目录 0 赛题思路1 问题要求2 假设约定3 符号约定4 建立模型5 模型求解6 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 问题要求 现在已知一个教室长为15米&#xff0c;宽为12米&…

Android相机-HAL-Rockchip-hal3

引言&#xff1a; 对于Android相机的 HAL层而言对上实现一套Framework的API接口&#xff0c;对下通过V4L2框架实现与kernel的交互。不同的平台会有不同的实现方案。主要是对Android HAL3的接口的实现。看看rockchip是怎么支持hal3的&#xff1f; 代码目录&#xff1a; hardw…

中文分词和tfidf特征应用

文章目录 引言1. NLP 的基础任务 --分词2. 中文分词2.1 中文分词-难点2.2 中文分词-正向最大匹配2.2.1 实现方式一2.2.2 实现方式二 利用前缀字典 2.3 中文分词-反向最大匹配2.4 中文分词-双向最大匹配2.5 中文分词-jieba分词2.5.1 基本用法2.5.2 分词模式2.5.3 其他功能 2.6 三…

【Java】基础练习(十一)

1.Poker 定义两个数组&#xff0c;一个数组存储扑克牌花色&#xff0c;另一个数组存储扑克牌&#xff08;A~K&#xff09;&#xff0c;输出52张扑克牌&#xff08;除大小王&#xff09; ♥A、♥2...&#xff08;1&#xff09;Poker类&#xff1a; package swp.kaifamiao.cod…

ARM DIY(一)电源、SD卡座、SOC 调试

文章目录 前言加热台焊接热风枪吹焊电烙铁补焊电源调试SD 卡座调试DRAM 电路调试串口电路调试SOC 调试成品 前言 之前打样的几块 ARM 板&#xff0c;一直放着没去焊接。今天再次看到&#xff0c;决定把它焊起来。 加热台焊接 为了提高焊接效率&#xff0c;先使用加热台焊接…

导出功能exportExcel (现成直接用)

1. 实体类字段上加 Excel(name "xxx"), 表示要导出的字段 Excel(name "订单号")private String orderNo; 2. controller (get请求) /*** 导出订单列表*/ApiOperation("导出订单列表")GetMapping("/export")public void export(HttpS…

缓存穿透、缓存击穿和缓存雪崩

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱发博客的嗯哼&#xff0c;爱好Java的小菜鸟 &#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&#x1f44d;一下博主哦 &#x1f4dd;社区论坛&#xff1a;希望大家能加入社区共同进步…

C语言实例_双向链表增删改查

一、双向链表介绍 双向链表&#xff08;Doubly Linked List&#xff09;是一种常见的数据结构&#xff0c;在单链表的基础上增加了向前遍历的功能。与单向链表不同&#xff0c;双向链表的每个节点除了包含指向下一个节点的指针外&#xff0c;还包含指向前一个节点的指针。 作用…

Spark on Yarn集群模式搭建及测试

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 点击传送&#xff1a;大数据学习专栏 持续更新中&#xff0c;感谢各位前辈朋友们支持学习~ 文章目录 1.Spark on Yarn集群模式介绍2.搭建环境准备3.搭建步骤 1.Spark on Yarn集群模式介…

重排链表(C语言)

题目&#xff1a; 示例&#xff1a; 思路&#xff1a; 这题我们将使用栈解决这个问题&#xff0c;利用栈先进后出的特点&#xff0c;从链表的中间位置进行入栈&#xff0c;寻找链表的中间位置参考&#xff1a;删除链表的中间节点&#xff0c;之后从头开始进行连接。 本题使用…

LLaMA中ROPE位置编码实现源码解析

1、Attention中q&#xff0c;经下式&#xff0c;生成新的q。m为句长length&#xff0c;d为embedding_dim/head θ i 1 1000 0 2 i d \theta_i\frac{1}{10000^\frac{2i}{d}} θi​10000d2i​1​ 2、LLaMA中RoPE源码 import torchdef precompute_freqs_cis(dim: int, end: i…