mirror of
https://github.com/subsurface/subsurface.git
synced 2025-01-19 14:25:27 +00:00
Dive media: Extract thumbnails from videos with ffmpeg
Extract thumbnails using ffmpeg. Behavior is controlled by three new preferences fields: - extract_video_thumbnails (bool): if true, thumbnails are calculated. - extract_video_thumbnail_position (int 0..100): position in video where thumbnail is fetched. - ffmpeg_executable (string): path of ffmpeg executable. If ffmpeg refuses to start, extract_video_thumbnails is set to false to avoid unnecessary churn. Video thumbnails are marked by an overlay. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
parent
51066e5478
commit
fce42d4858
14 changed files with 727 additions and 20 deletions
|
@ -97,6 +97,7 @@ set(SUBSURFACE_CORE_LIB_SRCS
|
||||||
uemis.c
|
uemis.c
|
||||||
uemis-downloader.c
|
uemis-downloader.c
|
||||||
version.c
|
version.c
|
||||||
|
videoframeextractor.cpp
|
||||||
windowtitleupdate.cpp
|
windowtitleupdate.cpp
|
||||||
worldmap-save.c
|
worldmap-save.c
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "divelist.h"
|
#include "divelist.h"
|
||||||
#include "qthelper.h"
|
#include "qthelper.h"
|
||||||
#include "imagedownloader.h"
|
#include "imagedownloader.h"
|
||||||
|
#include "videoframeextractor.h"
|
||||||
#include "qt-models/divepicturemodel.h"
|
#include "qt-models/divepicturemodel.h"
|
||||||
#include "metadata.h"
|
#include "metadata.h"
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
@ -96,7 +97,7 @@ Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QS
|
||||||
if (type == MEDIATYPE_IO_ERROR)
|
if (type == MEDIATYPE_IO_ERROR)
|
||||||
return { failImage, MEDIATYPE_IO_ERROR, 0 };
|
return { failImage, MEDIATYPE_IO_ERROR, 0 };
|
||||||
else if (type == MEDIATYPE_VIDEO)
|
else if (type == MEDIATYPE_VIDEO)
|
||||||
return addVideoThumbnailToCache(originalFilename, md.duration);
|
return fetchVideoThumbnail(filename, originalFilename, md.duration);
|
||||||
|
|
||||||
// Try if Qt can parse this image. If it does, use this as a thumbnail.
|
// Try if Qt can parse this image. If it does, use this as a thumbnail.
|
||||||
QImage thumb(filename);
|
QImage thumb(filename);
|
||||||
|
@ -110,7 +111,7 @@ Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QS
|
||||||
// Try to check for a video-file extension. Since we couldn't parse the video file,
|
// Try to check for a video-file extension. Since we couldn't parse the video file,
|
||||||
// we pass 0 as the duration.
|
// we pass 0 as the duration.
|
||||||
if (hasVideoFileExtension(filename))
|
if (hasVideoFileExtension(filename))
|
||||||
return addVideoThumbnailToCache(originalFilename, {0} );
|
return fetchVideoThumbnail(filename, originalFilename, {0} );
|
||||||
|
|
||||||
// Give up: we simply couldn't determine what this thing is.
|
// Give up: we simply couldn't determine what this thing is.
|
||||||
// But since we managed to read this file, mark this file in the cache as unknown.
|
// But since we managed to read this file, mark this file in the cache as unknown.
|
||||||
|
@ -163,9 +164,22 @@ static QImage renderIcon(const char *id, int size)
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As renderIcon, but render to a fixed width and scale height accordingly
|
||||||
|
// and have a transparent background.
|
||||||
|
static QImage renderIconWidth(const char *id, int size)
|
||||||
|
{
|
||||||
|
QSvgRenderer svg{QString(id)};
|
||||||
|
QSize svgSize = svg.defaultSize();
|
||||||
|
QImage res(size, size * svgSize.height() / svgSize.width(), QImage::Format_ARGB32);
|
||||||
|
QPainter painter(&res);
|
||||||
|
svg.render(&painter);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailSize())), // TODO: Don't misuse filter close icon
|
Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailSize())), // TODO: Don't misuse filter close icon
|
||||||
dummyImage(renderIcon(":camera-icon", maxThumbnailSize())),
|
dummyImage(renderIcon(":camera-icon", maxThumbnailSize())),
|
||||||
videoImage(renderIcon(":video-icon", maxThumbnailSize())),
|
videoImage(renderIcon(":video-icon", maxThumbnailSize())),
|
||||||
|
videoOverlayImage(renderIconWidth(":video-overlay", maxThumbnailSize())),
|
||||||
unknownImage(renderIcon(":unknown-icon", maxThumbnailSize()))
|
unknownImage(renderIcon(":unknown-icon", maxThumbnailSize()))
|
||||||
{
|
{
|
||||||
// Currently, we only process one image at a time. Stefan Fuchs reported problems when
|
// Currently, we only process one image at a time. Stefan Fuchs reported problems when
|
||||||
|
@ -173,6 +187,9 @@ Thumbnailer::Thumbnailer() : failImage(renderIcon(":filter-close", maxThumbnailS
|
||||||
pool.setMaxThreadCount(1);
|
pool.setMaxThreadCount(1);
|
||||||
connect(ImageDownloader::instance(), &ImageDownloader::loaded, this, &Thumbnailer::imageDownloaded);
|
connect(ImageDownloader::instance(), &ImageDownloader::loaded, this, &Thumbnailer::imageDownloaded);
|
||||||
connect(ImageDownloader::instance(), &ImageDownloader::failed, this, &Thumbnailer::imageDownloadFailed);
|
connect(ImageDownloader::instance(), &ImageDownloader::failed, this, &Thumbnailer::imageDownloadFailed);
|
||||||
|
connect(VideoFrameExtractor::instance(), &VideoFrameExtractor::extracted, this, &Thumbnailer::frameExtracted);
|
||||||
|
connect(VideoFrameExtractor::instance(), &VideoFrameExtractor::failed, this, &Thumbnailer::frameExtractionFailed);
|
||||||
|
connect(VideoFrameExtractor::instance(), &VideoFrameExtractor::failed, this, &Thumbnailer::frameExtractionInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
Thumbnailer *Thumbnailer::instance()
|
Thumbnailer *Thumbnailer::instance()
|
||||||
|
@ -188,7 +205,17 @@ Thumbnailer::Thumbnail Thumbnailer::getPictureThumbnailFromStream(QDataStream &s
|
||||||
return { res, MEDIATYPE_PICTURE, 0 };
|
return { res, MEDIATYPE_PICTURE, 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &stream)
|
void Thumbnailer::markVideoThumbnail(QImage &img)
|
||||||
|
{
|
||||||
|
QSize size = img.size();
|
||||||
|
QImage marker = videoOverlayImage.scaledToWidth(size.width());
|
||||||
|
marker = marker.copy(0, (marker.size().height() - size.height()) / 2, size.width(), size.height());
|
||||||
|
QPainter painter(&img);
|
||||||
|
painter.drawImage(0, 0, marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(duration_t)
|
||||||
|
Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &stream, const QString &filename)
|
||||||
{
|
{
|
||||||
quint32 duration, numPics;
|
quint32 duration, numPics;
|
||||||
stream >> duration >> numPics;
|
stream >> duration >> numPics;
|
||||||
|
@ -200,16 +227,27 @@ Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &str
|
||||||
if (stream.status() != QDataStream::Ok || duration > 36000 || numPics > 10000)
|
if (stream.status() != QDataStream::Ok || duration > 36000 || numPics > 10000)
|
||||||
return { QImage(), MEDIATYPE_VIDEO, 0 };
|
return { QImage(), MEDIATYPE_VIDEO, 0 };
|
||||||
|
|
||||||
|
// If the file didn't contain an image, but user turned on thumbnail extraction, schedule thumbnail
|
||||||
|
// for extraction. TODO: save failure to extract thumbnails to disk so that thumbnailing
|
||||||
|
// is not repeated ad-nauseum for broken images.
|
||||||
|
if (numPics == 0 && prefs.extract_video_thumbnails) {
|
||||||
|
QMetaObject::invokeMethod(VideoFrameExtractor::instance(), "extract", Qt::AutoConnection,
|
||||||
|
Q_ARG(QString, filename), Q_ARG(QString, filename), Q_ARG(duration_t, duration_t{(int32_t)duration}));
|
||||||
|
}
|
||||||
|
|
||||||
// Currently, we support only one picture
|
// Currently, we support only one picture
|
||||||
QImage res;
|
QImage res;
|
||||||
if (numPics > 0) {
|
if (numPics > 0) {
|
||||||
quint32 offset;
|
quint32 offset;
|
||||||
QImage res;
|
|
||||||
stream >> offset >> res;
|
stream >> offset >> res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No picture -> show dummy-icon
|
if (res.isNull())
|
||||||
return { res.isNull() ? videoImage : res, MEDIATYPE_VIDEO, (int32_t)duration };
|
res = videoImage; // No picture -> show dummy-icon
|
||||||
|
else
|
||||||
|
markVideoThumbnail(res); // We got an image -> place our video marker on top of it
|
||||||
|
|
||||||
|
return { res, MEDIATYPE_VIDEO, (int32_t)duration };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch a thumbnail from cache.
|
// Fetch a thumbnail from cache.
|
||||||
|
@ -248,13 +286,14 @@ Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MEDIATYPE_PICTURE: return getPictureThumbnailFromStream(stream);
|
case MEDIATYPE_PICTURE: return getPictureThumbnailFromStream(stream);
|
||||||
case MEDIATYPE_VIDEO: return getVideoThumbnailFromStream(stream);
|
case MEDIATYPE_VIDEO: return getVideoThumbnailFromStream(stream, picture_filename);
|
||||||
case MEDIATYPE_UNKNOWN: return { unknownImage, MEDIATYPE_UNKNOWN, 0 };
|
case MEDIATYPE_UNKNOWN: return { unknownImage, MEDIATYPE_UNKNOWN, 0 };
|
||||||
default: return { QImage(), MEDIATYPE_UNKNOWN, 0 };
|
default: return { QImage(), MEDIATYPE_UNKNOWN, 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, duration_t duration)
|
Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &picture_filename, duration_t duration,
|
||||||
|
const QImage &image, duration_t position)
|
||||||
{
|
{
|
||||||
// The format of video thumbnails:
|
// The format of video thumbnails:
|
||||||
// uint32 MEDIATYPE_VIDEO
|
// uint32 MEDIATYPE_VIDEO
|
||||||
|
@ -270,12 +309,36 @@ Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &pict
|
||||||
|
|
||||||
stream << (quint32)MEDIATYPE_VIDEO;
|
stream << (quint32)MEDIATYPE_VIDEO;
|
||||||
stream << (quint32)duration.seconds;
|
stream << (quint32)duration.seconds;
|
||||||
stream << (quint32)0; // Currently, we don't support extraction of images
|
|
||||||
|
if (image.isNull()) {
|
||||||
|
// No image provided
|
||||||
|
stream << (quint32)0;
|
||||||
|
} else {
|
||||||
|
// Currently, we support at most one image
|
||||||
|
stream << (quint32)1;
|
||||||
|
stream << (quint32)position.seconds;
|
||||||
|
stream << image;
|
||||||
|
}
|
||||||
|
|
||||||
file.commit();
|
file.commit();
|
||||||
}
|
}
|
||||||
return { videoImage, MEDIATYPE_VIDEO, duration };
|
return { videoImage, MEDIATYPE_VIDEO, duration };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Thumbnailer::Thumbnail Thumbnailer::fetchVideoThumbnail(const QString &filename, const QString &originalFilename, duration_t duration)
|
||||||
|
{
|
||||||
|
if (prefs.extract_video_thumbnails) {
|
||||||
|
// Video-thumbnailing is enabled. Fetch thumbnail in background thread and in the meanwhile
|
||||||
|
// return a dummy image.
|
||||||
|
QMetaObject::invokeMethod(VideoFrameExtractor::instance(), "extract", Qt::AutoConnection,
|
||||||
|
Q_ARG(QString, originalFilename), Q_ARG(QString, filename), Q_ARG(duration_t, duration));
|
||||||
|
return { videoImage, MEDIATYPE_VIDEO, duration };
|
||||||
|
} else {
|
||||||
|
// Video-thumbnailing is disabled. Write a thumbnail without picture.
|
||||||
|
return addVideoThumbnailToCache(originalFilename, duration, QImage(), {0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Thumbnailer::Thumbnail Thumbnailer::addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail)
|
Thumbnailer::Thumbnail Thumbnailer::addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail)
|
||||||
{
|
{
|
||||||
// The format of a picture-thumbnail is very simple:
|
// The format of a picture-thumbnail is very simple:
|
||||||
|
@ -304,6 +367,44 @@ Thumbnailer::Thumbnail Thumbnailer::addUnknownThumbnailToCache(const QString &pi
|
||||||
return { unknownImage, MEDIATYPE_UNKNOWN, 0 };
|
return { unknownImage, MEDIATYPE_UNKNOWN, 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Thumbnailer::frameExtracted(QString filename, QImage thumbnail, duration_t duration, duration_t offset)
|
||||||
|
{
|
||||||
|
if (thumbnail.isNull()) {
|
||||||
|
frameExtractionFailed(filename, duration);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
int size = maxThumbnailSize();
|
||||||
|
thumbnail = thumbnail.scaled(size, size, Qt::KeepAspectRatio);
|
||||||
|
markVideoThumbnail(thumbnail);
|
||||||
|
addVideoThumbnailToCache(filename, duration, thumbnail, offset);
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
workingOn.remove(filename);
|
||||||
|
emit thumbnailChanged(filename, thumbnail, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If frame extraction failed, don't show an error image, because we don't want
|
||||||
|
// to penalize users that haven't installed ffmpe. Simply remove this item from
|
||||||
|
// the work-queue.
|
||||||
|
void Thumbnailer::frameExtractionFailed(QString filename, duration_t duration)
|
||||||
|
{
|
||||||
|
// Frame extraction failed, but this was due to ffmpeg not starting
|
||||||
|
// add to the thumbnail cache as a video image with unknown thumbnail.
|
||||||
|
addVideoThumbnailToCache(filename, duration, QImage(), { 0 });
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
workingOn.remove(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Thumbnailer::frameExtractionInvalid(QString filename, duration_t)
|
||||||
|
{
|
||||||
|
// Frame extraction failed because ffmpeg could not parse the file.
|
||||||
|
// For now, let's mark this as an unknown file. The user may want
|
||||||
|
// to recalculate thumbnails with an updated ffmpeg binary..?
|
||||||
|
addUnknownThumbnailToCache(filename);
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
workingOn.remove(filename);
|
||||||
|
}
|
||||||
|
|
||||||
void Thumbnailer::recalculate(QString filename)
|
void Thumbnailer::recalculate(QString filename)
|
||||||
{
|
{
|
||||||
Thumbnail thumbnail = getHashedImage(filename, true);
|
Thumbnail thumbnail = getHashedImage(filename, true);
|
||||||
|
@ -380,6 +481,10 @@ void Thumbnailer::calculateThumbnails(const QVector<QString> &filenames)
|
||||||
|
|
||||||
void Thumbnailer::clearWorkQueue()
|
void Thumbnailer::clearWorkQueue()
|
||||||
{
|
{
|
||||||
|
// We also want to clear the working-queue of the video-frame-extractor so that
|
||||||
|
// we don't get thumbnails that we don't care about.
|
||||||
|
VideoFrameExtractor::instance()->clearWorkQueue();
|
||||||
|
|
||||||
QMutexLocker l(&lock);
|
QMutexLocker l(&lock);
|
||||||
for (auto it = workingOn.begin(); it != workingOn.end(); ++it)
|
for (auto it = workingOn.begin(); it != workingOn.end(); ++it)
|
||||||
it->cancel();
|
it->cancel();
|
||||||
|
|
|
@ -46,6 +46,9 @@ public:
|
||||||
public slots:
|
public slots:
|
||||||
void imageDownloaded(QString filename);
|
void imageDownloaded(QString filename);
|
||||||
void imageDownloadFailed(QString filename);
|
void imageDownloadFailed(QString filename);
|
||||||
|
void frameExtracted(QString filename, QImage thumbnail, duration_t duration, duration_t offset);
|
||||||
|
void frameExtractionFailed(QString filename, duration_t duration);
|
||||||
|
void frameExtractionInvalid(QString filename, duration_t duration);
|
||||||
signals:
|
signals:
|
||||||
void thumbnailChanged(QString filename, QImage thumbnail, duration_t duration);
|
void thumbnailChanged(QString filename, QImage thumbnail, duration_t duration);
|
||||||
private:
|
private:
|
||||||
|
@ -56,22 +59,26 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
Thumbnailer();
|
Thumbnailer();
|
||||||
|
Thumbnail fetchVideoThumbnail(const QString &filename, const QString &originalFilename, duration_t duration);
|
||||||
|
Thumbnail extractVideoThumbnail(const QString &picture_filename, duration_t duration);
|
||||||
Thumbnail addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail);
|
Thumbnail addPictureThumbnailToCache(const QString &picture_filename, const QImage &thumbnail);
|
||||||
Thumbnail addVideoThumbnailToCache(const QString &picture_filename, duration_t duration);
|
Thumbnail addVideoThumbnailToCache(const QString &picture_filename, duration_t duration, const QImage &thumbnail, duration_t position);
|
||||||
Thumbnail addUnknownThumbnailToCache(const QString &picture_filename);
|
Thumbnail addUnknownThumbnailToCache(const QString &picture_filename);
|
||||||
void recalculate(QString filename);
|
void recalculate(QString filename);
|
||||||
void processItem(QString filename, bool tryDownload);
|
void processItem(QString filename, bool tryDownload);
|
||||||
Thumbnail getThumbnailFromCache(const QString &picture_filename);
|
Thumbnail getThumbnailFromCache(const QString &picture_filename);
|
||||||
Thumbnail getPictureThumbnailFromStream(QDataStream &stream);
|
Thumbnail getPictureThumbnailFromStream(QDataStream &stream);
|
||||||
Thumbnail getVideoThumbnailFromStream(QDataStream &stream);
|
Thumbnail getVideoThumbnailFromStream(QDataStream &stream, const QString &filename);
|
||||||
Thumbnail fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload);
|
Thumbnail fetchImage(const QString &filename, const QString &originalFilename, bool tryDownload);
|
||||||
Thumbnail getHashedImage(const QString &filename, bool tryDownload);
|
Thumbnail getHashedImage(const QString &filename, bool tryDownload);
|
||||||
|
void markVideoThumbnail(QImage &img);
|
||||||
|
|
||||||
mutable QMutex lock;
|
mutable QMutex lock;
|
||||||
QThreadPool pool;
|
QThreadPool pool;
|
||||||
QImage failImage; // Shown when image-fetching fails
|
QImage failImage; // Shown when image-fetching fails
|
||||||
QImage dummyImage; // Shown before thumbnail is fetched
|
QImage dummyImage; // Shown before thumbnail is fetched
|
||||||
QImage videoImage; // Place holder for videos
|
QImage videoImage; // Place holder for videos
|
||||||
|
QImage videoOverlayImage; // Overlay for video thumbnails
|
||||||
QImage unknownImage; // Place holder for files where we couldn't determine the type
|
QImage unknownImage; // Place holder for files where we couldn't determine the type
|
||||||
|
|
||||||
QMap<QString,QFuture<void>> workingOn;
|
QMap<QString,QFuture<void>> workingOn;
|
||||||
|
|
|
@ -103,6 +103,9 @@ struct preferences {
|
||||||
|
|
||||||
// ********** General **********
|
// ********** General **********
|
||||||
bool auto_recalculate_thumbnails;
|
bool auto_recalculate_thumbnails;
|
||||||
|
bool extract_video_thumbnails;
|
||||||
|
int extract_video_thumbnails_position; // position in stream: 0=first 100=last second
|
||||||
|
const char *ffmpeg_executable; // path of ffmpeg binary
|
||||||
int defaultsetpoint; // default setpoint in mbar
|
int defaultsetpoint; // default setpoint in mbar
|
||||||
const char *default_cylinder;
|
const char *default_cylinder;
|
||||||
const char *default_filename;
|
const char *default_filename;
|
||||||
|
|
|
@ -1474,6 +1474,21 @@ bool GeneralSettingsObjectWrapper::autoRecalculateThumbnails() const
|
||||||
return prefs.auto_recalculate_thumbnails;
|
return prefs.auto_recalculate_thumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GeneralSettingsObjectWrapper::extractVideoThumbnails() const
|
||||||
|
{
|
||||||
|
return prefs.extract_video_thumbnails;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GeneralSettingsObjectWrapper::extractVideoThumbnailsPosition() const
|
||||||
|
{
|
||||||
|
return prefs.extract_video_thumbnails_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GeneralSettingsObjectWrapper::ffmpegExecutable() const
|
||||||
|
{
|
||||||
|
return prefs.ffmpeg_executable;
|
||||||
|
}
|
||||||
|
|
||||||
void GeneralSettingsObjectWrapper::setDefaultFilename(const QString& value)
|
void GeneralSettingsObjectWrapper::setDefaultFilename(const QString& value)
|
||||||
{
|
{
|
||||||
if (value == prefs.default_filename)
|
if (value == prefs.default_filename)
|
||||||
|
@ -1579,6 +1594,43 @@ void GeneralSettingsObjectWrapper::setAutoRecalculateThumbnails(bool value)
|
||||||
emit autoRecalculateThumbnailsChanged(value);
|
emit autoRecalculateThumbnailsChanged(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GeneralSettingsObjectWrapper::setExtractVideoThumbnails(bool value)
|
||||||
|
{
|
||||||
|
if (value == prefs.extract_video_thumbnails)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(group);
|
||||||
|
s.setValue("extract_video_thumbnails", value);
|
||||||
|
prefs.extract_video_thumbnails = value;
|
||||||
|
emit extractVideoThumbnailsChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeneralSettingsObjectWrapper::setExtractVideoThumbnailsPosition(int value)
|
||||||
|
{
|
||||||
|
if (value == prefs.extract_video_thumbnails_position)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(group);
|
||||||
|
s.setValue("extract_video_thumbnails_position", value);
|
||||||
|
prefs.extract_video_thumbnails_position = value;
|
||||||
|
emit extractVideoThumbnailsPositionChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeneralSettingsObjectWrapper::setFfmpegExecutable(const QString &value)
|
||||||
|
{
|
||||||
|
if (value == prefs.ffmpeg_executable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QSettings s;
|
||||||
|
s.beginGroup(group);
|
||||||
|
s.setValue("ffmpeg_executable", value);
|
||||||
|
free((void *)prefs.ffmpeg_executable);
|
||||||
|
prefs.ffmpeg_executable = copy_qstring(value);
|
||||||
|
emit ffmpegExecutableChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
LanguageSettingsObjectWrapper::LanguageSettingsObjectWrapper(QObject *parent) :
|
LanguageSettingsObjectWrapper::LanguageSettingsObjectWrapper(QObject *parent) :
|
||||||
QObject(parent)
|
QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
|
@ -443,6 +443,9 @@ class GeneralSettingsObjectWrapper : public QObject {
|
||||||
Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
|
Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
|
||||||
Q_PROPERTY(int pscr_ratio READ pscrRatio WRITE setPscrRatio NOTIFY pscrRatioChanged)
|
Q_PROPERTY(int pscr_ratio READ pscrRatio WRITE setPscrRatio NOTIFY pscrRatioChanged)
|
||||||
Q_PROPERTY(bool auto_recalculate_thumbnails READ autoRecalculateThumbnails WRITE setAutoRecalculateThumbnails NOTIFY autoRecalculateThumbnailsChanged)
|
Q_PROPERTY(bool auto_recalculate_thumbnails READ autoRecalculateThumbnails WRITE setAutoRecalculateThumbnails NOTIFY autoRecalculateThumbnailsChanged)
|
||||||
|
Q_PROPERTY(bool extract_video_thumbnails READ extractVideoThumbnails WRITE setExtractVideoThumbnails NOTIFY extractVideoThumbnailsChanged)
|
||||||
|
Q_PROPERTY(int extract_video_thumbnails_position READ extractVideoThumbnailsPosition WRITE setExtractVideoThumbnailsPosition NOTIFY extractVideoThumbnailsPositionChanged)
|
||||||
|
Q_PROPERTY(QString ffmpeg_executable READ ffmpegExecutable WRITE setFfmpegExecutable NOTIFY ffmpegExecutableChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GeneralSettingsObjectWrapper(QObject *parent);
|
GeneralSettingsObjectWrapper(QObject *parent);
|
||||||
|
@ -454,6 +457,9 @@ public:
|
||||||
int o2Consumption() const;
|
int o2Consumption() const;
|
||||||
int pscrRatio() const;
|
int pscrRatio() const;
|
||||||
bool autoRecalculateThumbnails() const;
|
bool autoRecalculateThumbnails() const;
|
||||||
|
bool extractVideoThumbnails() const;
|
||||||
|
int extractVideoThumbnailsPosition() const;
|
||||||
|
QString ffmpegExecutable() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setDefaultFilename (const QString& value);
|
void setDefaultFilename (const QString& value);
|
||||||
|
@ -464,6 +470,9 @@ public slots:
|
||||||
void setO2Consumption (int value);
|
void setO2Consumption (int value);
|
||||||
void setPscrRatio (int value);
|
void setPscrRatio (int value);
|
||||||
void setAutoRecalculateThumbnails (bool value);
|
void setAutoRecalculateThumbnails (bool value);
|
||||||
|
void setExtractVideoThumbnails (bool value);
|
||||||
|
void setExtractVideoThumbnailsPosition (int value);
|
||||||
|
void setFfmpegExecutable (const QString &value);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void defaultFilenameChanged(const QString& value);
|
void defaultFilenameChanged(const QString& value);
|
||||||
|
@ -474,6 +483,9 @@ signals:
|
||||||
void o2ConsumptionChanged(int value);
|
void o2ConsumptionChanged(int value);
|
||||||
void pscrRatioChanged(int value);
|
void pscrRatioChanged(int value);
|
||||||
void autoRecalculateThumbnailsChanged(int value);
|
void autoRecalculateThumbnailsChanged(int value);
|
||||||
|
void extractVideoThumbnailsChanged(bool value);
|
||||||
|
void extractVideoThumbnailsPositionChanged(int value);
|
||||||
|
void ffmpegExecutableChanged(const QString &value);
|
||||||
private:
|
private:
|
||||||
const QString group = QStringLiteral("GeneralSettings");
|
const QString group = QStringLiteral("GeneralSettings");
|
||||||
};
|
};
|
||||||
|
|
|
@ -100,6 +100,8 @@ struct preferences default_prefs = {
|
||||||
.cloud_timeout = 5,
|
.cloud_timeout = 5,
|
||||||
#endif
|
#endif
|
||||||
.auto_recalculate_thumbnails = true,
|
.auto_recalculate_thumbnails = true,
|
||||||
|
.extract_video_thumbnails = true,
|
||||||
|
.extract_video_thumbnails_position = 20, // The first fifth seems like a reasonable place
|
||||||
};
|
};
|
||||||
|
|
||||||
int run_survey;
|
int run_survey;
|
||||||
|
@ -287,6 +289,7 @@ void setup_system_prefs(void)
|
||||||
subsurface_OS_pref_setup();
|
subsurface_OS_pref_setup();
|
||||||
default_prefs.divelist_font = strdup(system_divelist_default_font);
|
default_prefs.divelist_font = strdup(system_divelist_default_font);
|
||||||
default_prefs.font_size = system_divelist_default_font_size;
|
default_prefs.font_size = system_divelist_default_font_size;
|
||||||
|
default_prefs.ffmpeg_executable = strdup("ffmpeg");
|
||||||
|
|
||||||
#if !defined(SUBSURFACE_MOBILE)
|
#if !defined(SUBSURFACE_MOBILE)
|
||||||
default_prefs.default_filename = copy_string(system_default_filename());
|
default_prefs.default_filename = copy_string(system_default_filename());
|
||||||
|
@ -331,6 +334,7 @@ void copy_prefs(struct preferences *src, struct preferences *dest)
|
||||||
dest->facebook.access_token = copy_string(src->facebook.access_token);
|
dest->facebook.access_token = copy_string(src->facebook.access_token);
|
||||||
dest->facebook.user_id = copy_string(src->facebook.user_id);
|
dest->facebook.user_id = copy_string(src->facebook.user_id);
|
||||||
dest->facebook.album_id = copy_string(src->facebook.album_id);
|
dest->facebook.album_id = copy_string(src->facebook.album_id);
|
||||||
|
dest->ffmpeg_executable = copy_string(src->ffmpeg_executable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
120
core/videoframeextractor.cpp
Normal file
120
core/videoframeextractor.cpp
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
|
#include "videoframeextractor.h"
|
||||||
|
#include "imagedownloader.h"
|
||||||
|
#include "core/pref.h"
|
||||||
|
#include "core/dive.h" // for report_error()!
|
||||||
|
|
||||||
|
#include <QtConcurrent>
|
||||||
|
#include <QProcess>
|
||||||
|
|
||||||
|
// Note: this is a global instead of a function-local variable on purpose.
|
||||||
|
// We don't want this to be generated in a different thread context if
|
||||||
|
// VideoFrameExtractor::instance() is called from a worker thread.
|
||||||
|
static VideoFrameExtractor frameExtractor;
|
||||||
|
VideoFrameExtractor *VideoFrameExtractor::instance()
|
||||||
|
{
|
||||||
|
return &frameExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoFrameExtractor::VideoFrameExtractor()
|
||||||
|
{
|
||||||
|
// Currently, we only process one video at a time.
|
||||||
|
// Eventually, we might want to increase this value.
|
||||||
|
pool.setMaxThreadCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoFrameExtractor::extract(QString originalFilename, QString filename, duration_t duration)
|
||||||
|
{
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
if (!workingOn.contains(originalFilename)) {
|
||||||
|
// We are not currently extracting this video - add it to the list.
|
||||||
|
workingOn.insert(originalFilename, QtConcurrent::run(&pool, [this, originalFilename, filename, duration]()
|
||||||
|
{ processItem(originalFilename, filename, duration); }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoFrameExtractor::fail(const QString &originalFilename, duration_t duration, bool isInvalid)
|
||||||
|
{
|
||||||
|
if (isInvalid)
|
||||||
|
emit invalid(originalFilename, duration);
|
||||||
|
else
|
||||||
|
emit failed(originalFilename, duration);
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
workingOn.remove(originalFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoFrameExtractor::clearWorkQueue()
|
||||||
|
{
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
for (auto it = workingOn.begin(); it != workingOn.end(); ++it)
|
||||||
|
it->cancel();
|
||||||
|
workingOn.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trivial helper: bring value into given range
|
||||||
|
template <typename T>
|
||||||
|
T clamp(T v, T lo, T hi)
|
||||||
|
{
|
||||||
|
return v < lo ? lo : v > hi ? hi : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoFrameExtractor::processItem(QString originalFilename, QString filename, duration_t duration)
|
||||||
|
{
|
||||||
|
// If video frame extraction is turned off (e.g. because we failed to start ffmpeg),
|
||||||
|
// abort immediately.
|
||||||
|
if (!prefs.extract_video_thumbnails) {
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
workingOn.remove(originalFilename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the time where we want to extract the image.
|
||||||
|
// If the duration is < 10 sec, just snap the first frame
|
||||||
|
duration_t position = { 0 };
|
||||||
|
if (duration.seconds > 10) {
|
||||||
|
// We round to second-precision. To be sure that we don't attempt reading past the
|
||||||
|
// video's end, round down by one second.
|
||||||
|
--duration.seconds;
|
||||||
|
position.seconds = clamp(duration.seconds * prefs.extract_video_thumbnails_position / 100,
|
||||||
|
0, duration.seconds);
|
||||||
|
}
|
||||||
|
QString posString = QString("%1:%2:%3").arg(position.seconds / 3600, 2, 10, QChar('0'))
|
||||||
|
.arg((position.seconds % 3600) / 60, 2, 10, QChar('0'))
|
||||||
|
.arg(position.seconds % 60, 2, 10, QChar('0'));
|
||||||
|
|
||||||
|
QProcess ffmpeg;
|
||||||
|
ffmpeg.start(prefs.ffmpeg_executable, QStringList {
|
||||||
|
"-ss", posString, "-i", filename, "-vframes", "1", "-q:v", "2", "-f", "image2", "-"
|
||||||
|
});
|
||||||
|
if (!ffmpeg.waitForStarted()) {
|
||||||
|
// Since we couldn't sart ffmpeg, turn off thumbnailing
|
||||||
|
// TODO: call the proper preferences-functions
|
||||||
|
prefs.extract_video_thumbnails = false;
|
||||||
|
report_error(qPrintable(tr("ffmpeg failed to start - video thumbnail creation suspended")));
|
||||||
|
qDebug() << "Failed to start ffmpeg";
|
||||||
|
return fail(originalFilename, duration, false);
|
||||||
|
}
|
||||||
|
if (!ffmpeg.waitForFinished()) {
|
||||||
|
qDebug() << "Failed waiting for ffmpeg";
|
||||||
|
report_error(qPrintable(tr("failed waiting for ffmpeg - video thumbnail creation suspended")));
|
||||||
|
return fail(originalFilename, duration, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray data = ffmpeg.readAll();
|
||||||
|
QImage img;
|
||||||
|
img.loadFromData(data);
|
||||||
|
if (img.isNull()) {
|
||||||
|
qInfo() << "Failed reading ffmpeg output";
|
||||||
|
// For debugging:
|
||||||
|
//qInfo() << "stdout: " << QString::fromUtf8(data);
|
||||||
|
ffmpeg.setReadChannel(QProcess::StandardError);
|
||||||
|
// For debugging:
|
||||||
|
//QByteArray stderr_output = ffmpeg.readAll();
|
||||||
|
//qInfo() << "stderr: " << QString::fromUtf8(stderr_output);
|
||||||
|
return fail(originalFilename, duration, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit extracted(originalFilename, img, duration, position);
|
||||||
|
QMutexLocker l(&lock);
|
||||||
|
workingOn.remove(originalFilename);
|
||||||
|
}
|
37
core/videoframeextractor.h
Normal file
37
core/videoframeextractor.h
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0
|
||||||
|
#ifndef VIDEOFRAMEEXTRACTOR_H
|
||||||
|
#define VIDEOFRAMEEXTRACTOR_H
|
||||||
|
|
||||||
|
#include "core/units.h"
|
||||||
|
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QFuture>
|
||||||
|
#include <QThreadPool>
|
||||||
|
#include <QQueue>
|
||||||
|
#include <QString>
|
||||||
|
#include <QPair>
|
||||||
|
|
||||||
|
class VideoFrameExtractor : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
VideoFrameExtractor();
|
||||||
|
static VideoFrameExtractor *instance();
|
||||||
|
signals:
|
||||||
|
void extracted(QString filename, QImage, duration_t duration, duration_t offset);
|
||||||
|
// There are two failure modes:
|
||||||
|
// failed() -> we failed to start ffmpeg. Write a thumbnail signalling "maybe try again".
|
||||||
|
// invalid() -> we started ffmpeg, but that couldn't extract an image. Signal "this file is broken".
|
||||||
|
void failed(QString filename, duration_t duration);
|
||||||
|
void invalid(QString filename, duration_t duration);
|
||||||
|
public slots:
|
||||||
|
void extract(QString originalFilename, QString filename, duration_t duration);
|
||||||
|
void clearWorkQueue();
|
||||||
|
private:
|
||||||
|
void processItem(QString originalFilename, QString filename, duration_t duration);
|
||||||
|
void fail(const QString &originalFilename, duration_t duration, bool isInvalid);
|
||||||
|
mutable QMutex lock;
|
||||||
|
QThreadPool pool;
|
||||||
|
QMap<QString, QFuture<void>> workingOn;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -43,6 +43,22 @@ void PreferencesDefaults::on_localDefaultFile_toggled(bool toggle)
|
||||||
ui->chooseFile->setEnabled(toggle);
|
ui->chooseFile->setEnabled(toggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PreferencesDefaults::on_ffmpegFile_clicked()
|
||||||
|
{
|
||||||
|
QFileInfo fi(system_default_filename());
|
||||||
|
QString ffmpegFileName = QFileDialog::getOpenFileName(this, tr("Select ffmpeg executable"));
|
||||||
|
|
||||||
|
if (!ffmpegFileName.isEmpty())
|
||||||
|
ui->ffmpegExecutable->setText(ffmpegFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreferencesDefaults::on_extractVideoThumbnails_toggled(bool toggled)
|
||||||
|
{
|
||||||
|
ui->videoThumbnailPosition->setEnabled(toggled);
|
||||||
|
ui->ffmpegExecutable->setEnabled(toggled);
|
||||||
|
ui->ffmpegFile->setEnabled(toggled);
|
||||||
|
}
|
||||||
|
|
||||||
void PreferencesDefaults::refreshSettings()
|
void PreferencesDefaults::refreshSettings()
|
||||||
{
|
{
|
||||||
ui->font->setCurrentFont(QString(prefs.divelist_font));
|
ui->font->setCurrentFont(QString(prefs.divelist_font));
|
||||||
|
@ -73,6 +89,14 @@ void PreferencesDefaults::refreshSettings()
|
||||||
ui->defaultfilename->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
|
ui->defaultfilename->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
|
||||||
ui->btnUseDefaultFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
|
ui->btnUseDefaultFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
|
||||||
ui->chooseFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
|
ui->chooseFile->setEnabled(prefs.default_file_behavior == LOCAL_DEFAULT_FILE);
|
||||||
|
|
||||||
|
ui->videoThumbnailPosition->setEnabled(prefs.extract_video_thumbnails);
|
||||||
|
ui->ffmpegExecutable->setEnabled(prefs.extract_video_thumbnails);
|
||||||
|
ui->ffmpegFile->setEnabled(prefs.extract_video_thumbnails);
|
||||||
|
|
||||||
|
ui->extractVideoThumbnails->setChecked(prefs.extract_video_thumbnails);
|
||||||
|
ui->videoThumbnailPosition->setValue(prefs.extract_video_thumbnails_position);
|
||||||
|
ui->ffmpegExecutable->setText(prefs.ffmpeg_executable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PreferencesDefaults::syncSettings()
|
void PreferencesDefaults::syncSettings()
|
||||||
|
@ -87,6 +111,9 @@ void PreferencesDefaults::syncSettings()
|
||||||
general->setDefaultFileBehavior(LOCAL_DEFAULT_FILE);
|
general->setDefaultFileBehavior(LOCAL_DEFAULT_FILE);
|
||||||
else if (ui->cloudDefaultFile->isChecked())
|
else if (ui->cloudDefaultFile->isChecked())
|
||||||
general->setDefaultFileBehavior(CLOUD_DEFAULT_FILE);
|
general->setDefaultFileBehavior(CLOUD_DEFAULT_FILE);
|
||||||
|
general->setExtractVideoThumbnails(ui->extractVideoThumbnails->isChecked());
|
||||||
|
general->setExtractVideoThumbnailsPosition(ui->videoThumbnailPosition->value());
|
||||||
|
general->setFfmpegExecutable(ui->ffmpegExecutable->text());
|
||||||
|
|
||||||
auto display = qPrefDisplay::instance();
|
auto display = qPrefDisplay::instance();
|
||||||
display->set_divelist_font(ui->font->currentFont().toString());
|
display->set_divelist_font(ui->font->currentFont().toString());
|
||||||
|
|
|
@ -20,6 +20,8 @@ public slots:
|
||||||
void on_chooseFile_clicked();
|
void on_chooseFile_clicked();
|
||||||
void on_btnUseDefaultFile_toggled(bool toggled);
|
void on_btnUseDefaultFile_toggled(bool toggled);
|
||||||
void on_localDefaultFile_toggled(bool toggled);
|
void on_localDefaultFile_toggled(bool toggled);
|
||||||
|
void on_ffmpegFile_clicked();
|
||||||
|
void on_extractVideoThumbnails_toggled(bool toggled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::PreferencesDefaults *ui;
|
Ui::PreferencesDefaults *ui;
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
<item>
|
<item>
|
||||||
<widget class="QRadioButton" name="noDefaultFile">
|
<widget class="QRadioButton" name="noDefaultFile">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>No default file</string>
|
<string>&No default file</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -209,6 +209,79 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_10">
|
||||||
|
<property name="title">
|
||||||
|
<string>Video thumbnails</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QFormLayout" name="formLayout">
|
||||||
|
<property name="horizontalSpacing">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<property name="verticalSpacing">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<property name="margin">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="ffmpegExectuableLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>ffmpeg executable</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3b">
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="ffmpegExecutable"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="ffmpegFile">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="videoThumbnailPositionLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Extract at position</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QSlider" name="videoThumbnailPosition">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>20</number>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="extractVideoThumbnailsLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Extract video thumbnails</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QCheckBox" name="extractVideoThumbnails">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox_9">
|
<widget class="QGroupBox" name="groupBox_9">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
|
|
263
icons/video_overlay.svg
Normal file
263
icons/video_overlay.svg
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
height="66"
|
||||||
|
width="22"
|
||||||
|
id="svg25756"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 22 66"
|
||||||
|
sodipodi:docname="video_overlay.svg"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1015"
|
||||||
|
id="namedview870"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="12.242424"
|
||||||
|
inkscape:cx="11"
|
||||||
|
inkscape:cy="33"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg25756" />
|
||||||
|
<metadata
|
||||||
|
id="metadata25760">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs3051">
|
||||||
|
<style
|
||||||
|
id="current-color-scheme"
|
||||||
|
type="text/css">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#4d4d4d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
id="path1012"
|
||||||
|
d="M 1.6913554e-16,4.7920842e-9 H 2.7632416 V 66 H 0 Z"
|
||||||
|
style="color:#4d4d4d;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
id="path904"
|
||||||
|
d="m 9,31 v 4 l 4,-2 z"
|
||||||
|
style="color:#4d4d4d;fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-miterlimit:4;stroke-dasharray:none;stroke-linejoin:bevel;stroke-linecap:round" />
|
||||||
|
<path
|
||||||
|
id="path888"
|
||||||
|
d="M 0.69081079,39.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,36.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path916" />
|
||||||
|
<path
|
||||||
|
id="path920"
|
||||||
|
d="M 0.69081079,33.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,30.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path924" />
|
||||||
|
<path
|
||||||
|
id="path928"
|
||||||
|
d="M 0.69081079,27.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,24.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path932" />
|
||||||
|
<path
|
||||||
|
id="path936"
|
||||||
|
d="M 0.69081079,21.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,18.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path940" />
|
||||||
|
<path
|
||||||
|
id="path944"
|
||||||
|
d="M 0.69081079,15.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,12.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path948" />
|
||||||
|
<path
|
||||||
|
id="path952"
|
||||||
|
d="M 0.69081079,9.8833914 H 2.0724306 V 11.265012 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,6.8833914 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path956" />
|
||||||
|
<path
|
||||||
|
id="path960"
|
||||||
|
d="M 0.69081079,3.8833914 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,0.88339134 H 2.0724306 V 2.2650124 H 0.69081079 Z"
|
||||||
|
id="path964" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,42.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path968" />
|
||||||
|
<path
|
||||||
|
id="path976"
|
||||||
|
d="M 0.69081079,45.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,48.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path980" />
|
||||||
|
<path
|
||||||
|
id="path984"
|
||||||
|
d="M 0.69081079,51.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,54.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path988" />
|
||||||
|
<path
|
||||||
|
id="path992"
|
||||||
|
d="M 0.69081079,57.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,60.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path996" />
|
||||||
|
<path
|
||||||
|
id="path1000"
|
||||||
|
d="M 0.69081079,60.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
id="path1004"
|
||||||
|
d="M 0.69081079,63.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="M 0.69081079,63.883391 H 2.0724316 v 1.381621 H 0.69081079 Z"
|
||||||
|
id="path1008" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.237,4.7920842e-9 h 2.763242 V 66 H 19.237 Z"
|
||||||
|
id="path1018" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,39.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1020" />
|
||||||
|
<path
|
||||||
|
id="path1022"
|
||||||
|
d="m 19.927811,36.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,33.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1024" />
|
||||||
|
<path
|
||||||
|
id="path1026"
|
||||||
|
d="m 19.927811,30.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,27.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1028" />
|
||||||
|
<path
|
||||||
|
id="path1030"
|
||||||
|
d="m 19.927811,24.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,21.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1032" />
|
||||||
|
<path
|
||||||
|
id="path1034"
|
||||||
|
d="m 19.927811,18.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,15.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1036" />
|
||||||
|
<path
|
||||||
|
id="path1038"
|
||||||
|
d="m 19.927811,12.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,9.8833914 h 1.38162 v 1.3816206 h -1.38162 z"
|
||||||
|
id="path1040" />
|
||||||
|
<path
|
||||||
|
id="path1042"
|
||||||
|
d="m 19.927811,6.8833914 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,3.8833914 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1044" />
|
||||||
|
<path
|
||||||
|
id="path1046"
|
||||||
|
d="m 19.927811,0.88339134 h 1.38162 V 2.2650124 h -1.38162 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
id="path1048"
|
||||||
|
d="m 19.927811,42.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,45.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1050" />
|
||||||
|
<path
|
||||||
|
id="path1052"
|
||||||
|
d="m 19.927811,48.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,51.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1054" />
|
||||||
|
<path
|
||||||
|
id="path1056"
|
||||||
|
d="m 19.927811,54.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,57.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1058" />
|
||||||
|
<path
|
||||||
|
id="path1060"
|
||||||
|
d="m 19.927811,60.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,60.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1062" />
|
||||||
|
<path
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
|
||||||
|
d="m 19.927811,63.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
id="path1064" />
|
||||||
|
<path
|
||||||
|
id="path1066"
|
||||||
|
d="m 19.927811,63.883391 h 1.381621 v 1.381621 h -1.381621 z"
|
||||||
|
style="color:#4d4d4d;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 11 KiB |
|
@ -93,6 +93,7 @@
|
||||||
<file alias="preferences-other-icon">icons/defaults.png</file>
|
<file alias="preferences-other-icon">icons/defaults.png</file>
|
||||||
<file alias="camera-icon">icons/camera.svg</file>
|
<file alias="camera-icon">icons/camera.svg</file>
|
||||||
<file alias="video-icon">icons/video.svg</file>
|
<file alias="video-icon">icons/video.svg</file>
|
||||||
|
<file alias="video-overlay">icons/video_overlay.svg</file>
|
||||||
<file alias="unknown-icon">icons/unknown.svg</file>
|
<file alias="unknown-icon">icons/unknown.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
Loading…
Add table
Reference in a new issue