Commit 648104f8 authored by David Lerch's avatar David Lerch
Browse files

Add support for generating thumb sequences (ThumbSequenceCreator)

Implement ThumbSequenceCreator instead of ThumbCreator, to support generating
thumbs for multiple frames of a video file, based on sequenceIndex().

Also add some primitive runtime caching to avoid creating thumbs again when
the sequence wraps around.

The seek percentages of the sequence frames and the cache size are
configurable.
parent bfb0796a
......@@ -3,7 +3,7 @@ project(ffmpegthumbs)
cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR)
set(QT_MIN_VERSION "5.2.0")
find_package(ECM 5.72.0 REQUIRED NO_MODULE)
find_package(ECM 5.80.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(FeatureSummary)
......
......@@ -19,10 +19,16 @@
#include "ffmpegthumbnailersettings5.h"
#include "ffmpegthumbs_debug.h"
#include <limits>
#include <mp4file.h>
#include <QImage>
#include <QCheckBox>
#include <QFormLayout>
#include <QImage>
#include <QLineEdit>
#include <QRegExpValidator>
#include <QSpinBox>
#include <KLocalizedString>
extern "C" {
......@@ -76,13 +82,18 @@ extern "C"
}
}
FFMpegThumbnailer::FFMpegThumbnailer()
{
FFMpegThumbnailerSettings* settings = FFMpegThumbnailerSettings::self();
if (settings->filmstrip()) {
m_Thumbnailer.addFilter(&m_FilmStrip);
}
m_thumbCache.setMaxCost(settings->cacheSize());
// Assume that the video file has an embedded thumb, in which case it gets inserted before the
// regular seek percentage-based thumbs. If we find out that the video doesn't have one, we can
// correct that overestimation.
updateSequenceIndexWraparoundPoint(1.0f);
}
FFMpegThumbnailer::~FFMpegThumbnailer()
......@@ -91,11 +102,36 @@ FFMpegThumbnailer::~FFMpegThumbnailer()
bool FFMpegThumbnailer::create(const QString& path, int width, int /*height*/, QImage& img)
{
int seqIdx = static_cast<int>(sequenceIndex());
if (seqIdx < 0) {
seqIdx = 0;
}
QList<int> seekPercentages = FFMpegThumbnailerSettings::sequenceSeekPercentages();
if (seekPercentages.isEmpty()) {
seekPercentages.append(20);
}
// We might have an embedded thumb in the video file, so we have to add 1. This gets corrected
// later if we don't have one.
seqIdx %= static_cast<int>(seekPercentages.size()) + 1;
const QString cacheKey = QString("%1$%2@%3").arg(path).arg(seqIdx).arg(width);
QImage* cachedImg = m_thumbCache[cacheKey];
if (cachedImg) {
img = *cachedImg;
return true;
}
// Try reading thumbnail embedded into video file
QByteArray ba = path.toLocal8Bit();
TagLib::MP4::File f(ba.data(), false);
// No matter the seqIdx, we have to know if the video has an embedded cover, even if we then don't return
// it. We could cache it to avoid repeating this for higher seqIdx values, but this should be fast enough
// to not be noticeable and caching adds unnecessary complexity.
if (f.isValid()) {
TagLib::MP4::Tag* tag = f.tag();
TagLib::MP4::ItemListMap itemsListMap = tag->itemListMap();
TagLib::MP4::Item coverItem = itemsListMap["covr"];
......@@ -105,20 +141,45 @@ bool FFMpegThumbnailer::create(const QString& path, int width, int /*height*/, Q
TagLib::MP4::CoverArt coverArt = coverArtList.front();
img.loadFromData((const uchar *)coverArt.data().data(),
coverArt.data().size());
}
}
if (!img.isNull()) {
// Video file has an embedded thumbnail -> return it for seqIdx=0 and shift the regular
// seek percentages one to the right
updateSequenceIndexWraparoundPoint(1.0f);
if (!img.isNull()) return true;
if (seqIdx == 0) {
return true;
}
seqIdx--;
} else {
updateSequenceIndexWraparoundPoint(0.0f);
}
// The previous modulo could be wrong now if the video had an embedded thumbnail.
seqIdx %= seekPercentages.size();
m_Thumbnailer.setThumbnailSize(width);
// 20% seek inside the video to generate the preview
m_Thumbnailer.setSeekPercentage(20);
m_Thumbnailer.setSeekPercentage(seekPercentages[seqIdx]);
//Smart frame selection is very slow compared to the fixed detection
//TODO: Use smart detection if the image is single colored.
//m_Thumbnailer.setSmartFrameSelection(true);
m_Thumbnailer.generateThumbnail(path, img);
return !img.isNull();
if (!img.isNull()) {
// seqIdx 0 will be served from KIO's regular thumbnail cache.
if (static_cast<int>(sequenceIndex()) != 0) {
const int cacheCost = static_cast<int>((img.sizeInBytes() + 1023) / 1024);
m_thumbCache.insert(cacheKey, new QImage(img), cacheCost);
}
return true;
}
return false;
}
ThumbCreator::Flags FFMpegThumbnailer::flags() const
......@@ -128,17 +189,103 @@ ThumbCreator::Flags FFMpegThumbnailer::flags() const
QWidget *FFMpegThumbnailer::createConfigurationWidget()
{
QCheckBox *filmstripCheckBox = new QCheckBox(i18nc("@option:check", "Embed filmstrip effect"));
filmstripCheckBox->setChecked(FFMpegThumbnailerSettings::filmstrip());
return filmstripCheckBox;
QWidget* widget = new QWidget();
QFormLayout* formLayout = new QFormLayout(widget);
QCheckBox* addFilmStripCheckBox = new QCheckBox (
i18nc("@option:check For checkbox labeled 'Embed filmstrip effect'", "Embed"));
addFilmStripCheckBox->setObjectName("FFMpegThumbnailer::addFilmStripCheckBox");
addFilmStripCheckBox->setChecked(FFMpegThumbnailerSettings::filmstrip());
formLayout->addRow(i18nc("@label:checkbox", "Embed filmstrip effect"), addFilmStripCheckBox);
QString seekPercentagesStr;
for (const int sp : FFMpegThumbnailerSettings::sequenceSeekPercentages()) {
if (!seekPercentagesStr.isEmpty()) {
seekPercentagesStr.append(' ');
}
seekPercentagesStr.append(QString::number(sp));
}
QLineEdit* sequenceSeekPercentagesLineEdit = new QLineEdit();
sequenceSeekPercentagesLineEdit->setObjectName("FFMpegThumbnailer::sequenceSeekPercentagesLineEdit");
sequenceSeekPercentagesLineEdit->setText(seekPercentagesStr);
sequenceSeekPercentagesLineEdit->setToolTip(i18nc("@info:tooltip",
"List of integers, separated by space or comma"));
formLayout->addRow(i18nc("@label:textbox", "Sequence seek percentages"), sequenceSeekPercentagesLineEdit);
QSpinBox* thumbCacheSizeSpinBox = new QSpinBox();
thumbCacheSizeSpinBox->setObjectName("FFMpegThumbnailer::thumbCacheSizeSpinBox");
thumbCacheSizeSpinBox->setRange(0, std::numeric_limits<int>::max());
thumbCacheSizeSpinBox->setValue(FFMpegThumbnailerSettings::cacheSize());
thumbCacheSizeSpinBox->setSuffix(i18nc("Kibibyte used as a spinbox suffix", " KiB"));
formLayout->addRow(i18nc("@label:spinbox", "Cache size"), thumbCacheSizeSpinBox);
return widget;
}
void FFMpegThumbnailer::writeConfiguration(const QWidget *configurationWidget)
void FFMpegThumbnailer::writeConfiguration(const QWidget* configurationWidget)
{
const QCheckBox *filmstripCheckBox = qobject_cast<const QCheckBox*>(configurationWidget);
if (filmstripCheckBox) {
FFMpegThumbnailerSettings* settings = FFMpegThumbnailerSettings::self();
settings->setFilmstrip(filmstripCheckBox->isChecked());
settings->save();
if (!configurationWidget) {
qCCritical(ffmpegthumbs_LOG) << "Invalid configuration widget";
return;
}
QCheckBox* addFilmStripCheckBox = configurationWidget->findChild<QCheckBox*> (
"FFMpegThumbnailer::addFilmStripCheckBox");
QLineEdit* sequenceSeekPercentagesLineEdit = configurationWidget->findChild<QLineEdit*> (
"FFMpegThumbnailer::sequenceSeekPercentagesLineEdit");
QSpinBox* thumbCacheSizeSpinBox = configurationWidget->findChild<QSpinBox*> (
"FFMpegThumbnailer::thumbCacheSizeSpinBox");
if (!addFilmStripCheckBox || !sequenceSeekPercentagesLineEdit || !thumbCacheSizeSpinBox) {
qCCritical(ffmpegthumbs_LOG) << "Invalid configuration widget";
return;
}
FFMpegThumbnailerSettings* settings = FFMpegThumbnailerSettings::self();
settings->setFilmstrip(addFilmStripCheckBox->isChecked());
const QString seekPercentagesStr = sequenceSeekPercentagesLineEdit->text();
const QVector<QStringRef> seekPercentagesStrList = seekPercentagesStr.splitRef (
QRegularExpression("(\\s*,\\s*)|\\s+"), Qt::SkipEmptyParts);
QList<int> seekPercentages;
bool seekPercentagesValid = true;
for (const QStringRef str : seekPercentagesStrList) {
const int sp = str.toInt(&seekPercentagesValid);
if (!seekPercentagesValid) {
break;
}
seekPercentages << sp;
}
if (seekPercentagesValid) {
settings->setSequenceSeekPercentages(seekPercentages);
}
settings->setCacheSize(thumbCacheSizeSpinBox->value());
m_thumbCache.setMaxCost(thumbCacheSizeSpinBox->value());
// Assume that the video file has an embedded thumb, in which case it gets inserted before the
// regular seek percentage-based thumbs. If we find out that the video doesn't have one, we can
// correct that overestimation.
updateSequenceIndexWraparoundPoint(1.0);
settings->save();
}
void FFMpegThumbnailer::updateSequenceIndexWraparoundPoint(float offset)
{
float wraparoundPoint = offset;
if (!FFMpegThumbnailerSettings::sequenceSeekPercentages().isEmpty()) {
wraparoundPoint += FFMpegThumbnailerSettings::sequenceSeekPercentages().size();
} else {
wraparoundPoint += 1.0f;
}
setSequenceIndexWraparoundPoint(wraparoundPoint);
}
......@@ -18,14 +18,23 @@
#define KFFMPEG_THUMBNAILER_H
#include <QObject>
#include <kio/thumbcreator.h>
#include <QCache>
#include <kio/thumbsequencecreator.h>
#include <ffmpegthumbnailer/videothumbnailer.h>
#include <ffmpegthumbnailer/filmstripfilter.h>
class FFMpegThumbnailer : public QObject, public ThumbCreator
class QCheckBox;
class QLineEdit;
class QSpinBox;
class FFMpegThumbnailer : public QObject, public ThumbSequenceCreator
{
Q_OBJECT
private:
typedef QCache<QString, QImage> ThumbCache;
public:
FFMpegThumbnailer();
virtual ~FFMpegThumbnailer();
......@@ -33,9 +42,14 @@ public:
virtual Flags flags() const override;
virtual QWidget* createConfigurationWidget() override;
virtual void writeConfiguration(const QWidget* configurationWidget) override;
private:
void updateSequenceIndexWraparoundPoint(float offset);
private:
ffmpegthumbnailer::VideoThumbnailer m_Thumbnailer;
ffmpegthumbnailer::FilmStripFilter m_FilmStrip;
ThumbCache m_thumbCache;
};
#endif
......@@ -9,5 +9,13 @@
<label>Embed filmstrip effect</label>
<default>true</default>
</entry>
<entry name="sequenceSeekPercentages" type="IntList">
<label>Sequence seek percentages</label>
<default>20,35,50,65,80</default>
</entry>
<entry name="cacheSize" type="UInt">
<label>Cache size (KiB)</label>
<default>51200</default>
</entry>
</group>
</kcfg>
......@@ -58,6 +58,7 @@ MimeType=video/*;application/x-flash-video;application/vnd.ms-asf;application/vn
X-KDE-Library=ffmpegthumbs
ServiceTypes=ThumbCreator
CacheThumbnail=true
HandleSequences=true
ThumbnailerVersion=2
IgnoreMaximumSize=true
Configurable=true
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment