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.