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

要使用 Qt 5 寫出一個撥放影片的程式,其實滿簡單的;因為 Qt 本身已經提供了一個「Qt Multimedia」的模組(文件),提供了以 QMediaPlayer

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

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》。

相關問題參考:

  • QMediaPlayer fails to play some fomats (mp4/mkv)
  • 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),畫面則會是正常的。


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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。