http://blog.csdn.net/mirkerson/article/details/38520135

stagefright框架(一)Video Playback的流程
在Android上,預設的多媒體框架(multimedia framework)是OpenCORE。OpenCORE的優點是兼顧了跨平台的移植性,而且已經過多方驗證,所以相對來說較為穩定;但是其缺點是過於龐大複雜,需要耗費相當多的時間去維護。從Android 2.0開始,Google引進了架構稍為簡潔的Stagefright,並且有逐漸取代OpenCORE的趨勢 (註1)。

[圖1] Stagefright在Android多媒體架構中的位置。

[圖2] Stagefright所涵蓋的模組 (註2)。
以下我們就先來看看Stagefright是如何播放一個影片檔。

Stagefright在Android中是以shared library的形式存在(libstagefright.so),其中的module -- AwesomePlayer可用來播放video/audio(註3)。AwesomePlayer提供許多API,可以讓上層的應用程式(Java/JNI)來呼叫,我們以一個簡單的程式來說明video playback的流程。

在Java中,若要播放一個影片檔,我們會這樣寫:

MediaPlayer mp = new MediaPlayer();
mp.setDataSource(PATH_TO_FILE); ...... (1)
mp.prepare(); ........................ (2)、(3)
mp.start(); .......................... (4)

在Stagefright中,則會看到相對應的處理;

(1)將檔案的絕對路徑指定給mUri

status_t AwesomePlayer::setDataSource(constchar* uri,...)
{
  return setDataSource_l(uri,...);
}

status_t AwesomePlayer::setDataSource_l(constchar* uri,...)
{
  mUri = uri;
}

(2)啟動mQueue,作為event handler

status_t AwesomePlayer::prepare()
{
  return prepare_l();
}

status_t AwesomePlayer::prepare_l()
{
  prepareAsync_l();

while (mFlags& PREPARING)
  {
    mPreparedCondition.wait(mLock);
  }
}

status_t AwesomePlayer::prepareAsync_l()
{
  mQueue.start();

mFlags |= PREPARING;
  mAsyncPrepareEvent = new AwesomeEvent(
                             this
                             &AwesomePlayer::onPrepareAsyncEvent);
  mQueue.postEvent(mAsyncPrepareEvent);
}

(3) onPrepareAsyncEvent被觸發

void AwesomePlayer::onPrepareAsyncEvent()
{
  finishSetDataSource_l();

initVideoDecoder();......(3.3)
  initAudioDecoder();
}

status_t AwesomePlayer::finishSetDataSource_l()
{
  dataSource = DataSource::CreateFromURI(mUri.string(),...);
  sp<</span>MediaExtractor> extractor=
                     MediaExtractor::Create(dataSource);.....(3.1)

return setDataSource_l(extractor);.........................(3.2)
}

(3.1)解析mUri所指定的檔案,並且根據其header來選擇對應的extractor

sp<</span>MediaExtractor> MediaExtractor::Create(const sp<</span>DataSource>&source,...)
{
  source->sniff(&tmp,...);
  mime = tmp.string();

if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG4)
  {
    return new MPEG4Extractor(source);
  }
  else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MPEG))
  {
    return new MP3Extractor(source);
  }
  else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB)
  {
    return new AMRExtractor(source);
  }
}

(3.2)使用extractor對檔案做A/V的分離 (mVideoTrack/mAudioTrack)

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

CHECK(meta->findCString(kKeyMIMEType,&mime));

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

void AwesomePlayer::setVideoSource(sp<</span>MediaSource> source)
{
  mVideoTrack = source;
}

(3.3)根據mVideoTrack中的編碼類型來選擇video decoder (mVideoSource)

status_t AwesomePlayer::initVideoDecoder()
{
  mVideoSource = OMXCodec::Create(mClient.interface(),
                                  mVideoTrack->getFormat(),
                                  false,
                                  mVideoTrack);
}

(4) 將mVideoEvent放入mQueue中,開始解碼播放,並交由mVideoRenderer來畫出

status_t AwesomePlayer::play()
{
  return play_l();
}

status_t AwesomePlayer::play_l()
{
  postVideoEvent_l();
}

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

void AwesomePlayer::onVideoEvent()
{
  mVideoSource->read(&mVideoBuffer,&options);
  [Check Timestamp]
  mVideoRenderer->render(mVideoBuffer);

postVideoEvent_l();
}

