使用 Qt 撰寫影片播放程式的一些紀錄


要使用 Qt 5 寫出一個撥放影片的程式,其實滿簡單的;因為 Qt 本身已經提供了一個「Qt Multimedia」的模組(文件),提供了以 QMediaPlayer文件)為主的類別、讓使用者可以快速地撰寫出一個撥放軟體了。

這邊 Heresy 則是記錄一下,最近使用他的一些紀錄。


基本使用

他最簡單的寫法,大概可以按照官方範例、寫成:

player = new QMediaPlayer;

playlist = new QMediaPlaylist(player);
playlist->addMedia(QUrl("http://example.com/myclip1.mp4"));
playlist->addMedia(QUrl("http://example.com/myclip2.mp4"));

videoWidget = new QVideoWidget;
player->setVideoOutput(videoWidget);

videoWidget->show();
playlist->setCurrentIndex(1);
player->play();

其中,QMediaPlayer 的物件 player,就是用來播放影片、並進行控制的元件;透過這個類別,可以做到絕大部分的影片撥放的控制,包括了開始/暫停、播放清單、撥放位置控制、聲音控制等等。

不過 QMediaPlayer 本身並不包含輸出畫面,要把影片的畫面顯示出來的話,要透過「setVideoOutput()」來指定要把畫面輸出到哪裡。

在 Qt 中,預設是提供了 QVideoWidget文件)和 QGraphicsVideoItem文件)這兩種元件,可以直接用來顯示 QMediaPlayer 讀取到的影片畫面。而前者基本上就是一個一般的 Qt UI Widget,可以和一般的 widghet 一樣配置在視窗中;後者則是一個 Qt Graphics View Framework 中的元件,可以更有彈性地繪製出來,並做一些調整。

如果是希望可以把 QMediaPlayer 所讀到的畫面一張一張拿出來的話,也還可以繼承 QAbstractVideoSurface 這個類別(文件),自己寫一個類別來接收每一個畫面、並進行後續處理。這部分可以參考關官方的《Video Overview》。

而除了 QAbstractVideoSurface 外,另一個可能可以把畫面一張一張拿出來用的,則是 QVideoProbe文件),如果系統環境允許這個架構的話,它的使用方法應該算是更為直覺的。


Qt 多媒體後台

實際上,Qt 的多媒體模組並沒有自己去處理眾多的多媒體檔案類型的撥放,他基本上是根據平台的不同,使用不同的系統後台(backend),像是以 Windows 來說,他就會去使用 Windows 系統本身的 DirectShow 和 Windows Media Foundation 這兩種環境當作後台;而像是 Unix-like 的系統,則是會使用 GStreamer 來做為後台。

而根據所使用的後台的不同,Qt 的多媒體模組能支援的功能也會有很大的差異,這也導致了開發者所撰寫的同一份程式,在不同的平台上,能支援的功能會非常地不一致。

像是 Heresy 自己在 Windows 10 上面測試,就發現直接使用官方預先編譯好的 Qt 5.7 所撰寫出來的多媒體撥放程式,並無法正確地撥放 MP4 檔案;但是如果在安裝「Haali Media Splitter」(官網)後,就可以正確撥放了。

在查了一些資料後,感覺上這個問題的原因,似乎是 Qt 跑去使用較舊的 DirectShow 來當作後台,而沒有使用比較新的 Windows Media Foundation 所造成的…

而或許也是因為這個問題,QVideoProbe文件)在 Heresy 這邊測試也是無法運作的… orz

至於想要把 backend 切換成 WMF?看起來似乎是無法動態切換、而是得重新編譯一份 Qt 才行了…而這部分,很麻煩啊… ><

另外,Qt 甚至考慮過要移除 WMF 的後台,所以看來 Qt 這部分的問題,可能還得搞一段時間了。

最後,這部分的文件可以參考官方的《Qt 5.7 Multimedia Backends》。

相關問題參考:


QAbstractVideoSurface 的簡單範例

在官方的《Video Overview》的「Working with Low Level Video Frames」這段,已經算是有提供 QAbstractVideoSurface 的基本使用範例了。

這邊要使用的畫,基本上就是撰寫一個繼承自 QAbstractVideoSurface 的類別、並實作 supportedPixelFormats()present() 這兩個函式了。

下面就是官方的範例:

