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-downloader.c
|
||||
version.c
|
||||
videoframeextractor.cpp
|
||||
windowtitleupdate.cpp
|
||||
worldmap-save.c
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "divelist.h"
|
||||
#include "qthelper.h"
|
||||
#include "imagedownloader.h"
|
||||
#include "videoframeextractor.h"
|
||||
#include "qt-models/divepicturemodel.h"
|
||||
#include "metadata.h"
|
||||
#include <unistd.h>
|
||||
|
@ -96,7 +97,7 @@ Thumbnailer::Thumbnail Thumbnailer::fetchImage(const QString &filename, const QS
|
|||
if (type == MEDIATYPE_IO_ERROR)
|
||||
return { failImage, MEDIATYPE_IO_ERROR, 0 };
|
||||
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.
|
||||
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,
|
||||
// we pass 0 as the duration.
|
||||
if (hasVideoFileExtension(filename))
|
||||
return addVideoThumbnailToCache(originalFilename, {0} );
|
||||
return fetchVideoThumbnail(filename, originalFilename, {0} );
|
||||
|
||||
// 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.
|
||||
|
@ -163,9 +164,22 @@ static QImage renderIcon(const char *id, int size)
|
|||
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
|
||||
dummyImage(renderIcon(":camera-icon", maxThumbnailSize())),
|
||||
videoImage(renderIcon(":video-icon", maxThumbnailSize())),
|
||||
videoOverlayImage(renderIconWidth(":video-overlay", maxThumbnailSize())),
|
||||
unknownImage(renderIcon(":unknown-icon", maxThumbnailSize()))
|
||||
{
|
||||
// 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);
|
||||
connect(ImageDownloader::instance(), &ImageDownloader::loaded, this, &Thumbnailer::imageDownloaded);
|
||||
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()
|
||||
|
@ -188,7 +205,17 @@ Thumbnailer::Thumbnail Thumbnailer::getPictureThumbnailFromStream(QDataStream &s
|
|||
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;
|
||||
stream >> duration >> numPics;
|
||||
|
@ -200,16 +227,27 @@ Thumbnailer::Thumbnail Thumbnailer::getVideoThumbnailFromStream(QDataStream &str
|
|||
if (stream.status() != QDataStream::Ok || duration > 36000 || numPics > 10000)
|
||||
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
|
||||
QImage res;
|
||||
if (numPics > 0) {
|
||||
quint32 offset;
|
||||
QImage res;
|
||||
stream >> offset >> res;
|
||||
}
|
||||
|
||||
// No picture -> show dummy-icon
|
||||
return { res.isNull() ? videoImage : res, MEDIATYPE_VIDEO, (int32_t)duration };
|
||||
if (res.isNull())
|
||||
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.
|
||||
|
@ -248,13 +286,14 @@ Thumbnailer::Thumbnail Thumbnailer::getThumbnailFromCache(const QString &picture
|
|||
|
||||
switch (type) {
|
||||
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 };
|
||||
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:
|
||||
// uint32 MEDIATYPE_VIDEO
|
||||
|
@ -270,12 +309,36 @@ Thumbnailer::Thumbnail Thumbnailer::addVideoThumbnailToCache(const QString &pict
|
|||
|
||||
stream << (quint32)MEDIATYPE_VIDEO;
|
||||
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();
|
||||
}
|
||||
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)
|
||||
{
|
||||
// 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 };
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Thumbnail thumbnail = getHashedImage(filename, true);
|
||||
|
@ -380,6 +481,10 @@ void Thumbnailer::calculateThumbnails(const QVector<QString> &filenames)
|
|||
|
||||
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);
|
||||
for (auto it = workingOn.begin(); it != workingOn.end(); ++it)
|
||||
it->cancel();
|
||||
|
|
|
@ -46,6 +46,9 @@ public:
|
|||
public slots:
|
||||
void imageDownloaded(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:
|
||||
void thumbnailChanged(QString filename, QImage thumbnail, duration_t duration);
|
||||
private:
|
||||
|
@ -56,22 +59,26 @@ private:
|
|||
};
|
||||
|
||||
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 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);
|
||||
void recalculate(QString filename);
|
||||
void processItem(QString filename, bool tryDownload);
|
||||
Thumbnail getThumbnailFromCache(const QString &picture_filename);
|
||||
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 getHashedImage(const QString &filename, bool tryDownload);
|
||||
void markVideoThumbnail(QImage &img);
|
||||
|
||||
mutable QMutex lock;
|
||||
QThreadPool pool;
|
||||
QImage failImage; // Shown when image-fetching fails
|
||||
QImage dummyImage; // Shown before thumbnail is fetched
|
||||
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
|
||||
|
||||
QMap<QString,QFuture<void>> workingOn;
|
||||
|
|
|
@ -103,6 +103,9 @@ struct preferences {
|
|||
|
||||
// ********** General **********
|
||||
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
|
||||
const char *default_cylinder;
|
||||
const char *default_filename;
|
||||
|
|
|
@ -1474,6 +1474,21 @@ bool GeneralSettingsObjectWrapper::autoRecalculateThumbnails() const
|
|||
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)
|
||||
{
|
||||
if (value == prefs.default_filename)
|
||||
|
@ -1579,6 +1594,43 @@ void GeneralSettingsObjectWrapper::setAutoRecalculateThumbnails(bool 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) :
|
||||
QObject(parent)
|
||||
{
|
||||
|
|
|
@ -435,14 +435,17 @@ private:
|
|||
|
||||
class GeneralSettingsObjectWrapper : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString default_filename READ defaultFilename WRITE setDefaultFilename NOTIFY defaultFilenameChanged)
|
||||
Q_PROPERTY(QString default_cylinder READ defaultCylinder WRITE setDefaultCylinder NOTIFY defaultCylinderChanged)
|
||||
Q_PROPERTY(short default_file_behavior READ defaultFileBehavior WRITE setDefaultFileBehavior NOTIFY defaultFileBehaviorChanged)
|
||||
Q_PROPERTY(bool use_default_file READ useDefaultFile WRITE setUseDefaultFile NOTIFY useDefaultFileChanged)
|
||||
Q_PROPERTY(int defaultsetpoint READ defaultSetPoint WRITE setDefaultSetPoint NOTIFY defaultSetPointChanged)
|
||||
Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
|
||||
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(QString default_filename READ defaultFilename WRITE setDefaultFilename NOTIFY defaultFilenameChanged)
|
||||
Q_PROPERTY(QString default_cylinder READ defaultCylinder WRITE setDefaultCylinder NOTIFY defaultCylinderChanged)
|
||||
Q_PROPERTY(short default_file_behavior READ defaultFileBehavior WRITE setDefaultFileBehavior NOTIFY defaultFileBehaviorChanged)
|
||||
Q_PROPERTY(bool use_default_file READ useDefaultFile WRITE setUseDefaultFile NOTIFY useDefaultFileChanged)
|
||||
Q_PROPERTY(int defaultsetpoint READ defaultSetPoint WRITE setDefaultSetPoint NOTIFY defaultSetPointChanged)
|
||||
Q_PROPERTY(int o2consumption READ o2Consumption WRITE setO2Consumption NOTIFY o2ConsumptionChanged)
|
||||
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 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:
|
||||
GeneralSettingsObjectWrapper(QObject *parent);
|
||||
|
@ -454,6 +457,9 @@ public:
|
|||
int o2Consumption() const;
|
||||
int pscrRatio() const;
|
||||
bool autoRecalculateThumbnails() const;
|
||||
bool extractVideoThumbnails() const;
|
||||
int extractVideoThumbnailsPosition() const;
|
||||
QString ffmpegExecutable() const;
|
||||
|
||||
public slots:
|
||||
void setDefaultFilename (const QString& value);
|
||||
|
@ -464,6 +470,9 @@ public slots:
|
|||
void setO2Consumption (int value);
|
||||
void setPscrRatio (int value);
|
||||
void setAutoRecalculateThumbnails (bool value);
|
||||
void setExtractVideoThumbnails (bool value);
|
||||
void setExtractVideoThumbnailsPosition (int value);
|
||||
void setFfmpegExecutable (const QString &value);
|
||||
|
||||
signals:
|
||||
void defaultFilenameChanged(const QString& value);
|
||||
|
@ -474,6 +483,9 @@ signals:
|
|||
void o2ConsumptionChanged(int value);
|
||||
void pscrRatioChanged(int value);
|
||||
void autoRecalculateThumbnailsChanged(int value);
|
||||
void extractVideoThumbnailsChanged(bool value);
|
||||
void extractVideoThumbnailsPositionChanged(int value);
|
||||
void ffmpegExecutableChanged(const QString &value);
|
||||
private:
|
||||
const QString group = QStringLiteral("GeneralSettings");
|
||||
};
|
||||
|
|
|
@ -100,6 +100,8 @@ struct preferences default_prefs = {
|
|||
.cloud_timeout = 5,
|
||||
#endif
|
||||
.auto_recalculate_thumbnails = true,
|
||||
.extract_video_thumbnails = true,
|
||||
.extract_video_thumbnails_position = 20, // The first fifth seems like a reasonable place
|
||||
};
|
||||
|
||||
int run_survey;
|
||||
|
@ -287,6 +289,7 @@ void setup_system_prefs(void)
|
|||
subsurface_OS_pref_setup();
|
||||
default_prefs.divelist_font = strdup(system_divelist_default_font);
|
||||
default_prefs.font_size = system_divelist_default_font_size;
|
||||
default_prefs.ffmpeg_executable = strdup("ffmpeg");
|
||||
|
||||
#if !defined(SUBSURFACE_MOBILE)
|
||||
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.user_id = copy_string(src->facebook.user_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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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->btnUseDefaultFile->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()
|
||||
|
@ -87,6 +111,9 @@ void PreferencesDefaults::syncSettings()
|
|||
general->setDefaultFileBehavior(LOCAL_DEFAULT_FILE);
|
||||
else if (ui->cloudDefaultFile->isChecked())
|
||||
general->setDefaultFileBehavior(CLOUD_DEFAULT_FILE);
|
||||
general->setExtractVideoThumbnails(ui->extractVideoThumbnails->isChecked());
|
||||
general->setExtractVideoThumbnailsPosition(ui->videoThumbnailPosition->value());
|
||||
general->setFfmpegExecutable(ui->ffmpegExecutable->text());
|
||||
|
||||
auto display = qPrefDisplay::instance();
|
||||
display->set_divelist_font(ui->font->currentFont().toString());
|
||||
|
|
|
@ -20,6 +20,8 @@ public slots:
|
|||
void on_chooseFile_clicked();
|
||||
void on_btnUseDefaultFile_toggled(bool toggled);
|
||||
void on_localDefaultFile_toggled(bool toggled);
|
||||
void on_ffmpegFile_clicked();
|
||||
void on_extractVideoThumbnails_toggled(bool toggled);
|
||||
|
||||
private:
|
||||
Ui::PreferencesDefaults *ui;
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
<item>
|
||||
<widget class="QRadioButton" name="noDefaultFile">
|
||||
<property name="text">
|
||||
<string>No default file</string>
|
||||
<string>&No default file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -209,6 +209,79 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</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>
|
||||
<widget class="QGroupBox" name="groupBox_9">
|
||||
<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="camera-icon">icons/camera.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>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
Loading…
Add table
Reference in a new issue