/src/ffmpegthumbs/ffmpegthumbnailer.cpp
Line | Count | Source |
1 | | /* |
2 | | SPDX-FileCopyrightText: 2010 Dirk Vanden Boer <dirk.vdb@gmail.com> |
3 | | SPDX-FileCopyrightText: 2020 Heiko Schäfer <heiko@rangun.de> |
4 | | |
5 | | SPDX-License-Identifier: GPL-2.0-or-later |
6 | | */ |
7 | | |
8 | | #include "ffmpegthumbnailer.h" |
9 | | #include "ffmpegthumbnailersettings5.h" |
10 | | #include "ffmpegthumbs_debug.h" |
11 | | |
12 | | #include <limits> |
13 | | |
14 | | #include <KPluginFactory> |
15 | | #include <QImage> |
16 | | |
17 | | extern "C" { |
18 | | #include <libavformat/avformat.h> |
19 | | #include <libavutil/dict.h> |
20 | | #include <libavutil/log.h> |
21 | | } |
22 | | |
23 | | namespace { |
24 | | struct FFmpegLogHandler { |
25 | 0 | static void handleMessage(void *ptr, int level, const char *fmt, va_list vargs) { |
26 | 0 | Q_UNUSED(ptr); |
27 | 0 |
|
28 | 0 | const QString message = QString::vasprintf(fmt, vargs); |
29 | 0 |
|
30 | 0 | switch(level) { |
31 | 0 | case AV_LOG_PANIC: // ffmpeg will crash now |
32 | 0 | qCCritical(ffmpegthumbs_LOG) << message; |
33 | 0 | break; |
34 | 0 | case AV_LOG_FATAL: // fatal as in can't decode, not crash |
35 | 0 | case AV_LOG_ERROR: |
36 | 0 | case AV_LOG_WARNING: |
37 | 0 | qCWarning(ffmpegthumbs_LOG) << message; |
38 | 0 | break; |
39 | 0 | case AV_LOG_INFO: |
40 | 0 | qCInfo(ffmpegthumbs_LOG) << message; |
41 | 0 | break; |
42 | 0 | case AV_LOG_VERBOSE: |
43 | 0 | case AV_LOG_DEBUG: |
44 | 0 | case AV_LOG_TRACE: |
45 | 0 | qCDebug(ffmpegthumbs_LOG) << message; |
46 | 0 | break; |
47 | 0 | default: |
48 | 0 | qCWarning(ffmpegthumbs_LOG) << "unhandled log level" << level << message; |
49 | 0 | break; |
50 | 0 | } |
51 | 0 | } |
52 | | |
53 | 0 | FFmpegLogHandler() { |
54 | 0 | av_log_set_callback(&FFmpegLogHandler::handleMessage); |
55 | 0 | } |
56 | | }; |
57 | | } //namespace |
58 | | |
59 | | FFMpegThumbnailer::FFMpegThumbnailer(QObject *parent, const QVariantList &args) |
60 | 682 | : KIO::ThumbnailCreator(parent, args) |
61 | 682 | { |
62 | 682 | FFMpegThumbnailerSettings* settings = FFMpegThumbnailerSettings::self(); |
63 | 682 | if (settings->filmstrip()) { |
64 | 682 | m_Thumbnailer.addFilter(&m_FilmStrip); |
65 | 682 | } |
66 | 682 | m_thumbCache.setMaxCost(settings->cacheSize()); |
67 | 682 | } |
68 | | |
69 | | FFMpegThumbnailer::~FFMpegThumbnailer() |
70 | 682 | { |
71 | 682 | } |
72 | | |
73 | | KIO::ThumbnailResult FFMpegThumbnailer::create(const KIO::ThumbnailRequest &request) |
74 | 682 | { |
75 | 682 | int seqIdx = static_cast<int>(request.sequenceIndex()); |
76 | 682 | if (seqIdx < 0) { |
77 | 0 | seqIdx = 0; |
78 | 0 | } |
79 | | |
80 | 682 | QList<int> seekPercentages = FFMpegThumbnailerSettings::sequenceSeekPercentages(); |
81 | 682 | if (seekPercentages.isEmpty()) { |
82 | 0 | seekPercentages.append(20); |
83 | 0 | } |
84 | | |
85 | | // We might have an embedded thumb in the video file, so we have to add 1. This gets corrected |
86 | | // later if we don't have one. |
87 | 682 | seqIdx %= static_cast<int>(seekPercentages.size()) + 1; |
88 | | |
89 | 682 | const QString path = request.url().toLocalFile(); |
90 | 682 | const QString cacheKey = QStringLiteral("%1$%2@%3").arg(path).arg(request.sequenceIndex()).arg(request.targetSize().width()); |
91 | | |
92 | 682 | QImage* cachedImg = m_thumbCache[cacheKey]; |
93 | 682 | if (cachedImg) { |
94 | 0 | return pass(*cachedImg); |
95 | 0 | } |
96 | | |
97 | | // Try reading thumbnail embedded into video file |
98 | 682 | QByteArray ba = path.toLocal8Bit(); |
99 | 682 | AVFormatContext* ct = avformat_alloc_context(); |
100 | 682 | AVPacket* pic = nullptr; |
101 | | |
102 | | // No matter the seqIdx, we have to know if the video has an embedded cover, even if we then don't return |
103 | | // it. We could cache it to avoid repeating this for higher seqIdx values, but this should be fast enough |
104 | | // to not be noticeable and caching adds unnecessary complexity. |
105 | 682 | if (ct && !avformat_open_input(&ct,ba.data(), nullptr, nullptr)) { |
106 | | |
107 | | // Using an priority system based on size or filename (matroska specification) to select the most suitable picture |
108 | 0 | int bestPrio = 0; |
109 | 0 | for (size_t i = 0; i < ct->nb_streams; ++i) { |
110 | 0 | if (ct->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) { |
111 | 0 | int prio = 0; |
112 | 0 | AVDictionaryEntry* fname = av_dict_get(ct->streams[i]->metadata, "filename", nullptr ,0); |
113 | 0 | if (fname) { |
114 | 0 | QString filename(QString::fromUtf8(fname->value)); |
115 | 0 | QString noextname = filename.section(QLatin1Char('.'), 0); |
116 | | // Prefer landscape and larger |
117 | 0 | if (noextname == QStringLiteral("cover_land")) { |
118 | 0 | prio = std::numeric_limits<int>::max(); |
119 | 0 | } |
120 | 0 | else if (noextname == QStringLiteral("small_cover_land")) { |
121 | 0 | prio = std::numeric_limits<int>::max()-1; |
122 | 0 | } |
123 | 0 | else if (noextname == QStringLiteral("cover")) { |
124 | 0 | prio = std::numeric_limits<int>::max()-2; |
125 | 0 | } |
126 | 0 | else if (noextname == QStringLiteral("small_cover")) { |
127 | 0 | prio = std::numeric_limits<int>::max()-3; |
128 | 0 | } |
129 | 0 | else { |
130 | 0 | prio = ct->streams[i]->attached_pic.size; |
131 | 0 | } |
132 | 0 | } |
133 | 0 | else { |
134 | 0 | prio = ct->streams[i]->attached_pic.size; |
135 | 0 | } |
136 | 0 | if (prio > bestPrio) { |
137 | 0 | pic = &(ct->streams[i]->attached_pic); |
138 | 0 | bestPrio = prio; |
139 | 0 | } |
140 | 0 | } |
141 | 0 | } |
142 | 0 | } |
143 | | |
144 | 682 | auto res = KIO::ThumbnailResult::fail(); |
145 | 682 | if (pic) { |
146 | 0 | QImage img; |
147 | 0 | img.loadFromData(pic->data, pic->size); |
148 | 0 | res = pass(img); |
149 | 0 | } |
150 | 682 | avformat_close_input(&ct); |
151 | | |
152 | 682 | float wraparoundPoint = 1.0f; |
153 | 682 | if (!res.image().isNull()) { |
154 | | // Video file has an embedded thumbnail -> return it for seqIdx=0 and shift the regular |
155 | | // seek percentages one to the right |
156 | |
|
157 | 0 | res.setSequenceIndexWraparoundPoint(updatedSequenceIndexWraparoundPoint(1.0f)); |
158 | |
|
159 | 0 | if (seqIdx == 0) { |
160 | 0 | return res; |
161 | 0 | } |
162 | | |
163 | 0 | seqIdx--; |
164 | 682 | } else { |
165 | 682 | wraparoundPoint = updatedSequenceIndexWraparoundPoint(0.0f); |
166 | 682 | } |
167 | | |
168 | | // The previous modulo could be wrong now if the video had an embedded thumbnail. |
169 | 682 | seqIdx %= seekPercentages.size(); |
170 | | |
171 | 682 | m_Thumbnailer.setThumbnailSize(request.targetSize().width()); |
172 | 682 | m_Thumbnailer.setSeekPercentage(seekPercentages[seqIdx]); |
173 | | //Smart frame selection is very slow compared to the fixed detection |
174 | | //TODO: Use smart detection if the image is single colored. |
175 | | //m_Thumbnailer.setSmartFrameSelection(true); |
176 | 682 | QImage img; |
177 | 682 | m_Thumbnailer.generateThumbnail(path, img); |
178 | | |
179 | 682 | if (!img.isNull()) { |
180 | | // seqIdx 0 will be served from KIO's regular thumbnail cache. |
181 | 0 | if (static_cast<int>(request.sequenceIndex()) != 0) { |
182 | 0 | const int cacheCost = static_cast<int>((img.sizeInBytes() + 1023) / 1024); |
183 | 0 | m_thumbCache.insert(cacheKey, new QImage(img), cacheCost); |
184 | 0 | } |
185 | |
|
186 | 0 | return pass(img, wraparoundPoint); |
187 | 0 | } |
188 | | |
189 | 682 | return KIO::ThumbnailResult::fail(); |
190 | 682 | } |
191 | | |
192 | | float FFMpegThumbnailer::updatedSequenceIndexWraparoundPoint(float offset) |
193 | 682 | { |
194 | 682 | float wraparoundPoint = offset; |
195 | 682 | if (!FFMpegThumbnailerSettings::sequenceSeekPercentages().isEmpty()) { |
196 | 682 | wraparoundPoint += FFMpegThumbnailerSettings::sequenceSeekPercentages().size(); |
197 | 682 | } else { |
198 | 0 | wraparoundPoint += 1.0f; |
199 | 0 | } |
200 | | |
201 | 682 | return wraparoundPoint; |
202 | 682 | } |
203 | | |
204 | | K_PLUGIN_CLASS_WITH_JSON(FFMpegThumbnailer, "ffmpegthumbs.json") Unexecuted instantiation: ffmpegthumbs_factory::tr(char const*, char const*, int) Unexecuted instantiation: ffmpegthumbs_factory::~ffmpegthumbs_factory() |
205 | | |
206 | | #include "ffmpegthumbnailer.moc" |
207 | | #include "moc_ffmpegthumbnailer.cpp" |