Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qffmpegmediadataholder.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "playbackengine/qffmpegmediadataholder_p.h"
5
9
10#include <QtMultimedia/qplaybackoptions.h>
11#include <QtMultimedia/private/qmediametadata_p.h>
12#include <QtCore/qiodevice.h>
13#include <QtCore/qdatetime.h>
14#include <QtCore/qloggingcategory.h>
15#include <QtCore/private/qminimalflatset_p.h>
16
17#include <optional>
18
20
21Q_STATIC_LOGGING_CATEGORY(qLcMediaDataHolder, "qt.multimedia.ffmpeg.mediadataholder")
22
23namespace QFFmpeg {
24
25static std::optional<TrackDuration> streamDuration(const AVStream &stream)
26{
27 if (stream.duration > 0)
28 return toTrackDuration(AVStreamDuration(stream.duration), &stream);
29
30 // In some cases ffmpeg reports negative duration that is definitely invalid.
31 // However, the correct duration may be read from the metadata.
32
33 if (stream.duration < 0 && stream.duration != AV_NOPTS_VALUE) {
34 qCWarning(qLcMediaDataHolder) << "AVStream duration" << stream.duration
35 << "is invalid. Taking it from the metadata";
36 }
37
38 if (const auto duration = av_dict_get(stream.metadata, "DURATION", nullptr, 0)) {
39 const auto time = QTime::fromString(QString::fromUtf8(duration->value));
40 return TrackDuration(qint64(1000) * time.msecsSinceStartOfDay());
41 }
42
43 return {};
44}
45
46static std::optional<TrackPosition> streamStart(const AVStream &stream,
47 const AVFormatContextUPtr &context)
48{
49 if (stream.start_time != AV_NOPTS_VALUE)
50 return toTrackPosition(AVStreamPosition(stream.start_time), &stream, context.get());
51 else
52 return {};
53}
54
55static QTransform displayMatrixToTransform(const int32_t *displayMatrix)
56{
57 // displayMatrix is stored as
58 //
59 // . -- X axis
60 // |
61 // | | a b u |
62 // Y | c d v |
63 // axis | x y w |
64 //
65 // where a, b, c, d, x, y are 16.16 fixed-point values,
66 // and u, v, w are 30.2 point values.
67 // Only a, b, c, d impacts on mirroring and rotation,
68 // so it's enough to propagate them to QTransform.
69 //
70 // If we were interested in getting proper XY scales,
71 // we would divide a,b,c,d by 2^16. The whole scale doesn't
72 // impact mirroring and rotation, so we don't do so.
73
74 auto toRotateMirrorValue = [displayMatrix](int index) {
75 // toRotateScaleValue would be:
76 // return displayMatrix[index] / qreal(1 << 16);
77 return displayMatrix[index];
78 };
79
80 return QTransform(toRotateMirrorValue(0), toRotateMirrorValue(1),
81 toRotateMirrorValue(3), toRotateMirrorValue(4),
82 0, 0);
83}
84
85static VideoTransformation streamTransformation(const AVStream *stream)
86{
87 Q_ASSERT(stream);
88
89 using SideDataSize = decltype(AVPacketSideData::size);
90 constexpr SideDataSize displayMatrixSize = sizeof(int32_t) * 9;
91 const AVPacketSideData *sideData = streamSideData(stream, AV_PKT_DATA_DISPLAYMATRIX);
92 if (!sideData || sideData->size < displayMatrixSize)
93 return {};
94
95 const auto displayMatrix = reinterpret_cast<const int32_t *>(sideData->data);
96 const QTransform transform = displayMatrixToTransform(displayMatrix);
97 const VideoTransformationOpt result = qVideoTransformationFromMatrix(transform);
98 if (!result) {
99 qCWarning(qLcMediaDataHolder)
100 << "Video stream contains malformed display matrix" << transform;
101 return {};
102 }
103 return *result;
104}
105
106static bool colorTransferSupportsHdr(const AVStream *stream)
107{
108 if (!stream)
109 return false;
110
111 const AVCodecParameters *codecPar = stream->codecpar;
112 if (!codecPar)
113 return false;
114
115 const QVideoFrameFormat::ColorTransfer colorTransfer = fromAvColorTransfer(codecPar->color_trc);
116
117 // Assume that content is using HDR if the color transfer supports high
118 // dynamic range. The video may still not utilize the extended range,
119 // but we can't determine the actual range without decoding frames.
120 return colorTransfer == QVideoFrameFormat::ColorTransfer_ST2084
121 || colorTransfer == QVideoFrameFormat::ColorTransfer_STD_B67;
122}
123
125{
126 // TODO: Add QMediaMetaData::Mirrored and take from it and QMediaMetaData::Orientation:
127 // int orientation = m_metaData.value(QMediaMetaData::Orientation).toInt();
128 // return static_cast<QtVideo::Rotation>(orientation);
129
130 const int streamIndex = m_currentAVStreamIndex[QPlatformMediaPlayer::VideoStream];
131 if (streamIndex < 0)
132 return {};
133
134 return streamTransformation(m_context->streams[streamIndex]);
135}
136
138{
139 return m_context.get();
140}
141
142int MediaDataHolder::currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
143{
144 return m_currentAVStreamIndex[trackType];
145}
146
176
177QPlatformMediaPlayer::TrackType MediaDataHolder::trackTypeFromMediaType(int mediaType)
178{
179 switch (mediaType) {
180 case AVMEDIA_TYPE_AUDIO:
181 return QPlatformMediaPlayer::AudioStream;
182 case AVMEDIA_TYPE_VIDEO:
183 return QPlatformMediaPlayer::VideoStream;
184 case AVMEDIA_TYPE_SUBTITLE:
185 return QPlatformMediaPlayer::SubtitleStream;
186 default:
187 return QPlatformMediaPlayer::NTrackTypes;
188 }
189}
190
191namespace {
192q23::expected<AVFormatContextUPtr, MediaDataHolder::ContextError>
193loadMedia(const QUrl &mediaUrl, QIODevice *stream, const QPlaybackOptions &playbackOptions,
194 const std::shared_ptr<ICancelToken> &cancelToken)
195{
196 using std::chrono::duration_cast;
197 using std::chrono::microseconds;
198 using std::chrono::milliseconds;
199
200 const QByteArray url = mediaUrl.toString(QUrl::PreferLocalFile).toUtf8();
201
202 AVFormatContextUPtr context{ avformat_alloc_context() };
203
204 if (stream) {
205 if (!stream->isOpen()) {
206 if (!stream->open(QIODevice::ReadOnly))
207 return q23::unexpected{
208 MediaDataHolder::ContextError{
209 QMediaPlayer::ResourceError,
210 QLatin1String("Could not open source device."),
211 },
212 };
213 }
214
215 auto seek = &seekQIODevice;
216
217 if (!stream->isSequential()) {
218 stream->seek(0);
219 } else {
220 context->ctx_flags |= AVFMTCTX_UNSEEKABLE;
221 seek = nullptr;
222 }
223
224 constexpr int bufferSize = 32768;
225 unsigned char *buffer = (unsigned char *)av_malloc(bufferSize);
226 context->pb = avio_alloc_context(buffer, bufferSize, false, stream, &readQIODevice, nullptr,
227 seek);
228 }
229
230 AVDictionaryHolder dict;
231 using RtmpProtocols =
232 QMinimalVarLengthFlatSet<std::basic_string_view<char16_t>, 6, std::less<>>;
233
234 static const RtmpProtocols rtmpProtocols{
235 u"rtmp", u"rtmpe", u"rtmps", u"rtmpt", u"rtmpse", u"rtmpte",
236 };
237
238 // for rtmp streams, the `timout` parameter implies acting as a server:
239 // https://ffmpeg.org/ffmpeg-protocols.html#rtmp
240 // This is not the semantics of QPlaybackOptions::networkTimeout, and will cause failures when
241 // opening streams
242 const bool setNetworkTimeout = !rtmpProtocols.contains(mediaUrl.scheme());
243
244 if (setNetworkTimeout) {
245 const milliseconds timeout = playbackOptions.networkTimeout();
246 av_dict_set_int(dict, "timeout", duration_cast<microseconds>(timeout).count(), 0);
247 qCDebug(qLcMediaDataHolder) << "Using custom network timeout:" << timeout;
248 }
249
250 {
251 const int probeSize = playbackOptions.probeSize();
252 if (probeSize != -1) {
253 constexpr int minProbeSizeFFmpeg = 32;
254 if (probeSize >= minProbeSizeFFmpeg) {
255 av_dict_set_int(dict, "probesize", probeSize, 0);
256 qCDebug(qLcMediaDataHolder) << "Using custom probesize" << probeSize;
257 } else
258 qCWarning(qLcMediaDataHolder) << "Invalid probe size, using default";
259 }
260 }
261
262 const QByteArray protocolWhitelist = qgetenv("QT_FFMPEG_PROTOCOL_WHITELIST");
263 if (!protocolWhitelist.isNull())
264 av_dict_set(dict, "protocol_whitelist", protocolWhitelist.data(), 0);
265
266 if (mediaUrl.scheme().compare(u"rtsp") == 0) {
267 const QByteArray rtspTransport = qgetenv("QT_FFMPEG_RTSP_TRANSPORT").trimmed();
268 if (!rtspTransport.isEmpty()) {
269 av_dict_set(dict, "rtsp_transport", rtspTransport.constData(), 0);
270 qCDebug(qLcMediaDataHolder) << "Using custom RTSP transport:" << rtspTransport;
271 }
272 }
273
274 if (playbackOptions.playbackIntent() == QPlaybackOptions::PlaybackIntent::LowLatencyStreaming) {
275 av_dict_set(dict, "fflags", "nobuffer", 0);
276 av_dict_set_int(dict, "flush_packets", 1, 0);
277 qCDebug(qLcMediaDataHolder) << "Enabled low latency streaming";
278 }
279
280 // QTBUG-145590: for hls streams, we want to disable http persistent connections to allow FFmpeg
281 // (before FFmpeg 8?) to mix raw and encrypted streams
282 // compare https://trac.ffmpeg.org/ticket/10599
283 if (avformat_version() < AV_VERSION_INT(62, 12, 100))
284 av_dict_set_int(dict, "http_persistent", 0, 0);
285
286 context->interrupt_callback.opaque = cancelToken.get();
287 context->interrupt_callback.callback = [](void *opaque) {
288 const auto *cancelToken = static_cast<const ICancelToken *>(opaque);
289 if (cancelToken && cancelToken->isCancelled())
290 return 1;
291 return 0;
292 };
293
294 int ret = 0;
295 {
296 AVFormatContext *contextRaw = context.release();
297 ret = avformat_open_input(&contextRaw, url.constData(), nullptr, dict);
298 context.reset(contextRaw);
299 }
300
301 if (ret < 0) {
302 auto code = QMediaPlayer::ResourceError;
303 if (ret == AVERROR(EACCES))
304 code = QMediaPlayer::AccessDeniedError;
305 else if (ret == AVERROR(EINVAL) || ret == AVERROR_INVALIDDATA)
306 code = QMediaPlayer::FormatError;
307
308 qCWarning(qLcMediaDataHolder)
309 << "Could not open media. FFmpeg error description:" << AVError(ret);
310
311 return q23::unexpected{
312 MediaDataHolder::ContextError{ code, QMediaPlayer::tr("Could not open file") },
313 };
314 }
315
316 ret = avformat_find_stream_info(context.get(), nullptr);
317 if (ret < 0) {
318 return q23::unexpected{
319 MediaDataHolder::ContextError{
320 QMediaPlayer::FormatError,
321 QMediaPlayer::tr("Could not find stream information for media file") },
322 };
323 }
324
325 if (qLcMediaDataHolder().isInfoEnabled())
326 av_dump_format(context.get(), 0, url.constData(), 0);
327
328
329 return context;
330}
331
332} // namespace
333
334MediaDataHolder::Maybe MediaDataHolder::create(const QUrl &url, QIODevice *stream,
335 const QPlaybackOptions &options,
336 const std::shared_ptr<ICancelToken> &cancelToken)
337{
338 q23::expected context = loadMedia(url, stream, options, cancelToken);
339 if (context) {
340 // MediaDataHolder is wrapped in a shared pointer to interop with signal/slot mechanism
341 return std::make_shared<MediaDataHolder>(
342 MediaDataHolder{ std::move(context.value()), cancelToken });
343 }
344 return q23::unexpected{ context.error() };
345}
346
347MediaDataHolder::MediaDataHolder(AVFormatContextUPtr context,
348 const std::shared_ptr<ICancelToken> &cancelToken)
350{
351 Q_ASSERT(context);
352
353 m_context = std::move(context);
354 m_isSeekable = !(m_context->ctx_flags & AVFMTCTX_UNSEEKABLE);
355
356 std::optional<TrackDuration> mediaDuration;
357
358 for (unsigned int i = 0; i < m_context->nb_streams; ++i) {
359
360 const auto *stream = m_context->streams[i];
361 const auto trackType = trackTypeFromMediaType(stream->codecpar->codec_type);
362
363 if (trackType == QPlatformMediaPlayer::NTrackTypes)
364 continue;
365
366 if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC)
367 continue; // Ignore attached picture streams because we treat them as metadata
368
369 if (stream->time_base.num <= 0 || stream->time_base.den <= 0) {
370 // An invalid stream timebase is not expected to be given by FFmpeg
371 qCWarning(qLcMediaDataHolder) << "A stream for the track type" << trackType
372 << "has an invalid timebase:" << stream->time_base;
373 continue;
374 }
375
376 auto metaData = QFFmpegMetaData::fromAVMetaData(stream->metadata);
377 const bool isDefault = stream->disposition & AV_DISPOSITION_DEFAULT;
378
379 if (trackType != QPlatformMediaPlayer::SubtitleStream) {
380 insertMediaData(metaData, trackType, stream);
381
382 if (isDefault && m_requestedStreams[trackType] < 0)
383 m_requestedStreams[trackType] = m_streamMap[trackType].size();
384 }
385
386 if (auto duration = streamDuration(*stream))
387 metaData.insert(QMediaMetaData::Duration, toUserDuration(*duration).get());
388
389 m_streamMap[trackType].append({ (int)i, isDefault, metaData });
390 }
391
392 // With some media files, streams may be lacking duration info. Let's
393 // get it from ffmpeg's duration estimation instead.
394 if (m_context->duration > 0ll)
395 mediaDuration = toTrackDuration(AVContextDuration(m_context->duration));
396
397 if (!mediaDuration) {
398 std::optional<AVContextDuration> contextStart = contextStartOffset(m_context.get());
399
400 std::optional<TrackPosition> startPosition = [&]() -> std::optional<TrackPosition> {
401 if (contextStart)
402 return toTrackDuration(*contextStart).asTimePoint();
403 return std::nullopt;
404 }();
405
406 std::optional<TrackPosition> endPosition;
407
408 QSpan streams{ m_context->streams, qsizetype(m_context->nb_streams) };
409 for (const AVStream *stream : streams) {
410 std::optional<TrackPosition> currentStreamStartPosition =
411 streamStart(*stream, m_context);
412 std::optional<TrackDuration> currentStreamDuration = streamDuration(*stream);
413 std::optional<TrackPosition> currentStreamEndPosition =
414 [&]() -> std::optional<TrackPosition> {
415 if (currentStreamStartPosition && currentStreamDuration)
416 return *currentStreamStartPosition + *currentStreamDuration;
417 return std::nullopt;
418 }();
419
420 if (startPosition)
421 startPosition = std::min(*startPosition,
422 currentStreamStartPosition.value_or(*startPosition));
423 else
424 startPosition = currentStreamStartPosition;
425
426 endPosition = std::max(endPosition, currentStreamEndPosition);
427 }
428
429 if (endPosition && startPosition)
430 mediaDuration = *endPosition - *startPosition;
431 }
432
433 m_duration = mediaDuration.value_or(TrackDuration::zero());
434
435 for (auto trackType :
436 { QPlatformMediaPlayer::VideoStream, QPlatformMediaPlayer::AudioStream }) {
437 auto &requestedStream = m_requestedStreams[trackType];
438 auto &streamMap = m_streamMap[trackType];
439
440 if (requestedStream < 0 && !streamMap.empty())
441 requestedStream = 0;
442
443 if (requestedStream >= 0)
444 m_currentAVStreamIndex[trackType] = streamMap[requestedStream].avStreamIndex;
445 }
446
447 updateMetaData();
448}
449
450namespace {
451
452/*!
453 \internal
454
455 Attempt to find an attached picture from the context's streams.
456 This will find ID3v2 pictures on audio files, and also pictures
457 attached to videos.
458 */
459QImage getAttachedPicture(const AVFormatContext *context)
460{
461 if (!context)
462 return {};
463
464 for (unsigned int i = 0; i < context->nb_streams; ++i) {
465 const AVStream* stream = context->streams[i];
466 if (!stream || !(stream->disposition & AV_DISPOSITION_ATTACHED_PIC))
467 continue;
468
469 const AVPacket *compressedImage = &stream->attached_pic;
470 if (!compressedImage || !compressedImage->data || compressedImage->size <= 0)
471 continue;
472
473 // Feed raw compressed data to QImage::fromData, which will decompress it
474 // if it is a recognized format.
475 QImage image = QImage::fromData({ compressedImage->data, compressedImage->size });
476 if (!image.isNull())
477 return image;
478 }
479
480 return {};
481}
482
483} // namespace
484
485void MediaDataHolder::updateMetaData()
486{
487 m_metaData = {};
488
489 if (!m_context)
490 return;
491
492 m_metaData = QFFmpegMetaData::fromAVMetaData(m_context->metadata);
493 m_metaData.insert(QMediaMetaData::FileFormat,
494 QVariant::fromValue(QFFmpegMediaFormatInfo::fileFormatForAVInputFormat(
495 *m_context->iformat)));
496 m_metaData.insert(QMediaMetaData::Duration, toUserDuration(m_duration).get());
497
498 if (!m_cachedThumbnail.has_value())
499 m_cachedThumbnail = getAttachedPicture(m_context.get());
500
501 QtMultimediaPrivate::setCoverArtImage(m_metaData, *m_cachedThumbnail);
502
503 for (auto trackType :
504 { QPlatformMediaPlayer::AudioStream, QPlatformMediaPlayer::VideoStream }) {
505 const auto streamIndex = m_currentAVStreamIndex[trackType];
506 if (streamIndex >= 0)
507 insertMediaData(m_metaData, trackType, m_context->streams[streamIndex]);
508 }
509}
510
512{
513 if (!m_context)
514 return false;
515
517 streamNumber = -1;
519 return false;
522
524 qCDebug(qLcMediaDataHolder) << ">>>>> change track" << type << "from" << oldIndex << "to"
525 << avStreamIndex;
526
527 // TODO: maybe add additional verifications
529
531
532 return true;
533}
534
539
541 QPlatformMediaPlayer::TrackType trackType) const
542{
543 Q_ASSERT(trackType < QPlatformMediaPlayer::NTrackTypes);
544
545 return m_streamMap[trackType];
546}
547
548} // namespace QFFmpeg
549
550QT_END_NAMESPACE
MediaDataHolder(AVFormatContextUPtr context, const std::shared_ptr< ICancelToken > &cancelToken)
int currentStreamIndex(QPlatformMediaPlayer::TrackType trackType) const
VideoTransformation transformation() const
const QList< StreamInfo > & streamInfo(QPlatformMediaPlayer::TrackType trackType) const
Definition qlist.h:82
static VideoTransformation streamTransformation(const AVStream *stream)
static std::optional< TrackPosition > streamStart(const AVStream &stream, const AVFormatContextUPtr &context)
static bool colorTransferSupportsHdr(const AVStream *stream)
static void insertMediaData(QMediaMetaData &metaData, QPlatformMediaPlayer::TrackType trackType, const AVStream *stream)
static std::optional< TrackDuration > streamDuration(const AVStream &stream)
QT_MANGLE_NAMESPACE(QMacScreenCaptureStreamDelegate) QMacScreenCaptureStreamDelegate
static QTransform displayMatrixToTransform(const int32_t *displayMatrix)
Combined button and popup list for selecting options.
virtual bool isCancelled() const =0