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

news2024/12/24 0:42:10

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 自己的过程,从而实现自身不断去拉流的效果。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL数据库——多表查询(1)-多表关系(一对多、多对对多、一对一)、多表查询概述(概念、笛卡尔积、分类)

目录 概述 多表关系 一对多&#xff08;多对一&#xff09; 多对多 一对一 多表查询概述 概念 笛卡尔积 分类 多表查询 多表关系多表查询概述内连接外连接自连接子查询多表查询案例 概述 项目开发中&#xff0c;在进行数据库表结构设计时&#xff0c;会根据业务需求及…

框架分析(6)-Ruby on Rails

框架分析&#xff08;6&#xff09;-Ruby on Rails 专栏介绍Ruby on Rails核心概念以及组件讲解MVC架构模式约定优于配置强大的ORM支持自动化测试丰富的插件生态系统RESTful路由安全性总结 优缺点优点快速开发简单易学MVC架构强大的ORM支持大量的插件和Gem支持 缺点性能问题学习…

【Go 基础篇】Go语言数组遍历:探索多种遍历数组的方式

数组作为一种基本的数据结构&#xff0c;在Go语言中扮演着重要角色。而数组的遍历是使用数组的基础&#xff0c;它涉及到如何按顺序访问数组中的每个元素。在本文中&#xff0c;我们将深入探讨Go语言中多种数组遍历的方式&#xff0c;为你展示如何高效地处理数组数据。 前言 …

从零做软件开发项目系列之六——软件测试

前言 软件测试是指在软件开发过程中对软件系统进行验证和验证的过程。它的目的是确保软件能够按照设计要求正常运行&#xff0c;同时检测和纠正可能存在的缺陷和问题。软件测试有助于提高软件质量、可靠性和用户满意度。 1 测试阶段 软件测试阶段是软件开发过程中的重要环节…

【电源专题】18650圆柱电芯内部结构及器件

18650圆柱锂离子电池是一种直径为18mm、高度为65mm的锂离子电池,它最大的特点是拥有非常高的能量密度,它是比较成熟的锂离子电池,各方面系统质量稳定性较好,广泛适用于10千瓦时左右的电池容量场合,例如在、在手机、笔记本电脑等小型电器上。 常见的18650电池分为锂离子电池…

【IMX6ULL驱动开发学习】09.Linux之I2C驱动框架简介和驱动程序模板

参考&#xff1a;Linux之I2C驱动_linux i2c驱动_风间琉璃•的博客-CSDN博客​​​​​​ 目录 一、I2C驱动框架简介 1.1 I2C总线驱动 1.2 I2C设备驱动 二、I2C总线-设备-驱动模型 2.1 i2c_driver 2.2 i2c_client 2.3 I2C 设备数据收发和处理 三、Linux I2C驱动程序模板…

自然语言处理: 第十章GPT的API使用

理论基础 现在的以GPT为首的生成类模型&#xff0c;它拥有对话的能力&#xff0c;它会根据你输入的暗示(prompt)或者指令(instruct)生成对应的回答。所以&#xff0c;不同的输入会导致不同的输出(其实由于chatgpt最终生成的答案是beam_search 以及随机采样的机制&#xff0c;所…

自定义Chronometer实现定时器

概述 自定义Chronometer实现定时器,引用方便&#xff0c;操作简单。 详细 前言 在Android开发过程中&#xff0c;计时控件是经常回使用到的&#xff0c;在Android控件库中有一个能快捷实现计时功能的控件&#xff0c;它就是Chronometer&#xff0c;今天我们基于它自定义实现…

LLM - Model Load_in_8bit For LLaMA

一.引言 LLM 量化是将大语言模型进行压缩和优化以减少其计算和存储需求的过程。 博主在使用 LLaMA-33B 时尝试使用量化加载模型&#xff0c;用传统 API 参数控制量化失败&#xff0c;改用其他依赖尝试成功。这里先铺下结论: ◆ Load_in_8bit ✔️ ◆ Load_in_4bit ❌ 二.LL…

基于SpringBoot+MybatisPlus+Shiro+mysql+redis智慧云智能教育平台

