Skip to content
27 changes: 22 additions & 5 deletions src/sources/metadatasourcetaglib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <vorbisfile.h>

#include <QFile>
#include <QFileInfo>
#include <memory>

#include "track/taglib/trackmetadata.h"
Expand Down Expand Up @@ -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: {
Expand Down
8 changes: 8 additions & 0 deletions src/sources/soundsource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down
152 changes: 147 additions & 5 deletions src/sources/soundsourceffmpeg.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "sources/soundsourceffmpeg.h"

#include <QFileInfo>

extern "C" {

#include <libavutil/avutil.h>
Expand Down Expand Up @@ -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
///////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<MetadataSource::ImportResult, QDateTime>
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<double>(streamFrameIndexRange.length()) /
sampleRate),
});
}

avformat_close_input(&pavInputFormatContext);
return std::make_pair(ImportResult::Succeeded, sourceSynchronizedAt);
}

bool SoundSourceFFmpeg::initResampling(
audio::ChannelCount* pResampledChannelCount,
audio::SampleRate* pResampledSampleRate) {
Expand Down
9 changes: 9 additions & 0 deletions src/sources/soundsourceffmpeg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImportResult, QDateTime> importTrackMetadataAndCoverImage(
TrackMetadata* pTrackMetadata,
QImage* pCoverArt,
bool resetMissingTagMetadata) const override;

void close() override;

static QString formatErrorString(int errnum);
Expand Down
Loading