(註1) 從Android2.3 (Gingerbread) 開始,預設的多媒體框架為 Stagefright。
(註2) Stagefright的架構尚不斷在演進中,本系列文章並未含括所有的模組。
(註3) Audio的播放是交由 AudioPlayer 來處理,請參考《Stagefright (6) - Audio Playback的流程》。

stagefright框架(二)- 和OpenMAX的運作

Stagefright的編解碼功能是利用OpenMAX框架,而且用的還是OpenCORE之OMX的實作,我們來看一下Stagefright和OMX是如何運作的。

(1) OMX_Init

OMXClient mClient;

AwesomePlayer::AwesomePlayer()
{
  mClient.connect();
}

status_t OMXClient::connect()
{
  mOMX = service->getOMX();
}

sp MediaPlayerService::getOMX()
{
  mOMX = new OMX;
}

OMX::OMX() : mMaster(new OMXMaster)

OMXMaster::OMXMaster()
{
  addPlugin(new OMXPVCodecsPlugin);
}

OMXPVCodecsPlugin::OMXPVCodecsPlugin()
{
  OMX_MasterInit();
}

OMX_ERRORTYPE OMX_MasterInit() <-- under OpenCORE
{
  return OMX_Init();
}

(2) OMX_SendCommand

OMXCodec::function_name()
{
  mOMX->sendCommand(mNode, OMX_CommandStateSet, OMX_StateIdle);
}
status_t OMX::sendCommand(node, cmd, param)
{
  return findInstance(node)->sendCommand(cmd, param);
}
status_t OMXNodeInstance::sendCommand(cmd, param)
{
  OMX_SendCommand(mHandle, cmd, param, NULL);
}

(3) 其他作用在 OMX 元件的指令

其他作用在OMX元件的指令也和OMX_SendCommand的callpath一樣,請見下表:

OMXCodec

OMX

OMXNodeInstance

useBuffer

useBuffer (OMX_UseBuffer)

getParameter

getParameter (OMX_GetParameter)

fillBuffer

fillBuffer (OMX_FillThisBuffer)

emptyBuffer

emptyBuffer (OMX_EmptyThisBuffer)

(4) Callback Functions

OMX_CALLBACKTYPE OMXNodeInstance::kCallbacks =
{
  &OnEvent, <--------------- omx_message::EVENT
  &OnEmptyBufferDone, <----- omx_message::EMPTY_BUFFER_DONE
  &OnFillBufferDone <------- omx_message::FILL_BUFFER_DONE
}

stagefright框架(三)-選擇Video Decoder

在《Stagefright (1) – Video Playback的流程》中,我們並沒有詳述Stagefright是如何根據影片檔的類型來選擇適合的video decoder,現在,就讓我們來看一看。

(1) Video decoder是在onPrepareAsyncEvent中的initVideoDecoder被決定的

OMXCodec::Create()會回傳video decoder給mVideoSource。

status_t AwesomePlayer::initVideoDecoder()
{
  mVideoSource = OMXCodec::Create(mClient.interface(),
                                  mVideoTrack->getFormat(),
                                  false,
                                  mVideoTrack);
}

sp OMXCodec::Create(&omx, &meta, createEncoder, &source, matchComponentName)
{
  meta->findCString(kKeyMIMEType, &mime);

findMatchingCodecs(mime, ..., &matchingCodecs); ........ (2)

for (size_t i = 0; i < matchingCodecs.size(); ++i)
  {
    componentName = matchingCodecs[i].string();

softwareCodec =
        InstantiateSoftwareCodec(componentName, ...); ..... (3)

if (softwareCodec != NULL) return softwareCodec;
        
    err = omx->allocateNode(componentName, ..., &node); ... (4)

if (err == OK)
    {
      codec = new OMXCodec(..., componentName, ...); ...... (5)
      return codec;
    }
  }
}

(2) 根據mVideoTrack的MIME從kDecoderInfo挑出合適的components

void OMXCodec::findMatchingCodecs(mime, ..., matchingCodecs)
{
  for (int index = 0;; ++index)
  {
    componentName = GetCodec(
                       kDecoderInfo,
                       sizeof(kDecoderInfo)/sizeof(kDecoderInfo[0]),
                       mime,
                       index);

matchingCodecs->push(String8(componentName));
  }
}

