Skip to content

Commit 2392dd5

Browse files
committed
feat: import Matroska/WebM stream info from FFmpeg container
TagLib does not support Matroska/WebM containers, so MetadataSourceTagLib returns ImportResult::Unavailable for these file types. As a result the library showed an empty duration column for such files, because the FFmpeg-based SoundSource never fed the imported stream info back into the track metadata. Add an importTrackMetadataAndCoverImage override that delegates all TagLib-supported file types to the default implementation, and for mkv/webm opens the FFmpeg container and reports the stream info (channel count, sample rate, bitrate and duration) matching the native values used by initResampling(). The duration is derived from the stream frame index range, mirroring the format-context duration fallback of tryOpen() when the stream duration is unknown.
1 parent 9d22784 commit 2392dd5

2 files changed

Lines changed: 131 additions & 0 deletions

File tree

src/sources/soundsourceffmpeg.cpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "sources/soundsourceffmpeg.h"
22

3+
#include <QFileInfo>
4+
35
extern "C" {
46

57
#include <libavutil/avutil.h>
@@ -814,6 +816,126 @@ SoundSource::OpenResult SoundSourceFFmpeg::tryOpen(
814816
return OpenResult::Succeeded;
815817
}
816818

819+
// @anchor: ffmpeg:import-metadata-override
820+
// TagLib does not support Matroska/WebM containers and its default
821+
// implementation in MetadataSourceTagLib returns Unavailable for these
822+
// file types. This override provides the stream info (especially the
823+
// duration) from the FFmpeg container, so the Mixxx library does not
824+
// show an empty duration column for such files.
825+
std::pair<MetadataSource::ImportResult, QDateTime>
826+
SoundSourceFFmpeg::importTrackMetadataAndCoverImage(
827+
TrackMetadata* pTrackMetadata,
828+
QImage* pCoverArt,
829+
bool resetMissingTagMetadata) const {
830+
// Delegate all file types supported by TagLib to the default
831+
// implementation, which also imports tags like title and artist.
832+
const QString fileSuffix =
833+
QFileInfo(getLocalFileName()).suffix().toLower();
834+
if (fileSuffix != QLatin1String("mkv") &&
835+
fileSuffix != QLatin1String("webm")) {
836+
return MetadataSourceTagLib::importTrackMetadataAndCoverImage(
837+
pTrackMetadata,
838+
pCoverArt,
839+
resetMissingTagMetadata);
840+
}
841+
842+
const auto sourceSynchronizedAt = getFileSynchronizedAt(
843+
QFile(getLocalFileName()));
844+
if (pTrackMetadata == nullptr) {
845+
// Cover art cannot be imported from Matroska/WebM files.
846+
return std::make_pair(ImportResult::Unavailable, sourceSynchronizedAt);
847+
}
848+
849+
// Open the container for metadata inspection. No decoder is required.
850+
AVFormatContext* pavInputFormatContext =
851+
openInputFile(getLocalFileName());
852+
if (pavInputFormatContext == nullptr) {
853+
return std::make_pair(ImportResult::Failed, sourceSynchronizedAt);
854+
}
855+
const int findStreamInfoResult =
856+
avformat_find_stream_info(pavInputFormatContext, nullptr);
857+
if (findStreamInfoResult != 0) {
858+
kLogger.warning()
859+
<< "Failed to read stream info for metadata import"
860+
<< getLocalFileName();
861+
avformat_close_input(&pavInputFormatContext);
862+
return std::make_pair(ImportResult::Failed, sourceSynchronizedAt);
863+
}
864+
865+
// Select the same audio stream as tryOpen() without opening a decoder.
866+
const int streamIndex = av_find_best_stream(
867+
pavInputFormatContext,
868+
AVMEDIA_TYPE_AUDIO,
869+
m_wantedStreamIndex,
870+
/*related_stream=*/ -1,
871+
/*decoder_ret=*/ nullptr,
872+
/*flags=*/ 0);
873+
if (streamIndex < 0) {
874+
kLogger.warning()
875+
<< "Failed to find audio stream for metadata import"
876+
<< getLocalFileName();
877+
avformat_close_input(&pavInputFormatContext);
878+
return std::make_pair(ImportResult::Failed, sourceSynchronizedAt);
879+
}
880+
AVStream* pavStream = pavInputFormatContext->streams[streamIndex];
881+
VERIFY_OR_DEBUG_ASSERT(pavStream != nullptr) {
882+
avformat_close_input(&pavInputFormatContext);
883+
return std::make_pair(ImportResult::Failed, sourceSynchronizedAt);
884+
}
885+
886+
// Mirror the duration fallback of tryOpen(): the stream duration of
887+
// Matroska/WebM files is often unknown, in which case the format
888+
// context duration is rescaled from AV_TIME_BASE to the stream time base.
889+
if (pavStream->duration == AV_NOPTS_VALUE) {
890+
if (pavInputFormatContext->duration == AV_NOPTS_VALUE) {
891+
kLogger.warning()
892+
<< "Unknown or unlimited stream duration"
893+
<< getLocalFileName();
894+
avformat_close_input(&pavInputFormatContext);
895+
return std::make_pair(ImportResult::Failed, sourceSynchronizedAt);
896+
}
897+
pavStream->duration = av_rescale_q(
898+
pavInputFormatContext->duration,
899+
AV_TIME_BASE_Q,
900+
pavStream->time_base);
901+
}
902+
903+
const auto streamFrameIndexRange =
904+
getStreamFrameIndexRange(*pavStream);
905+
VERIFY_OR_DEBUG_ASSERT(
906+
streamFrameIndexRange.start() <= streamFrameIndexRange.end()) {
907+
kLogger.warning()
908+
<< "Stream with unsupported or invalid frame index range"
909+
<< streamFrameIndexRange;
910+
avformat_close_input(&pavInputFormatContext);
911+
return std::make_pair(ImportResult::Failed, sourceSynchronizedAt);
912+
}
913+
914+
// Report the same stream properties as initResampling() to avoid a
915+
// mismatch between the imported and the decoded stream info.
916+
const auto channelCount = audio::ChannelCount(
917+
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1
918+
pavStream->codecpar->ch_layout.nb_channels
919+
#else
920+
pavStream->codecpar->channels
921+
#endif
922+
);
923+
const auto sampleRate =
924+
audio::SampleRate(pavStream->codecpar->sample_rate);
925+
if (channelCount.isValid() && sampleRate.isValid()) {
926+
pTrackMetadata->setStreamInfo(audio::StreamInfo{
927+
audio::SignalInfo{channelCount, sampleRate},
928+
audio::Bitrate(pavStream->codecpar->bit_rate / 1000), // kbit/s
929+
Duration::fromSeconds(
930+
static_cast<double>(streamFrameIndexRange.length()) /
931+
sampleRate),
932+
});
933+
}
934+
935+
avformat_close_input(&pavInputFormatContext);
936+
return std::make_pair(ImportResult::Succeeded, sourceSynchronizedAt);
937+
}
938+
817939
bool SoundSourceFFmpeg::initResampling(
818940
audio::ChannelCount* pResampledChannelCount,
819941
audio::SampleRate* pResampledSampleRate) {

src/sources/soundsourceffmpeg.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ class SoundSourceFFmpeg : public SoundSource {
2222

2323
~SoundSourceFFmpeg() override;
2424

25+
// Overrides the TagLib-based default implementation, because TagLib
26+
// does not support Matroska/WebM containers. For these file types the
27+
// stream info (especially the duration) is imported from the FFmpeg
28+
// container, otherwise the library would show an empty duration.
29+
std::pair<ImportResult, QDateTime> importTrackMetadataAndCoverImage(
30+
TrackMetadata* pTrackMetadata,
31+
QImage* pCoverArt,
32+
bool resetMissingTagMetadata) const override;
33+
2534
void close() override;
2635

2736
static QString formatErrorString(int errnum);

0 commit comments

Comments
 (0)