基于SpringBootMybatisPlusShiromysqlredis智慧云智能教育平台 一、系统介绍二、功能展示三.其他系统实现五.获取源码 一、系统介绍 声明&#xff1a;Java智慧云智能教育平台源码 前后端分离、 开发语言&#xff1a;JAVA 数据库&#xff1a;MySQL5.7以上 开发工具&#xff…

08 通过从 库1 复制 *.ibd 到 库2 导致 mysql 启动报错

前言 呵呵 最近同事有这样的一个需求 需要将 库1 的一张表 复制到 库2 然后 我想到了 之前一直使用的通过复制这个库的 data 文件来进行数据迁移的思路, 是需要复制这个 库对应的 data 目录下的数据文件, 以及 ibdata1 文件 然后 我又在想 这里的场景能否也使用这里的额方式…

基于树莓派设计的酒店房间号智能识别系统(图像识别)

基于树莓派4B设计的酒店房间号智能识别系统(图像识别) 一、设计需求 酒店房间识别系统的主要目的是:通过图像处理技术,以机器视觉的方式识别光学传感器传回的酒店房间图像中的数字,该系统可以以非接触的方式,以图像的方式获得需要的酒店房间读数,这能极大提高老式的酒店房…

ssp offer技巧 | 面试经验专栏介绍 | 金九银十

前言&#xff1a;欢迎来到我的面试经验专栏&#xff01;在这个专栏中&#xff0c;我将与大家分享我在多个领域的面试经验&#xff0c;涵盖Java、GO、操作系统、Mysql、计算机网络、Redis等领域。作为一名经验丰富的技术人员&#xff0c;我将通过文章的形式&#xff0c;为大家提…

基于SpringBoot的在线聊天系统

基于SpringBoot的在线聊天系统 一、系统介绍二、功能展示三.其他系统实现五.获取源码 一、系统介绍 源码编号&#xff1a;F-S03点击查看 项目类型&#xff1a;Java EE项目 项目名称&#xff1a;基于SpringBoot的在线聊天系统 项目架构&#xff1a;B/S架构 开发语言&#x…

【【萌新的STM32学习-16中断的基本介绍1】】

萌新的STM32学习-16中断的基本介绍1 中断 什么是中断 中断是打断CPU执行正常的程序&#xff0c;转而处理紧急程序&#xff0c;然后返回原暂停的程序继续执行&#xff0c;就叫中断 中断的作用 实时控制 &#xff1a; 就像对温度进行控制 故障控制 &#xff1a; 第一时间对突发情…

谷歌浏览器的受欢迎之谜:探析其引人入胜的特点

文章目录 &#x1f340;引言&#x1f340;1. 极速的浏览体验&#x1f340;2. 简洁直观的界面&#x1f340;3. 强大的同步功能&#x1f340;4. 丰富的扩展生态系统&#x1f340;5. 安全与隐私的关注&#x1f340;6. 持续的技术创新&#x1f340;7. 跨平台支持和云整合&#x1f3…

《机器学习核心技术》分类算法 - 决策树

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 决策树 1、决策树API2、决策时实际应用2.1、获取数据集2.2、划分数据集2.3、决策…

网络摄像头:SparkoCam Crack

SparkoCam 网络摄像头软件 SparkoCam 是一款网络摄像头和视频效果软件&#xff0c;用于广播实时网络摄像头效果并将其应用到视频聊天和录音中。 使用佳能/尼康数码单反相机作为常规网络摄像头通过向实时视频聊天和视频录制添加酷炫的网络摄像头效果和图形来增强 USB 网络摄像…

前端(十五)——GitHub开源一个react封装的图片预览组件

&#x1f475;博主&#xff1a;小猫娃来啦 &#x1f475;文章核心&#xff1a;GitHub开源一个react封装的图片预览组件 文章目录 组件开源代码下载地址运行效果展示实现思路使用思路和api实现的功能数据和入口部分代码展示 组件开源代码下载地址 Gitee&#xff1a;点此跳转下载…

CSS中如何实现多行文本溢出省略号效果?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 使用text-overflow 和 overflow 属性⭐ 使用clamp() 函数⭐ 使用 JavaScript 或 CSS 框架⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到…