Feeding custom HTTP(S) media streams to QtMultimedia

Qt is a powerful framework for writing multi-platform applications in C++. The markup language called QML also enhances Qt's capabilities by serving a runtime and mechanisms to instantiate C++ objects (inheriting the QObject type) from within a JavaScript-based runtime environment.

The integration between these JavaScript and C++ environments allows to reference code written in C++ from JavaScript an vice-versa. This nifty attribute is what we can make use of when creating a multimedia player capable of taking data from a custom input stream implementation.

GhostCloud implements this for it's document preview functionality, providing media data to the QMediaPlayer hidden inside of the MediaPlayer QML type. The main difference between playing media from a typical URL and the way GhostCloud does it is that fetching data from Nextcloud and ownCloud requires special HTTP headers to be set, specifically the Authorization and OCS-APIREQUEST headers.

As the goal for GhostCloud was to provide simple play and pause capabilities it was necessary to craft a custom QNetworkRequest with the headers set and passed on to the underlying QMediaPlayer object. This is done by a class type called WebDavMediaFeeder which may look like this:

#include <QObject>
#include <QUrl>
#include <QMediaPlayer>

class WebDavMediaFeeder : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QObject* mediaPlayer READ mediaPlayer
               WRITE setMediaPlayer
               NOTIFY mediaPlayerChanged)
    Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)

public:
    explicit WebDavMediaFeeder(
            QObject* parent = Q_NULLPTR,
            QObject* mediaPlayer = Q_NULLPTR);
    ~WebDavMediaFeeder();

    void setMediaPlayer(QObject* object);
    QObject* mediaPlayer();
    void setUrl(const QUrl& url);
    QUrl url();

public slots:
    void play();
    void pause();
    void stop();

private:
    QObject* m_mediaPlayer = Q_NULLPTR;
    QUrl m_url;

signals:
    void mediaPlayerChanged();
    void urlChanged();
    void settingsChanged();
};

Using Qt's property system we can set a MediaPlayer QML object instance for the media feeder to know which instance to prepare the network request for. The URL to the resource is also set on the media feeders url property from QML.
The WebDavMediaFeeder implements the slot methods for play, pause, and stop the stream which we can call from QML instead of calling the same methods from MediaPlayer. The underlying QMediaPlayer implementation is hidden inside a property called mediaObject which we can refrence via Qt's reflection mechanisms.

inline QMediaPlayer* mediaPlayerObjectCast(QObject* object) {
    if (!object) {
        qWarning() << "the provided object is a nullptr";
        return Q_NULLPTR;
    }

    QMediaPlayer* mediaPlayer = qvariant_cast<QMediaPlayer*>(object->property("mediaObject"));
    if (!mediaPlayer) {
        qWarning() << "the object doesn't contain a QMediaPlayer*";
        return Q_NULLPTR;
    }
    return mediaPlayer;
}

Now all we need is to prepare a custom header and call play() on the returned mediaPlayer. The QNetworkRequest is assigned new headers for NextCloud/ownCloud API endpoints to accept the request as well as the necessary authorization credentials.

inline QNetworkRequest getOcsRequest(const QNetworkRequest& request)
{
    qDebug() << Q_FUNC_INFO;

    // Read raw headers out of the provided request
    QMap<QByteArray, QByteArray> rawHeaders;
    for (const QByteArray& headerKey : request.rawHeaderList()) {
        rawHeaders.insert(headerKey, request.rawHeader(headerKey));
    }

    // Add required headers
    headers.insert(QByteArrayLiteral("Authorization"),
                   QByteArrayLiteral("123:abc"));
    headers.insert(QByteArrayLiteral("OCS-APIREQUEST"),
                   QByteArrayLiteral("true"));
    
    // Construct new QNetworkRequest with prepared header values
    QNetworkRequest newRequest(request);
    for (const QByteArray& headerKey : rawHeaders.keys()) {
        newRequest.setRawHeader(headerKey, rawHeaders.value(headerKey));
    }

    qDebug() << "headers" << newRequest.rawHeaderList();

    return newRequest;
}


void WebDavMediaFeeder::play()
{
    QMediaPlayer* providedMediaPlayer =
            mediaPlayerObjectCast(this->m_mediaPlayer);

    if (!providedMediaPlayer)
        return;

    const QNetworkRequest request = getOcsRequest(QNetworkRequest(this->m_url));
    if (providedMediaPlayer->state() == QMediaPlayer::StoppedState) {
        providedMediaPlayer->setMedia(QMediaContent(request));
    }
    providedMediaPlayer->play();
}

As a result the media player is given a QMediaContent object containing the new QNetworkRequest which was prepared by getOcsRequest().

For the WebDavMediaFeeder to be instantiated from QML we need to register the class type into Qt's type system using qmlRegisterType().

qmlRegisterType<WebDavMediaFeeder>("me.fredl.types", 1, 0, "WebDavMediaFeeder");

Finally we can use the custom implementation from within QML with little effort required:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtMultimedia 5.0
import me.fredl.types 1.0

Window {
    visible: true
    width: 640
    height: 480
    
    property string webDavUrl : "https://nextcloud.instance.host:443/remote.php/webdav/Nextcloud.mp4"
    
    Item {
        anchors.fill: parent

        // Media player for video and audio preview
        MediaPlayer {
            id: previewPlayer
            autoPlay: false
            onSourceChanged: {
                console.log("AV preview " + source)
            }
            onPlaybackStateChanged: {
                console.log("playback state " + playbackState)
            }
        }

        // Stream feeder
        WebDavMediaFeeder {
            id: mediaFeeder
            mediaPlayer: previewPlayer
            url: webDavUrl
        }
        VideoOutput {
            id: mediaView
            source: previewPlayer
            anchors.fill: parent
        }
        MouseArea {
            anchors.fill: parent
            onClicked: {
                mediaFeeder.play()
            }
        }
    }
}

This is all that's required for a custom HTTP(S) request to be fed to a QML-instantiated media player and played back inside Qt's scene graph renderer.

Alfred Neumayer

Born and raised in Austria, enjoys making music and writing software.

Austria