static const CodecInfo kDecoderInfo[] =
{
  ...
  { MEDIA_MIMETYPE_VIDEO_MPEG4, "OMX.qcom.video.decoder.mpeg4" },
  { MEDIA_MIMETYPE_VIDEO_MPEG4, "OMX.TI.Video.Decoder" },
  { MEDIA_MIMETYPE_VIDEO_MPEG4, "M4vH263Decoder" },
  ...
}

GetCodec會依據mime從kDecoderInfo挑出所有的component name,然後存到matchingCodecs中。

(3) 根據matchingCodecs中component的順序,我們會先去檢查其是否為software decoder

static sp InstantiateSoftwareCodec(name, ...)
{
  FactoryInfo kFactoryInfo[] =
  {
    ...
    FACTORY_REF(M4vH263Decoder)
    ...
  };

for (i = 0; i < sizeof(kFactoryInfo)/sizeof(kFactoryInfo[0]); ++i)
  {
    if (!strcmp(name, kFactoryInfo[i].name))
      return (*kFactoryInfo[i].CreateFunc)(source);
  }
}

所有的software decoder都會被列在kFactoryInfo中,我們藉由傳進來的name來對應到適合的decoder。

(4) 如果該component不是software decoder,則試著去配置對應的OMX component

status_t OMX::allocateNode(name, ..., node)
{
  mMaster->makeComponentInstance(
                           name,
                           &OMXNodeInstance::kCallbacks,
                           instance,
                           handle);
}

OMX_ERRORTYPE OMXMaster::makeComponentInstance(name, ...)
{
  plugin->makeComponentInstance(name, ...);
}

OMX_ERRORTYPE OMXPVCodecsPlugin::makeComponentInstance(name, ...)
{
  return OMX_MasterGetHandle(..., name, ...);
}

OMX_ERRORTYPE OMX_MasterGetHandle(...)
{
  return OMX_GetHandle(...);
}

(5) 若該component為OMX deocder,則回傳;否則繼續檢查下一個component

stagefright框架(四)-Video Buffer傳輸流程

這篇文章將介紹Stagefright中是如何和OMX video decoder傳遞buffer。

(1) OMXCodec會在一開始的時候透過read函式來傳送未解碼的data給decoder,並且要求decoder將解碼後的data傳回來

status_t OMXCodec::read(...)
{
  if (mInitialBufferSubmit)
  {
    mInitialBufferSubmit = false;

drainInputBuffers();<----- OMX_EmptyThisBuffer
    fillOutputBuffers();<----- OMX_FillThisBuffer
  }

...
}

void OMXCodec::drainInputBuffers()
{
  Vector<</span>BufferInfo>*buffers= &mPortBuffers[kPortIndexInput];

for (i = 0; i <</span> buffers->size();++i)
  {
    drainInputBuffer(&buffers->editItemAt(i));
  }
}

void OMXCodec::drainInputBuffer(BufferInfo*info)
{
  mOMX->emptyBuffer(...);
}

void OMXCodec::fillOutputBuffers()
{
  Vector<</span>BufferInfo>*buffers= &mPortBuffers[kPortIndexOutput];

for (i = 0; i <</span> buffers->size();++i)
  {
    fillOutputBuffer(&buffers->editItemAt(i));
  }
}

void OMXCodec::fillOutputBuffer(BufferInfo*info)
{
  mOMX->fillBuffer(...);
}

(2) Decoder從input port讀取資料後,開始進行解碼,並且回傳EmptyBufferDone通知OMXCodec

void OMXCodec::on_message(const omx_message &msg)
{
  switch (msg.type)
  {
    case omx_message::EMPTY_BUFFER_DONE:
    {
      IOMX::buffer_id buffer= msg.u.extended_buffer_data.buffer;
      drainInputBuffer(&buffers->editItemAt(i));
    }
  }
}

OMXCodec收到EMPTY_BUFFER_DONE之後,繼續傳送下一個未解碼的資料給decoder。

(3) Decoder將解碼完的資料送到output port,並回傳FillBufferDone通知OMXCodec

void OMXCodec::on_message(const omx_message &msg)
{
  switch (msg.type)
  {
    case omx_message::FILL_BUFFER_DONE:
    {
      IOMX::buffer_id buffer= msg.u.extended_buffer_data.buffer;
      fillOutputBuffer(info);

mFilledBuffers.push_back(i);
      mBufferFilled.signal();
    }
  }
}

