Warning, file /multimedia/ffmpegthumbs/ffmpegthumbnailer.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2010 Dirk Vanden Boer <dirk.vdb@gmail.com>
0003     SPDX-FileCopyrightText: 2020 Heiko Schäfer <heiko@rangun.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "ffmpegthumbnailer.h"
0009 #include "ffmpegthumbnailersettings5.h"
0010 #include "ffmpegthumbs_debug.h"
0011 
0012 #include <limits>
0013 
0014 #include <mp4file.h>
0015 
0016 #include <QCheckBox>
0017 #include <QFormLayout>
0018 #include <QImage>
0019 #include <QLineEdit>
0020 #include <QSpinBox>
0021 #include <KLocalizedString>
0022 
0023 extern "C" {
0024 #include <libavutil/log.h>
0025 }
0026 
0027 namespace {
0028 struct FFmpegLogHandler {
0029     static void handleMessage(void *ptr, int level, const char *fmt, va_list vargs) {
0030         Q_UNUSED(ptr);
0031 
0032         const QString message = QString::vasprintf(fmt, vargs);
0033 
0034         switch(level) {
0035         case AV_LOG_PANIC: // ffmpeg will crash now
0036             qCCritical(ffmpegthumbs_LOG) << message;
0037             break;
0038         case AV_LOG_FATAL: // fatal as in can't decode, not crash
0039         case AV_LOG_ERROR:
0040         case AV_LOG_WARNING:
0041             qCWarning(ffmpegthumbs_LOG) << message;
0042             break;
0043         case AV_LOG_INFO:
0044             qCInfo(ffmpegthumbs_LOG) << message;
0045             break;
0046         case AV_LOG_VERBOSE:
0047         case AV_LOG_DEBUG:
0048         case AV_LOG_TRACE:
0049             qCDebug(ffmpegthumbs_LOG) << message;
0050             break;
0051         default:
0052             qCWarning(ffmpegthumbs_LOG) << "unhandled log level" << level << message;
0053             break;
0054         }
0055     }
0056 
0057     FFmpegLogHandler() {
0058         av_log_set_callback(&FFmpegLogHandler::handleMessage);
0059     }
0060 };
0061 } //namespace
0062 
0063 extern "C"
0064 {
0065     Q_DECL_EXPORT ThumbCreator *new_creator()
0066     {
0067         // This is a threadsafe way to ensure that we only register it once
0068         static FFmpegLogHandler handler;
0069 
0070         return new FFMpegThumbnailer();
0071     }
0072 }
0073 
0074 FFMpegThumbnailer::FFMpegThumbnailer()
0075 {
0076     FFMpegThumbnailerSettings* settings = FFMpegThumbnailerSettings::self();
0077     if (settings->filmstrip()) {
0078         m_Thumbnailer.addFilter(&m_FilmStrip);
0079     }
0080     m_thumbCache.setMaxCost(settings->cacheSize());
0081 
0082     // Assume that the video file has an embedded thumb, in which case it gets inserted before the
0083     // regular seek percentage-based thumbs. If we find out that the video doesn't have one, we can
0084     // correct that overestimation.
0085     updateSequenceIndexWraparoundPoint(1.0f);
0086 }
0087 
0088 FFMpegThumbnailer::~FFMpegThumbnailer()
0089 {
0090 }
0091 
0092 bool FFMpegThumbnailer::create(const QString& path, int width, int /*height*/, QImage& img)
0093 {
0094     int seqIdx = static_cast<int>(sequenceIndex());
0095     if (seqIdx < 0) {
0096         seqIdx = 0;
0097     }
0098 
0099     QList<int> seekPercentages = FFMpegThumbnailerSettings::sequenceSeekPercentages();
0100     if (seekPercentages.isEmpty()) {
0101         seekPercentages.append(20);
0102     }
0103 
0104     // We might have an embedded thumb in the video file, so we have to add 1. This gets corrected
0105     // later if we don't have one.
0106     seqIdx %= static_cast<int>(seekPercentages.size()) + 1;
0107 
0108     const QString cacheKey = QString("%1$%2@%3").arg(path).arg(seqIdx).arg(width);
0109 
0110     QImage* cachedImg = m_thumbCache[cacheKey];
0111     if (cachedImg) {
0112         img = *cachedImg;
0113         return true;
0114     }
0115 
0116     // Try reading thumbnail embedded into video file
0117     QByteArray ba = path.toLocal8Bit();
0118     TagLib::MP4::File f(ba.data(), false);
0119 
0120     // No matter the seqIdx, we have to know if the video has an embedded cover, even if we then don't return
0121     // it. We could cache it to avoid repeating this for higher seqIdx values, but this should be fast enough
0122     // to not be noticeable and caching adds unnecessary complexity.
0123     if (f.isValid()) {
0124         TagLib::MP4::Tag* tag = f.tag();
0125         TagLib::MP4::ItemListMap itemsListMap = tag->itemListMap();
0126         TagLib::MP4::Item coverItem = itemsListMap["covr"];
0127         TagLib::MP4::CoverArtList coverArtList = coverItem.toCoverArtList();
0128 
0129         if (!coverArtList.isEmpty()) {
0130             TagLib::MP4::CoverArt coverArt = coverArtList.front();
0131             img.loadFromData((const uchar *)coverArt.data().data(),
0132                          coverArt.data().size());
0133         }
0134     }
0135 
0136     if (!img.isNull()) {
0137         // Video file has an embedded thumbnail -> return it for seqIdx=0 and shift the regular
0138         // seek percentages one to the right
0139 
0140         updateSequenceIndexWraparoundPoint(1.0f);
0141 
0142         if (seqIdx == 0) {
0143             return true;
0144         }
0145 
0146         seqIdx--;
0147     } else {
0148         updateSequenceIndexWraparoundPoint(0.0f);
0149     }
0150 
0151     // The previous modulo could be wrong now if the video had an embedded thumbnail.
0152     seqIdx %= seekPercentages.size();
0153 
0154     m_Thumbnailer.setThumbnailSize(width);
0155     m_Thumbnailer.setSeekPercentage(seekPercentages[seqIdx]);
0156     //Smart frame selection is very slow compared to the fixed detection
0157     //TODO: Use smart detection if the image is single colored.
0158     //m_Thumbnailer.setSmartFrameSelection(true);
0159     m_Thumbnailer.generateThumbnail(path, img);
0160 
0161     if (!img.isNull()) {
0162         // seqIdx 0 will be served from KIO's regular thumbnail cache.
0163         if (static_cast<int>(sequenceIndex()) != 0) {
0164             const int cacheCost = static_cast<int>((img.sizeInBytes() + 1023) / 1024);
0165             m_thumbCache.insert(cacheKey, new QImage(img), cacheCost);
0166         }
0167 
0168         return true;
0169     }
0170 
0171     return false;
0172 }
0173 
0174 ThumbCreator::Flags FFMpegThumbnailer::flags() const
0175 {
0176     return (Flags)(None);
0177 }
0178 
0179 void FFMpegThumbnailer::updateSequenceIndexWraparoundPoint(float offset)
0180 {
0181     float wraparoundPoint = offset;
0182 
0183     if (!FFMpegThumbnailerSettings::sequenceSeekPercentages().isEmpty()) {
0184         wraparoundPoint += FFMpegThumbnailerSettings::sequenceSeekPercentages().size();
0185     } else {
0186         wraparoundPoint += 1.0f;
0187     }
0188 
0189     setSequenceIndexWraparoundPoint(wraparoundPoint);
0190 }