File indexing completed on 2024-04-21 04:49:04
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 <KPluginFactory> 0015 #include <QCheckBox> 0016 #include <QFormLayout> 0017 #include <QImage> 0018 #include <QLineEdit> 0019 #include <QSpinBox> 0020 0021 extern "C" { 0022 #include <libavformat/avformat.h> 0023 #include <libavutil/dict.h> 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 FFMpegThumbnailer::FFMpegThumbnailer(QObject *parent, const QVariantList &args) 0064 : KIO::ThumbnailCreator(parent, args) 0065 { 0066 FFMpegThumbnailerSettings* settings = FFMpegThumbnailerSettings::self(); 0067 if (settings->filmstrip()) { 0068 m_Thumbnailer.addFilter(&m_FilmStrip); 0069 } 0070 m_thumbCache.setMaxCost(settings->cacheSize()); 0071 } 0072 0073 FFMpegThumbnailer::~FFMpegThumbnailer() 0074 { 0075 } 0076 0077 KIO::ThumbnailResult FFMpegThumbnailer::create(const KIO::ThumbnailRequest &request) 0078 { 0079 int seqIdx = static_cast<int>(request.sequenceIndex()); 0080 if (seqIdx < 0) { 0081 seqIdx = 0; 0082 } 0083 0084 QList<int> seekPercentages = FFMpegThumbnailerSettings::sequenceSeekPercentages(); 0085 if (seekPercentages.isEmpty()) { 0086 seekPercentages.append(20); 0087 } 0088 0089 // We might have an embedded thumb in the video file, so we have to add 1. This gets corrected 0090 // later if we don't have one. 0091 seqIdx %= static_cast<int>(seekPercentages.size()) + 1; 0092 0093 const QString path = request.url().toLocalFile(); 0094 const QString cacheKey = QStringLiteral("%1$%2@%3").arg(path).arg(request.sequenceIndex()).arg(request.targetSize().width()); 0095 0096 QImage* cachedImg = m_thumbCache[cacheKey]; 0097 if (cachedImg) { 0098 return pass(*cachedImg); 0099 } 0100 0101 // Try reading thumbnail embedded into video file 0102 QByteArray ba = path.toLocal8Bit(); 0103 AVFormatContext* ct = avformat_alloc_context(); 0104 AVPacket* pic = nullptr; 0105 0106 // No matter the seqIdx, we have to know if the video has an embedded cover, even if we then don't return 0107 // it. We could cache it to avoid repeating this for higher seqIdx values, but this should be fast enough 0108 // to not be noticeable and caching adds unnecessary complexity. 0109 if (ct && !avformat_open_input(&ct,ba.data(), nullptr, nullptr)) { 0110 0111 // Using an priority system based on size or filename (matroska specification) to select the most suitable picture 0112 int bestPrio = 0; 0113 for (size_t i = 0; i < ct->nb_streams; ++i) { 0114 if (ct->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) { 0115 int prio = 0; 0116 AVDictionaryEntry* fname = av_dict_get(ct->streams[i]->metadata, "filename", nullptr ,0); 0117 if (fname) { 0118 QString filename(QString::fromUtf8(fname->value)); 0119 QString noextname = filename.section(QLatin1Char('.'), 0); 0120 // Prefer landscape and larger 0121 if (noextname == QStringLiteral("cover_land")) { 0122 prio = std::numeric_limits<int>::max(); 0123 } 0124 else if (noextname == QStringLiteral("small_cover_land")) { 0125 prio = std::numeric_limits<int>::max()-1; 0126 } 0127 else if (noextname == QStringLiteral("cover")) { 0128 prio = std::numeric_limits<int>::max()-2; 0129 } 0130 else if (noextname == QStringLiteral("small_cover")) { 0131 prio = std::numeric_limits<int>::max()-3; 0132 } 0133 else { 0134 prio = ct->streams[i]->attached_pic.size; 0135 } 0136 } 0137 else { 0138 prio = ct->streams[i]->attached_pic.size; 0139 } 0140 if (prio > bestPrio) { 0141 pic = &(ct->streams[i]->attached_pic); 0142 bestPrio = prio; 0143 } 0144 } 0145 } 0146 } 0147 0148 auto res = KIO::ThumbnailResult::fail(); 0149 if (pic) { 0150 QImage img; 0151 img.loadFromData(pic->data, pic->size); 0152 res = pass(img); 0153 } 0154 avformat_close_input(&ct); 0155 0156 float wraparoundPoint = 1.0f; 0157 if (!res.image().isNull()) { 0158 // Video file has an embedded thumbnail -> return it for seqIdx=0 and shift the regular 0159 // seek percentages one to the right 0160 0161 res.setSequenceIndexWraparoundPoint(updatedSequenceIndexWraparoundPoint(1.0f)); 0162 0163 if (seqIdx == 0) { 0164 return res; 0165 } 0166 0167 seqIdx--; 0168 } else { 0169 wraparoundPoint = updatedSequenceIndexWraparoundPoint(0.0f); 0170 } 0171 0172 // The previous modulo could be wrong now if the video had an embedded thumbnail. 0173 seqIdx %= seekPercentages.size(); 0174 0175 m_Thumbnailer.setThumbnailSize(request.targetSize().width()); 0176 m_Thumbnailer.setSeekPercentage(seekPercentages[seqIdx]); 0177 //Smart frame selection is very slow compared to the fixed detection 0178 //TODO: Use smart detection if the image is single colored. 0179 //m_Thumbnailer.setSmartFrameSelection(true); 0180 QImage img; 0181 m_Thumbnailer.generateThumbnail(path, img); 0182 0183 if (!img.isNull()) { 0184 // seqIdx 0 will be served from KIO's regular thumbnail cache. 0185 if (static_cast<int>(request.sequenceIndex()) != 0) { 0186 const int cacheCost = static_cast<int>((img.sizeInBytes() + 1023) / 1024); 0187 m_thumbCache.insert(cacheKey, new QImage(img), cacheCost); 0188 } 0189 0190 return pass(img, wraparoundPoint); 0191 } 0192 0193 return KIO::ThumbnailResult::fail(); 0194 } 0195 0196 float FFMpegThumbnailer::updatedSequenceIndexWraparoundPoint(float offset) 0197 { 0198 float wraparoundPoint = offset; 0199 if (!FFMpegThumbnailerSettings::sequenceSeekPercentages().isEmpty()) { 0200 wraparoundPoint += FFMpegThumbnailerSettings::sequenceSeekPercentages().size(); 0201 } else { 0202 wraparoundPoint += 1.0f; 0203 } 0204 0205 return wraparoundPoint; 0206 } 0207 0208 K_PLUGIN_CLASS_WITH_JSON(FFMpegThumbnailer, "ffmpegthumbs.json") 0209 0210 #include "ffmpegthumbnailer.moc" 0211 #include "moc_ffmpegthumbnailer.cpp"