class MyVideoSurface : public QAbstractVideoSurface
{
  QList<QVideoFrame::PixelFormat> supportedPixelFormats(
    QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const
  {
    Q_UNUSED(handleType);
 
    // Return the formats you will support
    return QList<QVideoFrame::PixelFormat>() << QVideoFrame::Format_RGB565;
  }
 
  bool present(const QVideoFrame &frame)
  {
    Q_UNUSED(frame);
    // Handle the frame and do your processing
 
    return true;
  }
};

其中,supportedPixelFormats() 是用來告訴外部的物件,這個元件支援那些格式用的,而 present() 則是會在 QMediaPlayer 有新畫面時、把畫面以 QVideoFrame 的格式(文件)傳遞進來的函式。

至於 MyVideoSurface 撰寫完了該怎麼用呢?基本上就是透過 QMediaPlayersetVideoOutput() 這個函式,把他設定成 QMediaPlayer 的畫面輸出目標了。

不過,由於 QMediaPlayer 只允許單一的畫面輸出目標,所以在設定成輸出到 QAbstractVideoSurface 之後,螢幕上就不會有畫面了。而如果需要顯示影片的畫面的話,就得在 QAbstractVideoSurface 中,自己把內容畫出來了。

那怎麼處理 QVideoFrame 內的資料呢?Heresy 這邊是參考《[Tutorial] OpenGL and Qt: Video as a Texture》這篇文章的寫法,把 present() 寫成:

bool QtVideoFrameMapper::present(const QVideoFrame & frame)
{
  if (frame.isValid())
  {
    QVideoFrame videoFrame(frame);
 
    if (videoFrame.map(QAbstractVideoBuffer::ReadOnly))
    {
      if (m_iDataSize != videoFrame.mappedBytes())
      {
        m_iDataSize = 0;
        delete[] m_pData;
        m_pData = nullptr;
      }
 
      if (m_iDataSize == 0)
      {
        m_iDataSize = videoFrame.mappedBytes();
        m_pData = new uchar[m_iDataSize];
 
        QImage::Format mImgFormat = QVideoFrame::imageFormatFromPixelFormat(videoFrame.pixelFormat());

        m_imgFrame = QImage(m_pData, frame.width(), frame.height(), frame.bytesPerLine(), mImgFormat);
        emit onFormatChanged(QSize(frame.width(), frame.height()), mImgFormat);
      }
 
      memcpy(m_pData, videoFrame.bits(), videoFrame.mappedBytes());
      emit onNewFrame(m_imgFrame);
    }
 
    videoFrame.unmap();
  }
 
  return true;
}

這邊基本上就是把 frame 的資料,完整地複製一分到 m_pData 裡面了~而為了方便存取,這邊也另外把它包成 QImage 的物件 m_imgFrame 來使用。

這邊比較需要注意的是,由於 QVideoFrame 的實際畫面內容有可能不是在主記憶體、而是在顯示卡記憶體中,所以要存取的話,是需要先透過 map() 這個函式,確保 CPU 的程式可以存取到他的資料,而使用完之後,也要再呼叫 unMap()、來告訴 Qt 已經使用完畢了。

至於 onFormatChanged()onNewFrame() 則是自己定義的兩個 signal,用來告訴其他東西,影像的格式改變了、以及有取得新畫面了。


一些細節的問題

上面算是 Heresy 這邊目前使用上,主要的狀況,下面則是一些比較零星的問題。

  • QMediaPlayerpositionChanged 這個 signal 觸發頻率不高,基本上兩次的間隔會超過幾個 frame。所以沒辦法透過這個 signal 做精密的控制。

  • QMediaPlayer 在 Windows 10 上,似乎都會把影像用 RGB32 來儲存,相較於原始的 RGB888,其實應該算是浪費了不少資源…

  • QMediaPlayer 基本上也可以撥放圖檔,甚至動態 GIF;但是圖檔基本上沒有時間長度,而動態 GIF 雖然可以讀到影片長度,但是在使用播放清單時,卻還是會無限重播。

    • 無法讀取 PNG 檔。

  • 在使用 VIsualStudio 開發時,如果有開偵錯的話(按 F5 執行),那解出來的畫面是有問題的;但是如果是獨立執行(或是按 Ctrl + F5),畫面則會是正常的。


這篇紀錄就先寫到這邊了,之後有想到要補充的就再說吧。

對「使用 Qt 撰寫影片播放程式的一些紀錄」的想法

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料