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

岳阳网站开发网站运营哪家好c++ 网站开发

岳阳网站开发网站运营哪家好,c++ 网站开发,深圳龙岗做网站公司哪家好,软件下载Source 在播放器中起着拉流#xff08;Streaming#xff09;和解复用#xff08;demux#xff09;的作用#xff0c;Source 设计的好坏直接影响到播放器的基础功能#xff0c;我们这一节将会了解 NuPlayer 中的通用 Source#xff08;GenericSource#xff09;关注本地… Source 在播放器中起着拉流Streaming和解复用demux的作用Source 设计的好坏直接影响到播放器的基础功能我们这一节将会了解 NuPlayer 中的通用 SourceGenericSource关注本地播放架构直播流暂时先不研究。 1、NuPlayer::Source NuPlayer::Source 是一个抽象类定义了 Source 实现所需要的基本接口例如 prepareAsyncstartdequeueAccessUnit等除此之外还包含了一些共有的方法例如 Callback发送 等。 android 为我们提供了5种 Source 实现分别为 HTTPLiveSourceurl 为 m3u8 结尾的 http 链接时NuPlayer 将创建 HTTPLiveSourceRTSPSourceurl 为 rtsp 开头或者以sdp结尾时将创建 RTSPSourceRTPSourceGenericSource通用 Source当 url 不符合以上创建条件时会创建该 Source一般用于本地播放StreamingSourceNuPlayer 的 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, spABuffer *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 buttonFLAG_CAN_SEEK_FORWARD 4, // the 10 sec forward buttonFLAG_CAN_SEEK 8, // the seek barFLAG_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 中 kWhatPreparedSource prepare 完成通知 NuPlayerkWhatFlagsChangedprepare 过程中获取的码流支持的操作prepare 过程中上抛给 NuPlayerkWhatVideoSizeChangedprepare 过程中获取的码流宽高等信息prepare 过程中上抛给 NuPlayerkWhatBufferingUpdate上抛当前 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);}spAMessage 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 工厂创建一个 datasourcespDataSource dataSource PlayerServiceDataSourceFactory::getInstance()-CreateFromURI(mHTTPService, uri, mUriHeaders, contentType,static_castHTTPBase *(mHttpSource.get()));// ......if (!mDisconnected) {mDataSource dataSource;}} // ......}// 这里的 mIsStreaming 表示当前 Source 是不是网络串流的if (mDataSource-flags() DataSource::kIsCachingDataSource) {mCachedSource static_castNuCachedSource2 *(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) {spMetaData meta getFormatMeta_l(false /* audio */);spAMessage 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 主要做了如下几件事情 使用 DataSource 工厂创建合适的 datasource调用 media.extractor 服务将 DataSource 作为参数传入创建 Extractor获取 Extractor 解析到的 video track 信息将信息上抛将source flag 上抛将prepareAsync完成消息上抛。 status_t NuPlayer::GenericSource::initFromDataSource() {spIMediaExtractor extractor;spDataSource 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、获取码流信息包含码流时长等信息spMetaData 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) {spIMediaSource track extractor-getTrack(i);if (track NULL) {continue;}// 获取 track 的信息包含 mime typespMetaData 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 对象。 使用 DataSource 创建 IMediaExtractor获取 Extractor MetaData时长信息、track count、track metadata 这类流信息是存储的 extractor 中的所以需要从 Extractor 中获取到使用 Extractor 为 每个 track 创建 IMediaSource并存储在 vector 中后续读取指定 track 的数据通过该 IMediaSource 来完成将 IMediaSourcetrack 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);spAMessage 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; ) {VectorMediaBufferBase * mediaBuffers;status_t err NO_ERROR;spIMediaSource 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 dont 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、将数据转换为 ABufferspABuffer buffer mediaBufferToABuffer(mbuf, trackType);if (numBuffers 0 actualTimeUs ! nullptr) {*actualTimeUs timeUs;}// 9、给buffer附上一些额外信息if (seeking buffer ! nullptr) {spAMessage meta buffer-meta();if (meta ! nullptr mode MediaPlayerSeekMode::SEEK_CLOSEST seekTimeUs timeUs) {spAMessage 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 doesnt 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;}} }根据 tracktype 决定一次要读取的buffer的数量video buffer size 可能比较大所以读取的数量比较小audio 的情况反之如果需要seek那么读取的时候需要带下去 seek 信息开始读取之前先获取当前的 generation 信息generation 的作用是标记当前执行的操作是否已经过时了对于读取 audio 和 video 是用不到的循环读取直到读到指定数量的 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 中也用到了。它的作用我认为有两点 方便 debug在任务执行过程中检查是否有其他更高优先级的任务需要执行优先级高的任务会直接修改 generation 的数值从而中断当前任务的执行。 selectTrack 可能会改变当前正在播放的 mime typedecoder 需要被释放再重新创建所以需要在 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() {spDataSource dataSource, httpSource;{Mutex::Autolock _l_d(mDisconnectLock);dataSource mDataSource;httpSource mHttpSource;mDisconnected true;}if (dataSource ! NULL) {// disconnect data sourceif (dataSource-flags() DataSource::kIsCachingDataSource) {static_castNuCachedSource2 *(dataSource.get())-disconnect();}} else if (httpSource ! NULL) {static_castHTTPBase *(httpSource.get())-disconnect();} }2.6、seekTo 调用 seek 方法会等当前读取的工作完成自己以 seekTimeseekMode 作为参数去调用 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::maxint64_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, spABuffer *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; }如果 start 为 false 直接退出判断是 bufferpool 是否为空如果为空则尝试读取并直接退出从 bufferpool 中 dequeue buffer如果看 AnotherPacketSource 的源码会发现如果没有buffer了AnotherPacketSource.dequeueAccessUnit 会阻塞等待但是在这之前已经判断了是否为空所以这里并不会出现阻塞的情况检查 bufferpool 中的数据量如果不够了就尝试读取。 最后要看读取事件驱动的问题我们可以发现执行 prepare、start、seek、selectTrack 时都会调用一次 postReadBuffer难道执行一次 read 就没下文了吗 看了 dequeueAccessUnit 我们就可以知道本地文件的读取是依赖 decoder 的需求的decoder 要多少就读多少而 streaming 是不一样的它有一个自己 post 自己的过程从而实现自身不断去拉流的效果。
http://mrfarshtey.net/news/15977/

