|
1 | 1 | #include "sources/soundsourceffmpeg.h" |
2 | 2 |
|
| 3 | +#include <QFileInfo> |
| 4 | + |
3 | 5 | extern "C" { |
4 | 6 |
|
5 | 7 | #include <libavutil/avutil.h> |
@@ -814,6 +816,126 @@ SoundSource::OpenResult SoundSourceFFmpeg::tryOpen( |
814 | 816 | return OpenResult::Succeeded; |
815 | 817 | } |
816 | 818 |
|
| 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 | + |
817 | 939 | bool SoundSourceFFmpeg::initResampling( |
818 | 940 | audio::ChannelCount* pResampledChannelCount, |
819 | 941 | audio::SampleRate* pResampledSampleRate) { |
|
0 commit comments