OMXCodec收到FILL_BUFFER_DONE之後,將解碼後的資料放入mFilledBuffers,發出mBufferFilled信號,並且要求decoder繼續送出資料。

(4) read函式在後段等待mBufferFilled信號。當mFilledBuffers被填入資料後,read函式將其指定給buffer指標,並回傳給AwesomePlayer

status_t OMXCodec::read(MediaBuffer**buffer,...)
{
  ...

while (mFilledBuffers.empty())
  {
    mBufferFilled.wait(mLock);
  }

BufferInfo *info = &mPortBuffers[kPortIndexOutput].editItemAt(index);
  info->mMediaBuffer->add_ref();
  *buffer = info->mMediaBuffer;
}

stagefright框架(五)-Video Rendering

AwesomePlayer::onVideoEvent除了透過OMXCodec::read取得解碼後的資料外,還必須將這些資料(mVideoBuffer)傳給video renderer,以便畫到螢幕上去。

(1) 要將mVideoBuffer中的資料畫出來之前,必須先建立mVideoRenderer

void AwesomePlayer::onVideoEvent()
{
  ...

if (mVideoRenderer==NULL)
  {
    initRenderer_l();
  }

...
}

void AwesomePlayer::initRenderer_l()
{
  if (!strncmp("OMX.", component, 4))
  {
    mVideoRenderer = new AwesomeRemoteRenderer(
                           mClient.interface()->createRenderer(
                                                  mISurface,
                                                  component,
                                                  ...));..........(2)
  }
  else
  {
    mVideoRenderer = new AwesomeLocalRenderer(
                           ...,
                           component,
                           mISurface);............................(3)
  }
}

(2) 如果video decoder是OMX component,則建立一個AwesomeRemoteRenderer作為mVideoRenderer

從上段的程式碼(1)來看,AwesomeRemoteRenderer的本質是由OMX::createRenderer所創建的。createRenderer會先建立一個hardware renderer --SharedVideoRenderer (libstagefrighthw.so);若失敗,則建立software renderer -- SoftwareRenderer (surface)。

sp<</span>IOMXRenderer> OMX::createRenderer(...)
{
  VideoRenderer *impl = NULL;

libHandle = dlopen("libstagefrighthw.so", RTLD_NOW);

if (libHandle)
  {
    CreateRendererFunc func = dlsym(libHandle,...);

impl = (*func)(...);<----------------- Hardware Renderer
  }

if (!impl)
  {
    impl = new SoftwareRenderer(...);<---- Software Renderer
  }
}

(3) 如果video decoder是software component,則建立一個AwesomeLocalRenderer作為mVideoRenderer

AwesomeLocalRenderer的constructor會呼叫本身的init函式,其所做的事和OMX::createRenderer一模一樣。

void AwesomeLocalRenderer::init(...)
{
  mLibHandle = dlopen("libstagefrighthw.so", RTLD_NOW);

if (mLibHandle)
  {
    CreateRendererFunc func = dlsym(...);

mTarget = (*func)(...);<---------------- Hardware Renderer
  }

if (mTarget==NULL)
  {
    mTarget = new SoftwareRenderer(...);<--- Software Renderer
  }
}

(4) mVideoRenderer一經建立就可以開始將解碼後的資料傳給它

void AwesomePlayer::onVideoEvent()
{
  if (!mVideoBuffer)
  {
    mVideoSource->read(&mVideoBuffer,...);
  }

[Check Timestamp]

if (mVideoRenderer==NULL)
  {
    initRenderer_l();
  }

mVideoRenderer->render(mVideoBuffer);<</span>----- Render Data
}

stagefright框架(六)-Audio Playback的流程

到目前為止,我們都只著重在video處理的部分,對於audio卻隻字未提。這篇文章將會開始audio處理的流程。

Stagefright中關於audio的部分是交由AudioPlayer來處理,它是在AwesomePlayer::play_l中被建立的。

(1) 當上層應用程式要求播放影音時,AudioPlayer同時被建立出來,並且被啟動

status_t AwesomePlayer::play_l()
{
  ...

mAudioPlayer = new AudioPlayer(mAudioSink,...);
  mAudioPlayer->start(...);

...
}

