4#include "playbackengine/qffmpegmediadataholder_p.h"
13#include <QtMultimedia/qplaybackoptions.h>
14#include <QtMultimedia/private/qmediametadata_p.h>
19#include <QtCore/private/qminimalflatset_p.h>
29 if (stream.duration > 0)
30 return toTrackDuration(AVStreamDuration(stream.duration), &stream);
35 if (stream.duration < 0 && stream.duration != AV_NOPTS_VALUE) {
36 qCWarning(qLcMediaDataHolder) <<
"AVStream duration" << stream.duration
37 <<
"is invalid. Taking it from the metadata";
40 if (
const auto duration = av_dict_get(stream.metadata,
"DURATION",
nullptr, 0)) {
41 const auto time = QTime::fromString(QString::fromUtf8(duration->value));
42 return TrackDuration(qint64(1000) * time.msecsSinceStartOfDay());
49 const AVFormatContextUPtr &context)
51 if (stream.start_time != AV_NOPTS_VALUE)
52 return toTrackPosition(AVStreamPosition(stream.start_time), &stream, context.get());
76 auto toRotateMirrorValue = [displayMatrix](
int index) {
79 return displayMatrix[index];
82 return QTransform(toRotateMirrorValue(0), toRotateMirrorValue(1),
83 toRotateMirrorValue(3), toRotateMirrorValue(4),
91 using SideDataSize =
decltype(AVPacketSideData::size);
92 constexpr SideDataSize displayMatrixSize =
sizeof(int32_t) * 9;
93 const AVPacketSideData *sideData = streamSideData(stream, AV_PKT_DATA_DISPLAYMATRIX);
94 if (!sideData || sideData->size < displayMatrixSize)
97 const auto displayMatrix =
reinterpret_cast<
const int32_t *>(sideData->data);
98 const QTransform transform = displayMatrixToTransform(displayMatrix);
99 const VideoTransformationOpt result = qVideoTransformationFromMatrix(transform);
102 <<
"Video stream contains malformed display matrix" << transform;
113 const AVCodecParameters *codecPar = stream->codecpar;
117 const QVideoFrameFormat::ColorTransfer colorTransfer = fromAvColorTransfer(codecPar->color_trc);
122 return colorTransfer == QVideoFrameFormat::ColorTransfer_ST2084
123 || colorTransfer == QVideoFrameFormat::ColorTransfer_STD_B67;
132 const int streamIndex = m_currentAVStreamIndex[QPlatformMediaPlayer::VideoStream];
136 return streamTransformation(m_context->streams[streamIndex]);
141 return m_context.get();
146 return m_currentAVStreamIndex[trackType];
182 case AVMEDIA_TYPE_AUDIO:
183 return QPlatformMediaPlayer::AudioStream;
184 case AVMEDIA_TYPE_VIDEO:
185 return QPlatformMediaPlayer::VideoStream;
186 case AVMEDIA_TYPE_SUBTITLE:
187 return QPlatformMediaPlayer::SubtitleStream;
189 return QPlatformMediaPlayer::NTrackTypes;
194q23::expected<AVFormatContextUPtr, MediaDataHolder::ContextError>
195loadMedia(
const QUrl &mediaUrl, QIODevice *stream,
const QPlaybackOptions &playbackOptions,
198 using std::chrono::duration_cast;
199 using std::chrono::microseconds;
200 using std::chrono::milliseconds;
202 const QByteArray url = mediaUrl.toString(QUrl::PreferLocalFile).toUtf8();
204 AVFormatContextUPtr context{ avformat_alloc_context() };
207 if (!stream->isOpen()) {
208 if (!stream->open(QIODevice::ReadOnly))
209 return q23::unexpected{
210 MediaDataHolder::ContextError{
211 QMediaPlayer::ResourceError,
212 QLatin1String(
"Could not open source device."),
217 auto seek = &seekQIODevice;
219 if (!stream->isSequential()) {
222 context->ctx_flags |= AVFMTCTX_UNSEEKABLE;
226 constexpr int bufferSize = 32768;
227 unsigned char *buffer = (
unsigned char *)av_malloc(bufferSize);
228 context->pb = avio_alloc_context(buffer, bufferSize,
false, stream, &readQIODevice,
nullptr,
232 AVDictionaryHolder dict;
233 using RtmpProtocols =
234 QMinimalVarLengthFlatSet<std::basic_string_view<
char16_t>, 6, std::less<>>;
236 static const RtmpProtocols rtmpProtocols{
237 u"rtmp", u"rtmpe", u"rtmps", u"rtmpt", u"rtmpse", u"rtmpte",
244 const bool setNetworkTimeout = !rtmpProtocols.contains(mediaUrl.scheme());
246 if (setNetworkTimeout) {
247 const milliseconds timeout = playbackOptions.networkTimeout();
248 av_dict_set_int(dict,
"timeout", duration_cast<microseconds>(timeout).count(), 0);
249 qCDebug(qLcMediaDataHolder) <<
"Using custom network timeout:" << timeout;
253 const int probeSize = playbackOptions.probeSize();
254 if (probeSize != -1) {
255 constexpr int minProbeSizeFFmpeg = 32;
256 if (probeSize >= minProbeSizeFFmpeg) {
257 av_dict_set_int(dict,
"probesize", probeSize, 0);
258 qCDebug(qLcMediaDataHolder) <<
"Using custom probesize" << probeSize;
260 qCWarning(qLcMediaDataHolder) <<
"Invalid probe size, using default";
264 const QByteArray protocolWhitelist = qgetenv(
"QT_FFMPEG_PROTOCOL_WHITELIST");
265 if (!protocolWhitelist.isNull())
266 av_dict_set(dict,
"protocol_whitelist", protocolWhitelist.data(), 0);
268 if (mediaUrl.scheme().compare(u"rtsp") == 0) {
269 const QByteArray rtspTransport = qgetenv(
"QT_FFMPEG_RTSP_TRANSPORT").trimmed();
270 if (!rtspTransport.isEmpty()) {
271 av_dict_set(dict,
"rtsp_transport", rtspTransport.constData(), 0);
272 qCDebug(qLcMediaDataHolder) <<
"Using custom RTSP transport:" << rtspTransport;
276 if (playbackOptions.playbackIntent() == QPlaybackOptions::PlaybackIntent::LowLatencyStreaming) {
277 av_dict_set(dict,
"fflags",
"nobuffer", 0);
278 av_dict_set_int(dict,
"flush_packets", 1, 0);
279 qCDebug(qLcMediaDataHolder) <<
"Enabled low latency streaming";
285 if (avformat_version() < AV_VERSION_INT(62, 12, 100))
286 av_dict_set_int(dict,
"http_persistent", 0, 0);
288 context->interrupt_callback.opaque = cancelToken.get();
289 context->interrupt_callback.callback = [](
void *opaque) {
290 const auto *cancelToken =
static_cast<
const ICancelToken *>(opaque);
298 AVFormatContext *contextRaw = context.release();
299 ret = avformat_open_input(&contextRaw, url.constData(),
nullptr, dict);
300 context.reset(contextRaw);
304 auto code = QMediaPlayer::ResourceError;
305 if (ret == AVERROR(EACCES))
306 code = QMediaPlayer::AccessDeniedError;
307 else if (ret == AVERROR(EINVAL) || ret == AVERROR_INVALIDDATA)
308 code = QMediaPlayer::FormatError;
311 <<
"Could not open media. FFmpeg error description:" << AVError(ret);
313 return q23::unexpected{
314 MediaDataHolder::ContextError{ code, QMediaPlayer::tr(
"Could not open file") },
318 ret = avformat_find_stream_info(context.get(),
nullptr);
320 return q23::unexpected{
321 MediaDataHolder::ContextError{
322 QMediaPlayer::FormatError,
323 QMediaPlayer::tr(
"Could not find stream information for media file") },
327 if (qLcMediaDataHolder().isInfoEnabled())
328 av_dump_format(context.get(), 0, url.constData(), 0);
337 const QPlaybackOptions &options,
340 q23::expected context = loadMedia(url, stream, options, cancelToken);
346 return q23::unexpected{ context.error() };
351 : m_cancelToken{ cancelToken }
355 m_context = std::move(context);
356 m_isSeekable = !(m_context->ctx_flags & AVFMTCTX_UNSEEKABLE);
358 std::optional<TrackDuration> mediaDuration;
360 for (
unsigned int i = 0; i < m_context->nb_streams; ++i) {
362 const auto *stream = m_context->streams[i];
363 const auto trackType = trackTypeFromMediaType(stream->codecpar->codec_type);
365 if (trackType == QPlatformMediaPlayer::NTrackTypes)
368 if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC)
371 if (stream->time_base.num <= 0 || stream->time_base.den <= 0) {
373 qCWarning(qLcMediaDataHolder) <<
"A stream for the track type" << trackType
374 <<
"has an invalid timebase:" << stream->time_base;
378 auto metaData = QFFmpegMetaData::fromAVMetaData(stream->metadata);
379 const bool isDefault = stream->disposition & AV_DISPOSITION_DEFAULT;
381 if (trackType != QPlatformMediaPlayer::SubtitleStream) {
384 if (isDefault && m_requestedStreams[trackType] < 0)
385 m_requestedStreams[trackType] = m_streamMap[trackType].size();
388 if (
auto duration = streamDuration(*stream))
389 metaData.insert(QMediaMetaData::Duration, toUserDuration(*duration).get());
391 m_streamMap[trackType].append({ (
int)i, isDefault, metaData });
396 if (m_context->duration > 0ll)
397 mediaDuration = toTrackDuration(AVContextDuration(m_context->duration));
399 if (!mediaDuration) {
400 std::optional<AVContextDuration> contextStart = contextStartOffset(m_context.get());
402 std::optional<TrackPosition> startPosition = [&]() -> std::optional<TrackPosition> {
404 return toTrackDuration(*contextStart).asTimePoint();
408 std::optional<TrackPosition> endPosition;
410 QSpan streams{ m_context->streams, qsizetype(m_context->nb_streams) };
411 for (
const AVStream *stream : streams) {
412 std::optional<TrackPosition> currentStreamStartPosition =
413 streamStart(*stream, m_context);
414 std::optional<TrackDuration> currentStreamDuration = streamDuration(*stream);
415 std::optional<TrackPosition> currentStreamEndPosition =
416 [&]() -> std::optional<TrackPosition> {
417 if (currentStreamStartPosition && currentStreamDuration)
418 return *currentStreamStartPosition + *currentStreamDuration;
423 startPosition = std::min(*startPosition,
424 currentStreamStartPosition.value_or(*startPosition));
426 startPosition = currentStreamStartPosition;
428 endPosition = std::max(endPosition, currentStreamEndPosition);
431 if (endPosition && startPosition)
432 mediaDuration = *endPosition - *startPosition;
435 m_duration = mediaDuration.value_or(TrackDuration::zero());
437 for (
auto trackType :
438 { QPlatformMediaPlayer::VideoStream, QPlatformMediaPlayer::AudioStream }) {
439 auto &requestedStream = m_requestedStreams[trackType];
440 auto &streamMap = m_streamMap[trackType];
442 if (requestedStream < 0 && !streamMap.empty())
445 if (requestedStream >= 0)
446 m_currentAVStreamIndex[trackType] = streamMap[requestedStream].avStreamIndex;
455
456
457
458
459
460
461QImage getAttachedPicture(
const AVFormatContext *context)
466 for (
unsigned int i = 0; i < context->nb_streams; ++i) {
467 const AVStream* stream = context->streams[i];
468 if (!stream || !(stream->disposition & AV_DISPOSITION_ATTACHED_PIC))
471 const AVPacket *compressedImage = &stream->attached_pic;
472 if (!compressedImage || !compressedImage->data || compressedImage->size <= 0)
477 QImage image = QImage::fromData({ compressedImage->data, compressedImage->size });
494 m_metaData = QFFmpegMetaData::fromAVMetaData(m_context->metadata);
495 m_metaData.insert(QMediaMetaData::FileFormat,
496 QVariant::fromValue(QFFmpegMediaFormatInfo::fileFormatForAVInputFormat(
497 *m_context->iformat)));
498 m_metaData.insert(QMediaMetaData::Duration, toUserDuration(m_duration).get());
500 if (!m_cachedThumbnail.has_value())
501 m_cachedThumbnail = getAttachedPicture(m_context.get());
503 QtMultimediaPrivate::setCoverArtImage(m_metaData, *m_cachedThumbnail);
505 for (
auto trackType :
506 { QPlatformMediaPlayer::AudioStream, QPlatformMediaPlayer::VideoStream }) {
507 const auto streamIndex = m_currentAVStreamIndex[trackType];
508 if (streamIndex >= 0)
509 insertMediaData(m_metaData, trackType, m_context->streams[streamIndex]);
543 QPlatformMediaPlayer::TrackType trackType)
const
545 Q_ASSERT(trackType < QPlatformMediaPlayer::NTrackTypes);
547 return m_streamMap[trackType];
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)
#define qCWarning(category,...)
#define qCDebug(category,...)
#define Q_STATIC_LOGGING_CATEGORY(name,...)
virtual bool isCancelled() const =0