当前位置: 首页 > news >正文

Android 13 - Media框架(7)- 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 creationif (mDataSource == NULL) {mIsSecure = false;if (!mUri.empty()) {const char* uri = mUri.c_str();String8 contentType;// ......// 1. 使用 DataSource 工厂创建一个 datasourcesp<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 sourcestatus_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 appFLAG_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、遍历所有的 trackfor (size_t i = 0; i < numtracks; ++i) {sp<IMediaSource> track = extractor->getTrack(i);if (track == NULL) {continue;}// 获取 track 的信息,包含 mime typesp<MetaData> meta = extractor->getTrackMetaData(i);const char *mime;CHECK(meta->findCString(kKeyMIMEType, &mime));// 创建一个 Track 来封装获取到的 track source,以及 track buffer poolif (!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;}}// 获取 bitrateint32_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 seeksbreak;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 的 generationint32_t generation = getDataGeneration(trackType);// 3、循环读取,直到读到指定数量的 bufferfor (size_t numBuffers = 0; numBuffers < maxBuffers; ) {Vector<MediaBufferBase *> mediaBuffers;status_t err = NO_ERROR;sp<IMediaSource> source = track->mSource;mLock.unlock();// 4、读取bufferif (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;}// 解析读到的每一个bufferfor (; 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、将数据转换为 ABuffersp<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 sourceif (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 自己的过程,从而实现自身不断去拉流的效果。

相关文章:

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

Source 在播放器中起着拉流&#xff08;Streaming&#xff09;和解复用&#xff08;demux&#xff09;的作用&#xff0c;Source 设计的好坏直接影响到播放器的基础功能&#xff0c;我们这一节将会了解 NuPlayer 中的通用 Source&#xff08;GenericSource&#xff09;关注本地…...

MySql015——使用子查询

一、创建customers表 ######################## # Create customers table ######################## use study;CREATE TABLE customers (cust_id int NOT NULL AUTO_INCREMENT,cust_name char(50) NOT NULL ,cust_address char(50) NULL ,cust_city char…...

leetcode 355 设计推特

用链表存储用户发送的每一个推特&#xff0c;用堆获取最先的10条动态 class Twitter {Map<Integer,Set<Integer>> followMap;//规定最新的放到最后Map<Integer,Tweet> postMap;//优先队列(堆&#xff09;PriorityQueue<Tweet> priorityQueue;int time…...

倒数 2 周|期待 2023 Google开发者大会

9 月 6-7 日&#xff0c;中国上海 前沿科技&#xff0c;新知同享 趣味体验&#xff0c;灵感齐聚 技术生态&#xff0c;多元共进 关注官网最新信息&#xff0c;敬请期待大会开幕 2023 Google 开发者大会官网 相信你一定记得&#xff0c;在今年 5 月的 Google I/O 大会上&am…...

代码随想录day57

516最长回文子序列 class Solution { public:int longestPalindromeSubseq(string s) {vector<vector<int>>dp(s.size(),vector<int>(s.size(),0));for(int i0;i<s.size();i)dp[i][i]1;for(int is.size()-1;i>0;i--){for(int ji1;j<s.size();j){if…...

YOLOv5、v8改进:CrissCrossAttention注意力机制

目录 1.简介 2. yolov5添加方法&#xff1a; 2.1common.py构建CrissCrossAttention模块 2.2yolo.py中注册 CrissCrossAttention模块 2.3修改yaml文件。 1.简介 这是ICCV2019的用于语义分割的论文&#xff0c;可以说和CVPR2019的DANet遥相呼应。 和DANet一样&#xff0c;…...

RabbitMQ特性介绍和使用案例

❤ 作者主页&#xff1a;李奕赫揍小邰的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是李奕赫&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习!!!&#x1f389;&#x1f389; 文章目录 RabbitMQ特性…...

Ansible 使用 RHEL 系统角色

安装 RHEL 系统角色软件包&#xff0c;并创建符合以下条件的 playbook /home/greg/ansible/timesync.yml 在所有受管节点上运行 使用 timesync 角色 配置该角色&#xff0c;以使用当前有效的 NTP 提供商 配置该角色&#xff0c;以使用时间服务器 172.25.254.254 配置该角色&am…...

重新认识Android中的线程

线程的几种创建方式 new Thread&#xff1a;可复写Thread#run方法。也可以传递Runnable对象&#xff0c;更加灵活。缺点&#xff1a;缺乏统一管理&#xff0c;可能无限制新建线程&#xff0c;相互之间竞争&#xff0c;及可能占用过多系统的资源导致死机或oom。 new Thread(new…...

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

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

DELL Power Edge R740 安装 OracleLinux-R7-U9-Server

一、准备好 OracleLinux-R7-U9-Server-x86_64-dvd 安装介子&#xff1a; 二、通过 iDRAC挂dvd 安装介子 三、在 iDRAC 开机控制选择虚拟 CD/DCD/ISO 电源控制选择 复位系统&#xff08;热启动&#xff09; 四、进入安装阶段 五、配置时区 六、配置磁盘 七、删除之前的旧分区 …...

深入了解OpenStack:创建定制化QCOW2格式镜像的完全指南

OpenStack 创建自定义的QCOW2格式镜像 前言 建议虚机网络配置为 NAT 或 桥接&#xff0c;因为未来 KVM虚机 需要借助 虚机 的外网能力进行联网安装软件包 虚机在启动前&#xff0c;必须在 VMware Workstation 上为其开启虚拟化引擎 虚拟化 Intel VT-x/EPT 或 AMD-V 安装kvm …...

【Java 中级】一文精通 Spring MVC - 数据格式化器(六)

&#x1f449;博主介绍&#xff1a; 博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家&#xff0c;WEB架构师&#xff0c;阿里云专家博主&#xff0c;华为云云享专家&#xff0c;51CTO 专家博主 ⛪️ 个人社区&#x…...

Linux内核学习(十二)—— 页高速缓存和页回写(基于Linux 2.6内核)

目录 一、缓存手段 二、Linux 页高速缓存 三、flusher 线程 Linux 内核实现了一个被叫做页高速缓存&#xff08;page cache&#xff09;的磁盘缓存&#xff0c;它主要用来减少对磁盘的 I/O 操作。它是通过把磁盘中的数据缓存到内存中&#xff0c;把对磁盘的访问变为对物理内…...

大数据-玩转数据-Flink窗口函数

一、Flink窗口函数 前面指定了窗口的分配器, 接着我们需要来指定如何计算, 这事由window function来负责. 一旦窗口关闭, window function 去计算处理窗口中的每个元素. window function 可以是ReduceFunction,AggregateFunction,or ProcessWindowFunction中的任意一种. Reduc…...

Docker网络-探索容器网络如何相互通信

当今世界&#xff0c;企业热衷于容器化&#xff0c;这需要强大的网络技能来正确配置容器架构&#xff0c;因此引入了 Docker Networking 的概念。Docker 是一种容器化平台&#xff0c;允许您在独立、轻量级的容器中运行应用程序和服务。Docker 提供了一套强大的网络功能&#x…...

ESP32-CAM模块Arduino环境搭建测试

ESP32-CAM模块Arduino环境搭建测试 一.ESP32OV2640摄像头模块CameraWebServer视频查看 二.测试ESP32-CAM(后续称cam模块)代码是否上传执行成功测试 const int led0 12; const int led1 13;void setup() {// put your setup code here, to run once:pinMode(led0, OUTPUT);pin…...

webassembly001 webassembly简述

WebAssembly 官方地址:https://webassembly.org/相关历史 https://en.wikipedia.org/wiki/WebAssembly https://brendaneich.com/2015/06/from-asm-js-to-webassembly/WebAssembly&#xff08;缩写为Wasm&#xff09;是一种基于堆栈的虚拟机的二进制指令格式。Wasm 被设计为编…...

vue 使用C-Lodop打印小票

先从官网下载js文件 https://www.lodop.net/LodopDemo.html 打开安装程序&#xff0c;一直下一步既可&#xff0c;我这边已经安装过就不演示了。 // 引入 import { getLodop } from /utils/CLodopfuncs.js;// 使用 let LODOP getLodop()let Count LODOP.GET_PRINTER_COUNT…...

【C++进阶(二)】STL大法--vector的深度剖析以及模拟实现

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:C从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习C   &#x1f51d;&#x1f51d; vector 1. 前言2. 熟悉vector的接口函数2.1 vec…...

1. import pandas as pd 导入库

【目录】 文章目录 1. import pandas as pd 导入库1. pandas库的概念2. 导入pandas库2.1 常规导入2.2 别名导入 3. 别名的作用4. 课堂练习 【正文】 1. import pandas as pd 导入库 【学习时间】 10分钟 1. pandas库的概念 pandas&#xff1a;熊猫panda的复数&#xff0c; …...

DMK5框选变量之后不显示其他位置的此变量高亮

使用软件MDK5.3.8版本 如下在2的位置选择之后&#xff0c;其他同样的变量没有高亮&#xff0c;因为1的原因折叠了&#xff1b; 展开折叠之后就可以了...

0061__Appium

Appium Documentation - Appium Documentation APP自动化测试&#xff08;3&#xff09;-Appium Inspector介绍_六天测试工程师的博客-CSDN博客 https://github.com/appium/appium-inspector https://github.com/appium/appium-desktop https://github.com/appium/appium...

【DEVOPS】需求跟踪管理全面落地

0. 目录 1. 现状/背景2. 需求管理存在的问题3. 改进思路/措施4. 所谓"禅道尚未普及/铺开"5. 最后6. 相关 1. 现状/背景 近期又被领导问到"如何对项目过程中的需求进行量化和跟踪管理"。这真是一个狗皮膏药似的问题&#xff0c;反反复复地&#xff0c;隔一…...

算法修炼Day57|647. 回文子串 ● 516.最长回文子序列

LeetCode:647. 回文子串 647. 回文子串 - 力扣&#xff08;LeetCode&#xff09; 1.思路 暴力思路见对应代码… 动规解法&#xff1a;画图推导动规公式&#xff0c;当前状态由左侧和左下角推出&#xff0c;所以首层应该采用倒序的方式&#xff0c;内部采用正序的方式。 2.…...

呈现数据的精妙之道:选择合适的可视化方法

在当今数据时代&#xff0c;数据可视化已成为理解和传达信息的重要手段。然而&#xff0c;选择适合的数据可视化方法对于有效地呈现数据至关重要。不同的数据和目标需要不同的可视化方法&#xff0c;下面我们将探讨如何选择最佳的数据可视化方法来呈现数据。 1. 理解数据类型&a…...

数据结构(Java实现)-java对象的比较

元素的比较 基本类型的比较 在Java中&#xff0c;基本类型的对象可以直接比较大小。 对象比较的问题 Java中引用类型的变量不能直接按照 > 或者 < 方式进行比较 默认情况下调用的就是equal方法&#xff0c;但是该方法的比较规则是&#xff1a;没有比较引用变量引用对象的…...

Wolfram Mathematica 13 for Mac 数学计算工具

Wolfram Mathematica for Mac是一款功能强大、划时代的科学计算软件。它结合了数字和符号计算引擎、图形系统、编程语言、文本系统以及与其他应用程序的高级连接&#xff0c;在许多功能方面处于世界领先地位&#xff0c;截至2009年&#xff0c;它是使用最广泛的数学软件之一。人…...

系统架构设计高级技能 · Web架构

现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream weaving wings, let the dream fly in reality. 点击进入系列文章目录 系统架构设计高级技能 Web架构 一、Web架构介绍1.1 Web架构涉及技术1.2 单台服务…...

再写CentOS7升级OpenSSL-1.0.1U

本文在CentOS7.4以及TencentOS 2.4上测试通过。 原系统自带OpenSSL 1.0.2k-fips。 编译安装方法跟之前的没啥区别。 从官网下载1.0.1u版https://www.openssl.org/source/ 使用tar解包 tar xfz openssl-1.0.1u.tar.gz 依次执行如下&#xff1a; cd openssl-1.0.1u ./con…...

wordpress后台登录页面美化/网络推广赚钱

1.真机调试时出现 takinginstallLock 堵塞 解决:这是由于你的设备有程序正在安装,等待安装完成,或重启你的设备 2.Cannot run on the selected destination 在项目的Resources目录下找到info.plist,单击该文件&#xff0c;在Xcode右上角点击“Hide or show the Utilities”按钮…...

东莞工业品网站建设/如何做平台推广赚钱

编写函数&#xff0c;参数是两个非负整数n和m,返回组合数 &#xff0c; 其中m<n<25。例如&#xff0c;n25,m12时答案为5200300。转载于:https://www.cnblogs.com/jjzzx/p/5338052.html...

政府网站如何管理系统/如何做好网络营销

MIUI14是小米公司推出的一款定制版安卓系统&#xff0c;它拥有很多有用的功能和技巧。以下是一些使用技巧&#xff1a; 自定义主屏幕&#xff1a;您可以在主屏幕上添加或删除小部件&#xff0c;以获得更好的使用体验。 电池优化&#xff1a;通过在“设置”>“电池与性能”中…...

百度网盟有哪些网站/google浏览器官网

jquery 对 Json 的各种遍历 概述&#xff1a; JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式&#xff0c;采用完全独立于语言的文本格式&#xff0c;是理想的数据交换格式。同时&#xff0c;JSON是 JavaScript 原生格式&#xff0c;这意味着在 JavaScript 中…...

网站设计原则的第三要素/公司网站首页设计

在异步清除中&#xff0c;利用vue 中data存放setTimeout的标识进行清除时&#xff0c;无法清除。则需要在函数前加上window.即可如window.setTimeout与window.clearTimeout具体代码如下精简后的代码。环境为electron-vue 渲染进程异步获取主进程上html并渲染到页面、过程中需要…...

wordpress 分享后可见/楚雄今日头条新闻

那是因为你没有破解&#xff0c;你只是选择了试用30天 接下来我讲解一下如何破解&#xff1a; 先去网上下载一个13.0破解器&#xff0c;按照说明操作即可 需要一个licence.data文件 把里面的xxxxxx地方用quartus软件tools&#xff0c;licence那儿的第一个字符串去代替即可 …...