(2) AudioPlayer在啟動的過程中會先去讀取第一筆解碼後的資料,並且開啟audio output

status_t AudioPlayer::start(...)
{
  mSource->read(&mFirstBuffer);

if (mAudioSink.get()!=NULL)
  {
    mAudioSink->open(...,&AudioPlayer::AudioSinkCallback,...);
    mAudioSink->start();
  }
  else
  {
    mAudioTrack = new AudioTrack(...,&AudioPlayer::AudioCallback,...);
    mAudioTrack->start();
  }
}

從AudioPlayer::start的程式碼來看,AudioPlayer似乎並沒有將mFirstBuffer傳給audio output。

(3) 開啟audio output的同時,AudioPlayer會將callback函式設給它,之後每當callback函式被呼叫,AudioPlayer便去audio decoder讀取解碼後的資料

size_t AudioPlayer::AudioSinkCallback(audioSink, buffer, size,...)
{
  return fillBuffer(buffer, size);
}
void AudioPlayer::AudioCallback(..., info)
{
  buffer = info;
  fillBuffer(buffer->raw, buffer->size);
}
size_t AudioPlayer::fillBuffer(data, size)
{
  mSource->read(&mInputBuffer,...);
  memcpy(data, mInputBuffer->data(),...);
}

解碼後audio資料的讀取就是由callback函式所驅動,但是callback函式又是怎麼由audio output去驅動的,目前從程式碼上還看不出來。另外一方面,從上面的程式片段可以看出,fillBuffer將資料(mInputBuffer)複製到data之後,audio output應該會去取用data。

(5) 至於audio decoder的工作流程則和video decoder相同,可參閱《Stagefright (4)- Video Buffer傳輸流程》

stagefright框架(七)-Audio和Video的同步

講完了audio和video的處理流程,接下來要看的是audio和video同步化(synchronization)的問題。OpenCORE的做法是設置一個主clock,而audio和video就分別以此作為輸出的依據。而在Stagefright中,audio的輸出是透過callback函式來驅動,video則根據audio的timestamp來做同步。以下是詳細的說明:

(1) 當callback函式驅動AudioPlayer讀取解碼後的資料時,AudioPlayer會取得兩個時間戳 -- mPositionTimeMediaUs和mPositionTimeRealUs

size_t AudioPlayer::fillBuffer(data, size)
{
  ...

mSource->read(&mInputBuffer,...);

mInputBuffer->meta_data()->findInt64(kKeyTime,&mPositionTimeMediaUs);
  mPositionTimeRealUs = ((mNumFramesPlayed + size_done / mFrameSize)* 1000000)/mSampleRate;

...
}

mPositionTimeMediaUs是資料裡面所載明的時間戳(timestamp);mPositionTimeRealUs則是播放此資料的實際時間(依據framenumber及samplerate得出)。

(2) Stagefright中的video便依據從AudioPlayer得出來之兩個時間戳的差值,作為播放的依據

void AwesomePlayer::onVideoEvent()
{
  ...

mVideoSource->read(&mVideoBuffer,...);
  mVideoBuffer->meta_data()->findInt64(kKeyTime,&timeUs);

mAudioPlayer->getMediaTimeMapping(&realTimeUs,&mediaTimeUs);
  mTimeSourceDeltaUs = realTimeUs - mediaTimeUs;

nowUs = ts->getRealTimeUs()- mTimeSourceDeltaUs;
  latenessUs = nowUs - timeUs;

...
}

AwesomePlayer從AudioPlayer取得realTimeUs(即mPositionTimeRealUs)和mediaTimeUs(即mPositionTimeMediaUs),並算出其差值mTimeSourceDeltaUs。

(3) 最後我們將該video資料做排程

void AwesomePlayer::onVideoEvent()
{
  ...
  if (latenessUs> 40000)
  {
    mVideoBuffer->release();
    mVideoBuffer = NULL;

postVideoEvent_l();
    return;
  }
  if (latenessUs<</span>-10000)
  {
    postVideoEvent_l(10000);
    return;
  }

mVideoRenderer->render(mVideoBuffer);

...
}


