diff --git a/src/sources/metadatasourcetaglib.cpp b/src/sources/metadatasourcetaglib.cpp index f20e2af24d5a..f6bc46e0ee6f 100644 --- a/src/sources/metadatasourcetaglib.cpp +++ b/src/sources/metadatasourcetaglib.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "track/taglib/trackmetadata.h" @@ -108,11 +109,27 @@ MetadataSourceTagLib::importTrackMetadataAndCoverImage( << "of type" << fileTypeToString(m_fileType); } - // Rationale: If a file contains different types of tags only - // a single type of tag will be read. Tag types are read in a - // fixed order. Both track metadata and cover art will be read - // from the same tag types. Only the first available tag type - // is read and data in subsequent tags is ignored. +// Rationale: If a file contains different types of tags only +// a single type of tag will be read. Tag types are read in a +// fixed order. Both track metadata and cover art will be read +// from the same tag types. Only the first available tag type +// is read and data in subsequent tags is ignored. + + // Bypass TagLib for Matroska/WebM files — TagLib does not support + // these container formats (returns FileType::Unknown). Duration and + // metadata are read from the FFmpeg-based sound source instead + // (see soundsource.cpp for the mime-type bypass). + if (m_fileType == taglib::FileType::Unknown) { + QString fileSuffix = QFileInfo(m_fileName).suffix().toLower(); + if (fileSuffix == QLatin1String("mkv") || fileSuffix == QLatin1String("webm")) { + kLogger.debug() + << "TagLib does not support" << m_fileName + << "— using FFmpeg-based duration/metadata instead"; + // Return empty metadata; duration will be read from the + // sound source's FFmpeg stream info. + return afterImport(ImportResult::Unavailable); + } + } switch (m_fileType) { case taglib::FileType::MPEG: { diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index 42a7636c6eb8..37571ea78319 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -53,6 +53,14 @@ QString SoundSource::getTypeFromFile(const QFileInfo& fileInfo) { // https://mixxx.zulipchat.com/#narrow/stream/109171-development/topic/mimetype.20sometimes.20wrong return fileSuffix; } + if (fileSuffix == QLatin1String("mkv") || fileSuffix == QLatin1String("webm")) { + // Bypass the insufficient mime type lookup from content for Matroska/WebM files. + // Qt's QMimeDatabase may not properly recognize these container formats, + // causing "no mime type registered" errors. The file suffix is used instead + // to determine the appropriate SoundSource provider. + return fileSuffix; + } + QMimeType mimeType = QMimeDatabase().mimeTypeForFile( fileInfo, QMimeDatabase::MatchContent); #ifdef __STEM__ diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index b5d2779154b5..bf19b9f9ea2d 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -1,5 +1,7 @@ #include "sources/soundsourceffmpeg.h" +#include + extern "C" { #include @@ -407,6 +409,11 @@ QStringList SoundSourceProviderFFmpeg::getSupportedFileTypes() const { } else if (!strcmp(pavInputFormat->name, "wv")) { list.append("wv"); continue; + } else if (!strcmp(pavInputFormat->name, "matroska,webm")) { + list.append("mkv"); + list.append("webm"); + continue; + /////////////////////////////////////////////////////////// // Codecs with failing tests /////////////////////////////////////////////////////////// @@ -743,11 +750,26 @@ SoundSource::OpenResult SoundSourceFFmpeg::tryOpen( } if (m_pavStream->duration == AV_NOPTS_VALUE) { - // Streams with unknown or unlimited duration are - // not (yet) supported. - kLogger.warning() - << "Unknown or unlimited stream duration"; - return OpenResult::Failed; + if (m_pavInputFormatContext->duration != AV_NOPTS_VALUE) { + // AVFormatContext::duration is measured in AV_TIME_BASE units + // (1/1e6 s), whereas AVStream::duration is measured in + // stream->time_base units. Rescale accordingly, otherwise the + // frame index range is inflated (e.g. ~1000x for webm/mkv + // streams with a 1/1000 s time base) which breaks seeking and + // the waveform/spectrogram. + m_pavStream->duration = av_rescale_q( + m_pavInputFormatContext->duration, + AV_TIME_BASE_Q, + m_pavStream->time_base); + kLogger.debug() + << "using format context duration instead of stream duration"; + } else { + // Streams with unknown or unlimited duration are + // not (yet) supported. + kLogger.warning() + << "Unknown or unlimited stream duration"; + return OpenResult::Failed; + } } const auto streamFrameIndexRange = getStreamFrameIndexRange(*m_pavStream); @@ -794,6 +816,126 @@ SoundSource::OpenResult SoundSourceFFmpeg::tryOpen( return OpenResult::Succeeded; } +// @anchor: ffmpeg:import-metadata-override +// TagLib does not support Matroska/WebM containers and its default +// implementation in MetadataSourceTagLib returns Unavailable for these +// file types. This override provides the stream info (especially the +// duration) from the FFmpeg container, so the Mixxx library does not +// show an empty duration column for such files. +std::pair +SoundSourceFFmpeg::importTrackMetadataAndCoverImage( + TrackMetadata* pTrackMetadata, + QImage* pCoverArt, + bool resetMissingTagMetadata) const { + // Delegate all file types supported by TagLib to the default + // implementation, which also imports tags like title and artist. + const QString fileSuffix = + QFileInfo(getLocalFileName()).suffix().toLower(); + if (fileSuffix != QLatin1String("mkv") && + fileSuffix != QLatin1String("webm")) { + return MetadataSourceTagLib::importTrackMetadataAndCoverImage( + pTrackMetadata, + pCoverArt, + resetMissingTagMetadata); + } + + const auto sourceSynchronizedAt = getFileSynchronizedAt( + QFile(getLocalFileName())); + if (pTrackMetadata == nullptr) { + // Cover art cannot be imported from Matroska/WebM files. + return std::make_pair(ImportResult::Unavailable, sourceSynchronizedAt); + } + + // Open the container for metadata inspection. No decoder is required. + AVFormatContext* pavInputFormatContext = + openInputFile(getLocalFileName()); + if (pavInputFormatContext == nullptr) { + return std::make_pair(ImportResult::Failed, sourceSynchronizedAt); + } + const int findStreamInfoResult = + avformat_find_stream_info(pavInputFormatContext, nullptr); + if (findStreamInfoResult != 0) { + kLogger.warning() + << "Failed to read stream info for metadata import" + << getLocalFileName(); + avformat_close_input(&pavInputFormatContext); + return std::make_pair(ImportResult::Failed, sourceSynchronizedAt); + } + + // Select the same audio stream as tryOpen() without opening a decoder. + const int streamIndex = av_find_best_stream( + pavInputFormatContext, + AVMEDIA_TYPE_AUDIO, + m_wantedStreamIndex, + /*related_stream=*/ -1, + /*decoder_ret=*/ nullptr, + /*flags=*/ 0); + if (streamIndex < 0) { + kLogger.warning() + << "Failed to find audio stream for metadata import" + << getLocalFileName(); + avformat_close_input(&pavInputFormatContext); + return std::make_pair(ImportResult::Failed, sourceSynchronizedAt); + } + AVStream* pavStream = pavInputFormatContext->streams[streamIndex]; + VERIFY_OR_DEBUG_ASSERT(pavStream != nullptr) { + avformat_close_input(&pavInputFormatContext); + return std::make_pair(ImportResult::Failed, sourceSynchronizedAt); + } + + // Mirror the duration fallback of tryOpen(): the stream duration of + // Matroska/WebM files is often unknown, in which case the format + // context duration is rescaled from AV_TIME_BASE to the stream time base. + if (pavStream->duration == AV_NOPTS_VALUE) { + if (pavInputFormatContext->duration == AV_NOPTS_VALUE) { + kLogger.warning() + << "Unknown or unlimited stream duration" + << getLocalFileName(); + avformat_close_input(&pavInputFormatContext); + return std::make_pair(ImportResult::Failed, sourceSynchronizedAt); + } + pavStream->duration = av_rescale_q( + pavInputFormatContext->duration, + AV_TIME_BASE_Q, + pavStream->time_base); + } + + const auto streamFrameIndexRange = + getStreamFrameIndexRange(*pavStream); + VERIFY_OR_DEBUG_ASSERT( + streamFrameIndexRange.start() <= streamFrameIndexRange.end()) { + kLogger.warning() + << "Stream with unsupported or invalid frame index range" + << streamFrameIndexRange; + avformat_close_input(&pavInputFormatContext); + return std::make_pair(ImportResult::Failed, sourceSynchronizedAt); + } + + // Report the same stream properties as initResampling() to avoid a + // mismatch between the imported and the decoded stream info. + const auto channelCount = audio::ChannelCount( +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1 + pavStream->codecpar->ch_layout.nb_channels +#else + pavStream->codecpar->channels +#endif + ); + const auto sampleRate = + audio::SampleRate(pavStream->codecpar->sample_rate); + if (channelCount.isValid() && sampleRate.isValid()) { + pTrackMetadata->setStreamInfo(audio::StreamInfo{ + audio::SignalInfo{channelCount, sampleRate}, + audio::Bitrate(pavStream->codecpar->bit_rate / 1000), // kbit/s + Duration::fromSeconds( + static_cast(streamFrameIndexRange.length()) / + sampleRate), + }); + } + + avformat_close_input(&pavInputFormatContext); + return std::make_pair(ImportResult::Succeeded, sourceSynchronizedAt); +} + bool SoundSourceFFmpeg::initResampling( audio::ChannelCount* pResampledChannelCount, audio::SampleRate* pResampledSampleRate) { diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index c9fab7a76d58..c1f4064afb2a 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -22,6 +22,15 @@ class SoundSourceFFmpeg : public SoundSource { ~SoundSourceFFmpeg() override; + // Overrides the TagLib-based default implementation, because TagLib + // does not support Matroska/WebM containers. For these file types the + // stream info (especially the duration) is imported from the FFmpeg + // container, otherwise the library would show an empty duration. + std::pair importTrackMetadataAndCoverImage( + TrackMetadata* pTrackMetadata, + QImage* pCoverArt, + bool resetMissingTagMetadata) const override; + void close() override; static QString formatErrorString(int errnum);