feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include <qbuffer.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qprocess.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringlist.h>
|
||||
#include <qtemporarydir.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ZShell::models {
|
||||
namespace {
|
||||
|
||||
inline const QStringList kVideoExtensions = {
|
||||
"mp4",
|
||||
"mkv",
|
||||
"webm",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"m4v",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"3gp",
|
||||
"ogv",
|
||||
"ts",
|
||||
"vob",
|
||||
};
|
||||
|
||||
inline bool hasVideoExtension(const QString& path) {
|
||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||
}
|
||||
|
||||
inline const QStringList kHardwareCodecs = {
|
||||
"av1",
|
||||
"hevc",
|
||||
"h264",
|
||||
"vp9",
|
||||
"vp8",
|
||||
"vc1",
|
||||
"mpeg2",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"mjpeg",
|
||||
};
|
||||
|
||||
inline bool probeHardwareCodec(const QString& path) {
|
||||
QProcess process;
|
||||
process.start(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
path,
|
||||
});
|
||||
process.waitForFinished(5000);
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString codec =
|
||||
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||
return kHardwareCodecs.contains(codec);
|
||||
}
|
||||
|
||||
inline QSize coverSizeNoUpscale(const QSize& source, const QSize& fit) {
|
||||
if (!source.isValid() || source.isEmpty() || !fit.isValid() ||
|
||||
fit.isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const QSize cover = source.scaled(fit, Qt::KeepAspectRatioByExpanding);
|
||||
if (cover.width() <= source.width() && cover.height() <= source.height()) {
|
||||
return cover;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
inline QImage cleanImage(const QImage& src) {
|
||||
QImage out(src.size(), QImage::Format_RGB888);
|
||||
if (out.isNull()) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const auto rowBytes =
|
||||
static_cast<size_t>(std::min(src.bytesPerLine(), out.bytesPerLine()));
|
||||
for (int y = 0; y < src.height(); ++y) {
|
||||
std::memcpy(out.scanLine(y), src.constScanLine(y), rowBytes);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
inline QByteArray imageCacheBytes(const QString& path, const QSize& fit) {
|
||||
QImageReader reader(path);
|
||||
reader.setAutoTransform(true);
|
||||
|
||||
const QSize source = reader.size();
|
||||
const QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target != source && !target.isEmpty()) {
|
||||
reader.setScaledSize(target);
|
||||
}
|
||||
|
||||
QImage image = reader.read();
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
|
||||
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
if (!buffer.open(QIODevice::WriteOnly) ||
|
||||
!image.save(&buffer, "JPEG", 90)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
|
||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
if (!process.waitForFinished(15000)) {
|
||||
process.kill();
|
||||
process.waitForFinished(1000);
|
||||
}
|
||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
||||
};
|
||||
|
||||
double duration = 0.0;
|
||||
const auto probe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
src,
|
||||
});
|
||||
if (probe.first == 0) {
|
||||
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
|
||||
}
|
||||
|
||||
QSize source;
|
||||
const auto resProbe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-of",
|
||||
"csv=s=x:p=0",
|
||||
src,
|
||||
});
|
||||
if (resProbe.first == 0) {
|
||||
const auto parts =
|
||||
QString::fromUtf8(resProbe.second).trimmed().split('x');
|
||||
if (parts.size() == 2) {
|
||||
source = QSize(parts[0].toInt(), parts[1].toInt());
|
||||
}
|
||||
}
|
||||
|
||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
||||
: 8;
|
||||
|
||||
QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target.isEmpty()) {
|
||||
target = fit;
|
||||
}
|
||||
|
||||
target.setWidth(target.width() & ~1);
|
||||
target.setHeight(target.height() & ~1);
|
||||
|
||||
const QString scale =
|
||||
QStringLiteral(
|
||||
"scale=%1:%2:force_original_aspect_ratio=decrease,"
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
|
||||
.arg(target.width())
|
||||
.arg(target.height());
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int bestIndex = -1;
|
||||
double bestChroma = -1.0;
|
||||
double bestLuma = -1.0;
|
||||
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
|
||||
const QString seek = duration > 0
|
||||
? QString::number(frac * duration, 'f', 3)
|
||||
: QStringLiteral("0");
|
||||
|
||||
const QString framePath =
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
seek,
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QImage frame(framePath);
|
||||
if (frame.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = frame.convertToFormat(QImage::Format_RGB888);
|
||||
|
||||
double chroma = 0.0;
|
||||
double luma = 0.0;
|
||||
int count = 0;
|
||||
for (int y = 0; y < frame.height(); ++y) {
|
||||
const auto* line = frame.constScanLine(y);
|
||||
for (int x = 0; x < frame.width(); ++x) {
|
||||
const int r = line[x * 3];
|
||||
const int g = line[x * 3 + 1];
|
||||
const int b = line[x * 3 + 2];
|
||||
chroma += std::max({r, g, b}) - std::min({r, g, b});
|
||||
luma += r + g + b;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
const int n = std::max(1, count);
|
||||
chroma /= n;
|
||||
luma /= 3.0 * n;
|
||||
|
||||
if (chroma > bestChroma + 0.5 ||
|
||||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
|
||||
bestChroma = chroma;
|
||||
bestLuma = luma;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex < 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile frame(tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)));
|
||||
if (!frame.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return frame.readAll();
|
||||
}
|
||||
|
||||
inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) {
|
||||
QImage image = QImage::fromData(source, "JPEG");
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (image.width() > maxSide || image.height() > maxSide) {
|
||||
image = image.scaled(
|
||||
maxSide, maxSide, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
|
||||
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
if (!buffer.open(QIODevice::WriteOnly) ||
|
||||
!image.save(&buffer, "JPEG", 85)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ZShell::models
|
||||
Reference in New Issue
Block a user