android stagefright框架相关推荐

  1. android stagefright 框架

    原文:[转]android stagefright 框架 链接:http://blog.chinaunix.net/uid-9838896-id-2976618.html 在Android上,预设的多 ...

  2. android多媒体框架学习 详解

    原址 一:多媒体框架概述 jellybean 的多媒体跟以前的版本,通过对比没啥变化,最大的变化是google终于舍得给multimedia建个独立的git了(framework/av),等你好久了! ...

  3. android多媒体框架学习 详解 最新版本

    一:多媒体框架概述 jellybean 的多媒体跟以前的版本,通过对比没啥变化,最大的变化是google终于舍得给multimedia建个独立的git了(framework/av),等你好久了!也体现 ...

  4. StageFright框架流程解读

    1.    StageFright介绍 Android froyo版本多媒体引擎做了变动,新添加了stagefright框架,并且默认情况android选择stagefright,并没有完全抛弃ope ...

  5. Android View框架的measure机制

    Android中View框架的工作机制中,主要有三个过程: 1.View树的测量(measure)Android View框架的measure机制 http://www.cnblogs.com/xyh ...

  6. 5个最佳的Android测试框架

    2019独角兽企业重金招聘Python工程师标准>>> 谷歌的Android生态系统正在不断地迅速扩张.有证据表明,新的移动OEM正在攻陷世界的每一个角落,不同的屏幕尺寸.ROM / ...

  7. 开发自己的山寨Android注解框架

    目录 开发自己的山寨Android注解框架 开发自己的山寨Android注解框架 参考 Github黄油刀 Overview 在上一章我们学习了Java的注解(Annotation),但是我想大家可能 ...

  8. android layout后还原位置,Android图片框架photoview如何记住所有状态并还原,包括缩放度,缩放后的移动的距离等等...

    Android图片框架photoview如何记住状态并还原,包括缩放度,缩放后的移动的距离等等,尝试了好多方法都没有作用. private void generateImages() { for (i ...

  9. stagefright框架概述

    多媒体框架stagefright是android2.3后替代opencore的.整个框架非常庞大,这里将它分为编码器.解码器.合成器.分离器.PCM数据和YUV数据输入.数据源(DataSource) ...

最新文章

  1. PHP executable not found. Install PHP 7 and add it to your PATH or set the php.executablePath settin
  2. win10下pycharm安装opencv tensorflow anaconda
  3. 死磕Java并发:J.U.C之读写锁:ReentrantReadWriteLock
  4. 【NGINX+PHP7.3+MYSQL】安装指南
  5. Python学习笔记:电子邮件,POP3收取邮件
  6. 提取图片纹理_Fundamentals Of Computer Graphics 第十一章 纹理映射(中)
  7. 【Trie】【费用流】管道监控(loj 3026)
  8. CIO大咖专访 | 从实战中提炼的企业数字化转型要点
  9. TensorFlow第八步 Nesterov's accelerated gradient descent+L2 regularization
  10. python中布尔变量的值为_python 布尔操作实现代码 python是如何定义并使用变量的...
  11. Java字符串中最长回文子字符串
  12. 梦幻之旅--深入大规模芯片设计全过程
  13. 《薄世宁医学通识讲义》笔记
  14. 如何删除360浏览器的桔梗导航
  15. 什么是工业物联网?与工业互联网有什么区别
  16. sudo: no valid sudoers sources found,quitting sudo sudoers改坏后无法使用sudo的解决办法
  17. UE5学习笔记(五)——地形及地表材质
  18. linux应用程序注册表,如何打开 Linux 中 Windows 程序的注册表编辑器
  19. [转载]机器人工程师的漫漫长路
  20. Linux - 如何生成configure文件

热门文章

  1. Typora+PicGo+GitHub图床的搭建和常见问题的个人解决方法
  2. 我让小屋更漂亮计算机教案,漂亮的房间教案
  3. win10 调整了文本大小后桌面图标间距变宽,改不回去
  4. 抖音游戏发行人项目,靠量去堆砌也能月入三千
  5. 精通 TensorFlow 2.x 计算机视觉:第一部分
  6. Vue-3D-Model:用简单的方式来展示三维模型
  7. 2021(SSL) 自监督学习最新力作:Barlow Twins: Self-Supervised Learning via Redundancy Reduction
  8. 30个视频号运用方法,你都知道哪些?(四)
  9. PowerPoint绘图笔不能用
  10. linux temp文件夹在哪_全方位剖析 Linux 操作系统,太全了