4#include "playbackengine/qffmpegmediadataholder_p.h"
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>
17#if defined(Q_OS_ANDROID)
18# include <QtFFmpegMediaPluginImpl/private/qandroidcacertificates_p.h>
25Q_STATIC_LOGGING_CATEGORY(qLcMediaDataHolder,
"qt.multimedia.ffmpeg.mediadataholder")
33 if (stream.duration > 0)
34 return toTrackDuration(AVStreamDuration(stream.duration), &stream);
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";
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());
53 const AVDemuxerContextUPtr &context)
55 if (stream.start_time != AV_NOPTS_VALUE)
56 return toTrackPosition(AVStreamPosition(stream.start_time), &stream, context.get());
80 auto toRotateMirrorValue = [displayMatrix](
int index) {
83 return displayMatrix[index];
86 return QTransform(toRotateMirrorValue(0), toRotateMirrorValue(1),
87 toRotateMirrorValue(3), toRotateMirrorValue(4),
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)
101 const auto displayMatrix =
reinterpret_cast<
const int32_t *>(sideData->data);
102 const QTransform transform = displayMatrixToTransform(displayMatrix);
103 const VideoTransformationOpt result = qVideoTransformationFromMatrix(transform);
105 qCWarning(qLcMediaDataHolder)
106 <<
"Video stream contains malformed display matrix" << transform;
117 const AVCodecParameters *codecPar = stream->codecpar;
121 const QVideoFrameFormat::ColorTransfer colorTransfer = fromAvColorTransfer(codecPar->color_trc);
126 return colorTransfer == QVideoFrameFormat::ColorTransfer_ST2084
127 || colorTransfer == QVideoFrameFormat::ColorTransfer_STD_B67;
136 const int streamIndex = m_currentAVStreamIndex[TrackType::VideoStream];
140 return streamTransformation(m_context->streams[streamIndex]);
145 return m_context.get();
150 return m_currentAVStreamIndex[trackType];
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;
197q23::expected<AVDemuxerContextUPtr, MediaDataHolder::ContextError>
198loadMedia(
const QUrl &mediaUrl, QIODevice *stream,
const QPlaybackOptions &playbackOptions,
199 const std::shared_ptr<ICancelToken> &cancelToken)
201 using std::chrono::duration_cast;
202 using std::chrono::microseconds;
203 using std::chrono::milliseconds;
205 const QByteArray url = mediaUrl.toString(QUrl::PreferLocalFile).toUtf8();
207 AVDemuxerContextUPtr context{ avformat_alloc_context() };
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."),
220 auto seek = &seekQIODevice;
222 if (!stream->isSequential()) {
225 context->ctx_flags |= AVFMTCTX_UNSEEKABLE;
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,
235 AVDictionaryHolder dict;
236 using RtmpProtocols =
237 QMinimalVarLengthFlatSet<std::basic_string_view<
char16_t>, 6, std::less<>>;
239 static const RtmpProtocols rtmpProtocols{
240 u"rtmp", u"rtmpe", u"rtmps", u"rtmpt", u"rtmpse", u"rtmpte",
247 const bool setNetworkTimeout = !rtmpProtocols.contains(mediaUrl.scheme());
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;
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;
263 qCWarning(qLcMediaDataHolder) <<
"Invalid probe size, using default";
267 const QByteArray protocolWhitelist = qgetenv(
"QT_FFMPEG_PROTOCOL_WHITELIST");
268 if (!protocolWhitelist.isNull())
269 av_dict_set(dict,
"protocol_whitelist", protocolWhitelist.data(), 0);
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;
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";
288 if (avformat_version() < AV_VERSION_INT(62, 12, 100))
289 av_dict_set_int(dict,
"http_persistent", 0, 0);
302 const QString caBundlePath = androidFFmpegCaBundlePath();
303 if (!caBundlePath.isEmpty())
304 av_dict_set(dict,
"ca_file", caBundlePath.toUtf8().constData(), 0);
310 context->interrupt_callback.opaque = cancelToken.get();
311 context->interrupt_callback.callback = [](
void *opaque) {
312 const auto *cancelToken =
static_cast<
const ICancelToken *>(opaque);
319 AVIOContext *customIo = context->pb;
321 AVFormatContext *contextRaw = context.release();
322 ret = avformat_open_input(&contextRaw, url.constData(),
nullptr, dict);
323 context.reset(contextRaw);
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;
333 qCWarning(qLcMediaDataHolder)
334 <<
"Could not open media. FFmpeg error description:" << AVError(ret);
336 freeAVIOContext(customIo);
337 return q23::unexpected{
338 MediaDataHolder::ContextError{ code, QMediaPlayer::tr(
"Could not open file") },
342 ret = avformat_find_stream_info(context.get(),
nullptr);
344 return q23::unexpected{
345 MediaDataHolder::ContextError{
346 QMediaPlayer::FormatError,
347 QMediaPlayer::tr(
"Could not find stream information for media file") },
351 if (qLcMediaDataHolder().isInfoEnabled())
352 av_dump_format(context.get(), 0, url.constData(), 0);
361 const QPlaybackOptions &options,
362 const std::shared_ptr<ICancelToken> &cancelToken)
364 q23::expected context = loadMedia(url, stream, options, cancelToken);
367 return std::make_shared<MediaDataHolder>(
368 MediaDataHolder{ std::move(context.value()), cancelToken });
370 return q23::unexpected{ context.error() };
374 const std::shared_ptr<ICancelToken> &cancelToken)
379 m_context = std::move(context);
380 m_isSeekable = !(m_context->ctx_flags & AVFMTCTX_UNSEEKABLE);
382 std::optional<TrackDuration> mediaDuration;
384 for (
unsigned int i = 0; i < m_context->nb_streams; ++i) {
386 const auto *stream = m_context->streams[i];
387 const auto trackType = trackTypeFromMediaType(stream->codecpar->codec_type);
392 if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC)
395 if (stream->time_base.num <= 0 || stream->time_base.den <= 0) {
397 qCWarning(qLcMediaDataHolder) <<
"A stream for the track type" << trackType
398 <<
"has an invalid timebase:" << stream->time_base;
402 auto metaData = QFFmpegMetaData::fromAVMetaData(stream->metadata);
403 const bool isDefault = stream->disposition & AV_DISPOSITION_DEFAULT;
405 if (trackType != TrackType::SubtitleStream) {
408 auto &requestedStream = m_requestedStreams[*trackType];
409 if (isDefault && requestedStream < 0)
410 requestedStream = m_streamMap[*trackType].size();
413 if (
auto duration = streamDuration(*stream))
414 metaData.insert(QMediaMetaData::Duration, toUserDuration(*duration).get());
416 m_streamMap[*trackType].append({ (
int)i, isDefault, metaData });
421 if (m_context->duration > 0ll)
422 mediaDuration = toTrackDuration(AVContextDuration(m_context->duration));
424 if (!mediaDuration) {
425 std::optional<AVContextDuration> contextStart = contextStartOffset(m_context.get());
427 std::optional<TrackPosition> startPosition = [&]() -> std::optional<TrackPosition> {
429 return toTrackDuration(*contextStart).asTimePoint();
433 std::optional<TrackPosition> endPosition;
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;
448 startPosition = std::min(*startPosition,
449 currentStreamStartPosition.value_or(*startPosition));
451 startPosition = currentStreamStartPosition;
453 endPosition = std::max(endPosition, currentStreamEndPosition);
456 if (endPosition && startPosition)
457 mediaDuration = *endPosition - *startPosition;
460 m_duration = mediaDuration.value_or(TrackDuration::zero());
462 for (
auto trackType : { TrackType::VideoStream,
463 TrackType::AudioStream }) {
464 auto &requestedStream = m_requestedStreams[trackType];
465 auto &streamMap = m_streamMap[trackType];
467 if (requestedStream < 0 && !streamMap.empty())
470 if (requestedStream >= 0)
471 m_currentAVStreamIndex[trackType] =
472 streamMap[requestedStream].avStreamIndex;
481
482
483
484
485
486
487QImage getAttachedPicture(
const AVFormatContext *context)
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))
497 const AVPacket *compressedImage = &stream->attached_pic;
498 if (!compressedImage || !compressedImage->data || compressedImage->size <= 0)
503 QImage image = QImage::fromData({ compressedImage->data, compressedImage->size });
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());
526 if (!m_cachedThumbnail.has_value())
527 m_cachedThumbnail = getAttachedPicture(m_context.get());
529 QtMultimediaPrivate::setCoverArtImage(m_metaData, *m_cachedThumbnail);
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]);
544 if (streamNumber < 0 || streamNumber >= m_streamMap[type].size())
546 if (m_requestedStreams[type] == streamNumber)
548 m_requestedStreams[type] = streamNumber;
549 const int avStreamIndex = m_streamMap[type].value(streamNumber).avStreamIndex;
551 const int oldIndex = m_currentAVStreamIndex[type];
552 qCDebug(qLcMediaDataHolder) <<
">>>>> change track" << type <<
"from" << oldIndex <<
"to"
556 m_currentAVStreamIndex[type] = avStreamIndex;
565 return m_requestedStreams[type];
569 TrackType trackType)
const
571 return m_streamMap[trackType];
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