相关文章:

  • flex做的网站世界著名的设计公司
  • 双线主机可以做彩票网站吗广东新闻联播主持人名单
  • 查询个人信息的网站seo的流程是怎么样的
  • 淘宝网站建设的目标是什么高端网站建设案例
  • 凡科做商品网站的教学视频40岁以上的设计师都去哪了
  • 网站添加可信任站点怎么做顺义的网站建设公司
  • 辽宁大学网站怎么做开发者头条
  • 新乡做网站的多吗网站分类导航代码
  • 蓝色经典通用网站模板html源码下载江门平台入口
  • 网站开发资格证书蓬莱网站建设哪家专业
  • 哪个网站衬衣做的好王烨萍
  • 企业网站的主要功能板块wordpress注册数学验证码
  • 建设银行宁夏分行网站什么是网络营销成败的关键
  • 网站发的文章如何优化wordpress 文章去掉时间
  • 演示公司soap公司网站百度大数据官网入口
  • 知名企业网站规划书做淘宝客网站能接广告吗
  • php开发的培训网站建设上海上设建筑工程有限公司
  • 做网站用什么开发语言alexa排名与什么有关系
  • seo网站推广价格dw网站结构图怎么做
  • 网站建设教程百度云手机网站建设市场报价
  • 自学网站开发多少时间网页设计公司杭州
  • 企业网站的需求是什么有电脑网站怎么做手机网站
  • 0基础做网站工具能上国外网站的dns
  • asp网站建设实录源码广州大石附近做网站的公司哪家好
  • 免费下wordpress南京网络优化公司有哪些
  • 做网站备案时审批号优化技术
  • wordpress 即时站内搜索路桥贝斯特做网站好吗
  • .jsp网站开发技术seo系统
  • 网站服务器如何选择网址导航发布页
  • 长沙建设品牌网站网站设计行业资讯