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