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:
+19
-8
@@ -27,7 +27,7 @@ Searcher {
|
|||||||
property bool recentlyChanged
|
property bool recentlyChanged
|
||||||
property bool showPreview: false
|
property bool showPreview: false
|
||||||
|
|
||||||
function getCategoryFor(w: FileSystemEntry): string {
|
function getCategoryFor(w: WallpaperEntry): string {
|
||||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||||
if (category.includes("/"))
|
if (category.includes("/"))
|
||||||
category = category.slice(0, category.indexOf("/"));
|
category = category.slice(0, category.indexOf("/"));
|
||||||
@@ -46,12 +46,12 @@ Searcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function thumbnailFor(path: string): string {
|
function thumbnailFor(path: string): string {
|
||||||
if (!isVideoPath(path))
|
if (!path)
|
||||||
return path;
|
return "";
|
||||||
|
|
||||||
for (const entry of root.list) {
|
for (const entry of root.list) {
|
||||||
if (entry.path === path && entry.thumbnailPath)
|
if (entry.path === path)
|
||||||
return entry.thumbnailPath;
|
return entry.thumbnailPath || path;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
@@ -147,12 +147,13 @@ Searcher {
|
|||||||
onTriggered: monitorCrops.writeAdapter()
|
onTriggered: monitorCrops.writeAdapter()
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemModel {
|
WallpaperDb {
|
||||||
id: wallpapers
|
id: wallpapers
|
||||||
|
|
||||||
filter: FileSystemModel.Media
|
dbPath: `${Paths.state}/wallpapers.sqlite`
|
||||||
|
cacheSize: wallpaper.largestBufferSize
|
||||||
|
requireHardwareCodec: true
|
||||||
path: Config.general.wallpaperPath
|
path: Config.general.wallpaperPath
|
||||||
recursive: true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
@@ -171,9 +172,19 @@ Searcher {
|
|||||||
WallpaperService {
|
WallpaperService {
|
||||||
id: wallpaper
|
id: wallpaper
|
||||||
|
|
||||||
|
dbPath: wallpapers.dbPath
|
||||||
path: root.current
|
path: root.current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: wallpapers
|
||||||
|
|
||||||
|
function onCacheStored(path: string): void {
|
||||||
|
if (path === root.current)
|
||||||
|
wallpaper.invalidateCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Variants {
|
Variants {
|
||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
|||||||
@@ -55,15 +55,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
Image {
|
||||||
id: img
|
id: img
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
cache: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
height: root.tileHeight
|
height: root.tileHeight
|
||||||
path: root.modelData ? root.modelData.thumbnailPath : ""
|
source: root.modelData ? root.modelData.thumbnailPath : ""
|
||||||
smooth: root.isCurrent
|
smooth: root.isCurrent
|
||||||
sourceSize.height: root.tileHeight
|
sourceSize.height: root.tileHeight
|
||||||
sourceSize.width: root.tileHeight * (16 / 9)
|
sourceSize.width: root.tileHeight * (16 / 9)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ PageBase {
|
|||||||
|
|
||||||
WallItem {
|
WallItem {
|
||||||
id: wallItem
|
id: wallItem
|
||||||
required property FileSystemEntry modelData
|
required property WallpaperEntry modelData
|
||||||
|
|
||||||
enabled: modelData
|
enabled: modelData
|
||||||
opacity: modelData ? 1 : 0
|
opacity: modelData ? 1 : 0
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
|
|||||||
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
|
||||||
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
|
||||||
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
|
||||||
|
|
||||||
|
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
|
||||||
|
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
|
||||||
|
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
|
||||||
|
|
||||||
|
find_package(CUDAToolkit QUIET)
|
||||||
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
|
||||||
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
|
||||||
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ qml_module(ZShell-internal
|
|||||||
linearindicatormanager.hpp linearindicatormanager.cpp
|
linearindicatormanager.hpp linearindicatormanager.cpp
|
||||||
strokecanvasitem.hpp strokecanvasitem.cpp
|
strokecanvasitem.hpp strokecanvasitem.cpp
|
||||||
strokecanvasrenderer.hpp strokecanvasrenderer.cpp
|
strokecanvasrenderer.hpp strokecanvasrenderer.cpp
|
||||||
|
videoplayer.hpp videoplayer.cpp
|
||||||
cursor.hpp
|
cursor.hpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
|
Qt::Sql
|
||||||
Qt::Core
|
Qt::Core
|
||||||
Qt::Network
|
Qt::Network
|
||||||
Qt::DBus
|
Qt::DBus
|
||||||
@@ -40,4 +42,20 @@ qml_module(ZShell-internal
|
|||||||
PkgConfig::WAYLAND_EGL
|
PkgConfig::WAYLAND_EGL
|
||||||
PkgConfig::GL
|
PkgConfig::GL
|
||||||
PkgConfig::MPV
|
PkgConfig::MPV
|
||||||
|
PkgConfig::AVCODEC
|
||||||
|
PkgConfig::AVFORMAT
|
||||||
|
PkgConfig::AVUTIL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
qt_add_resources(ZShell-internal "wallpaper_shaders"
|
||||||
|
PREFIX "/"
|
||||||
|
FILES
|
||||||
|
shaders/wallpaper.vert
|
||||||
|
shaders/wallpaper.frag
|
||||||
|
shaders/wallpaper_video.frag
|
||||||
|
)
|
||||||
|
|
||||||
|
if(CUDAToolkit_FOUND)
|
||||||
|
target_link_libraries(ZShell-internal PRIVATE CUDA::cuda_driver)
|
||||||
|
target_compile_definitions(ZShell-internal PRIVATE ZHAVE_VIDEO=1)
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
uniform sampler2D source;
|
||||||
|
uniform vec2 size;
|
||||||
|
uniform vec4 crop;
|
||||||
|
uniform float alpha;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
||||||
|
vec4 c = texture(source, crop.xy + p * crop.zw);
|
||||||
|
outColor = vec4(c.rgb, c.a * alpha);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
void main() {
|
||||||
|
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||||
|
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
uniform sampler2D luma;
|
||||||
|
uniform sampler2D chroma;
|
||||||
|
uniform vec2 size;
|
||||||
|
uniform vec2 srcSize;
|
||||||
|
uniform vec4 crop;
|
||||||
|
uniform float alpha;
|
||||||
|
uniform int matrix;
|
||||||
|
uniform int range;
|
||||||
|
uniform vec2 chromaOff;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
float noise(vec2 p) {
|
||||||
|
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float PI = 3.1415926535893;
|
||||||
|
|
||||||
|
float sinc(float x) {
|
||||||
|
x *= PI;
|
||||||
|
return abs(x) < 1e-6 ? 1.0 : sin(x) / x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float lanczos(float x) {
|
||||||
|
if (abs(x) >= 3.0)
|
||||||
|
return 0.0;
|
||||||
|
return sinc(x) * sinc(x / 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hermite(float x) {
|
||||||
|
float a = abs(x);
|
||||||
|
if (a >= 1.0)
|
||||||
|
return 0.0;
|
||||||
|
return 1.0 - 3.0 * a * a + 2.0 * a * a * a;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 resample(sampler2D tex, vec2 plane, vec2 dest, vec2 uv) {
|
||||||
|
vec2 pt = 1.0 / plane;
|
||||||
|
float wx[6];
|
||||||
|
float wy[6];
|
||||||
|
vec2 base;
|
||||||
|
|
||||||
|
int upx = dest.x >= plane.x ? 1 : 0;
|
||||||
|
int tx = upx == 1 ? 6 : 2;
|
||||||
|
float fsx = max(1.0, plane.x / dest.x);
|
||||||
|
float f = fract(uv.x * plane.x - 0.5);
|
||||||
|
base.x = uv.x - f * pt.x - pt.x * (float(tx) * 0.5 - 1.0);
|
||||||
|
float sx = 0.0;
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
if (i >= tx) { wx[i] = 0.0; continue; }
|
||||||
|
float x = f - (float(i) - float(tx) * 0.5 + 1.0);
|
||||||
|
wx[i] = upx == 1 ? lanczos(x / fsx) : hermite(x / fsx);
|
||||||
|
sx += wx[i];
|
||||||
|
}
|
||||||
|
if (sx != 0.0)
|
||||||
|
for (int i = 0; i < 6; ++i) wx[i] /= sx;
|
||||||
|
|
||||||
|
int upy = dest.y >= plane.y ? 1 : 0;
|
||||||
|
int ty = upy == 1 ? 6 : 2;
|
||||||
|
float fsy = max(1.0, plane.y / dest.y);
|
||||||
|
f = fract(uv.y * plane.y - 0.5);
|
||||||
|
base.y = uv.y - f * pt.y - pt.y * (float(ty) * 0.5 - 1.0);
|
||||||
|
float sy = 0.0;
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
if (i >= ty) { wy[i] = 0.0; continue; }
|
||||||
|
float y = f - (float(i) - float(ty) * 0.5 + 1.0);
|
||||||
|
wy[i] = upy == 1 ? lanczos(y / fsy) : hermite(y / fsy);
|
||||||
|
sy += wy[i];
|
||||||
|
}
|
||||||
|
if (sy != 0.0)
|
||||||
|
for (int i = 0; i < 6; ++i) wy[i] /= sy;
|
||||||
|
|
||||||
|
vec4 col = vec4(0.0);
|
||||||
|
for (int i = 0; i < 6; ++i) {
|
||||||
|
if (wx[i] == 0.0)
|
||||||
|
continue;
|
||||||
|
for (int j = 0; j < 6; ++j) {
|
||||||
|
if (wy[j] == 0.0)
|
||||||
|
continue;
|
||||||
|
vec2 c = base + vec2(float(i) * pt.x, float(j) * pt.y);
|
||||||
|
col += (wx[i] * wy[j]) * texture(tex, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
|
||||||
|
vec2 uv = crop.xy + p * crop.zw;
|
||||||
|
float Yv = resample(luma, srcSize, size, uv).r;
|
||||||
|
vec2 ch = resample(chroma, srcSize * 0.5, size, uv + chromaOff).rg;
|
||||||
|
vec3 yuv;
|
||||||
|
if (range == 0) {
|
||||||
|
yuv = vec3((Yv - 16.0 / 255.0) / (219.0 / 255.0),
|
||||||
|
(ch.r - 128.0 / 255.0) / (224.0 / 255.0),
|
||||||
|
(ch.g - 128.0 / 255.0) / (224.0 / 255.0));
|
||||||
|
} else {
|
||||||
|
yuv = vec3(Yv, ch.r - 0.5, ch.g - 0.5);
|
||||||
|
}
|
||||||
|
mat3 m = matrix == 1
|
||||||
|
? mat3(1.0, 1.0, 1.0, 0.0, -0.187324, 1.855600, 1.574800, -0.468124, 0.0)
|
||||||
|
: mat3(1.0, 1.0, 1.0, 0.0, -0.344136, 1.772000, 1.402000, -0.714136, 0.0);
|
||||||
|
vec3 rgb = m * yuv;
|
||||||
|
rgb += (noise(gl_FragCoord.xy) - 0.5) / 255.0;
|
||||||
|
outColor = vec4(clamp(rgb, 0.0, 1.0), alpha);
|
||||||
|
}
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
#include "videoplayer.hpp"
|
||||||
|
|
||||||
|
#ifdef ZHAVE_VIDEO
|
||||||
|
|
||||||
|
#define EGL_EGLEXT_PROTOTYPES
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glext.h>
|
||||||
|
#include <cuda.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <climits>
|
||||||
|
|
||||||
|
#include <QtLogging>
|
||||||
|
|
||||||
|
extern "C" CUresult cuGraphicsGLRegisterImage(
|
||||||
|
CUgraphicsResource* resource,
|
||||||
|
GLuint image,
|
||||||
|
GLenum target,
|
||||||
|
unsigned int flags);
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(videoLog, "ZShell.Internal.wallpaper")
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum AVPixelFormat chooseFormat(
|
||||||
|
AVCodecContext* ctx, const AVPixelFormat* pix_fmts) {
|
||||||
|
(void)ctx;
|
||||||
|
for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
|
||||||
|
if (*p == AV_PIX_FMT_CUDA) return AV_PIX_FMT_CUDA;
|
||||||
|
return pix_fmts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
VideoPlayer::VideoPlayer(EGLDisplay display) : m_eglDisplay(display) {}
|
||||||
|
|
||||||
|
VideoPlayer::~VideoPlayer() {
|
||||||
|
stop();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
if (m_pending) {
|
||||||
|
av_frame_unref(m_pending);
|
||||||
|
av_frame_free(&m_pending);
|
||||||
|
}
|
||||||
|
if (m_displayed) {
|
||||||
|
av_frame_unref(m_displayed);
|
||||||
|
av_frame_free(&m_displayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_cuCtx) {
|
||||||
|
CUcontext prev = nullptr;
|
||||||
|
cuCtxPushCurrent(m_cuCtx);
|
||||||
|
if (m_resY) {
|
||||||
|
cuGraphicsUnregisterResource(m_resY);
|
||||||
|
m_resY = nullptr;
|
||||||
|
}
|
||||||
|
if (m_resUV) {
|
||||||
|
cuGraphicsUnregisterResource(m_resUV);
|
||||||
|
m_resUV = nullptr;
|
||||||
|
}
|
||||||
|
if (m_texY) {
|
||||||
|
glDeleteTextures(1, &m_texY);
|
||||||
|
m_texY = 0;
|
||||||
|
}
|
||||||
|
if (m_texUV) {
|
||||||
|
glDeleteTextures(1, &m_texUV);
|
||||||
|
m_texUV = 0;
|
||||||
|
}
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
cuCtxDestroy(m_cuCtx);
|
||||||
|
m_cuCtx = nullptr;
|
||||||
|
}
|
||||||
|
m_texSize = QSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayer::setReadyCallback(std::function<void()> callback) {
|
||||||
|
m_readyCb = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayer::setFrameCallback(std::function<void()> callback) {
|
||||||
|
m_frameCb = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoPlayer::openDecoder(const QString& path) {
|
||||||
|
if (avformat_open_input(
|
||||||
|
&m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) {
|
||||||
|
qCWarning(
|
||||||
|
videoLog,
|
||||||
|
"VideoPlayer: could not open %s",
|
||||||
|
path.toUtf8().constData());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (avformat_find_stream_info(m_fmt, nullptr) < 0) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: could not read stream info");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_streamIndex = -1;
|
||||||
|
for (unsigned i = 0; i < m_fmt->nb_streams; ++i)
|
||||||
|
if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||||
|
m_streamIndex = int(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (m_streamIndex < 0) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: no video stream");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar;
|
||||||
|
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
|
||||||
|
const AVCodec* hwcodec = nullptr;
|
||||||
|
void* it = nullptr;
|
||||||
|
while (const AVCodec* c = av_codec_iterate(&it)) {
|
||||||
|
if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue;
|
||||||
|
for (int i = 0;
|
||||||
|
const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i);
|
||||||
|
++i)
|
||||||
|
if (cfg->device_type == AV_HWDEVICE_TYPE_CUDA &&
|
||||||
|
(cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||||
|
hwcodec = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (hwcodec) break;
|
||||||
|
}
|
||||||
|
if (hwcodec) codec = hwcodec;
|
||||||
|
if (!codec) {
|
||||||
|
qCWarning(
|
||||||
|
videoLog,
|
||||||
|
"VideoPlayer: no decoder for codec id %d",
|
||||||
|
int(par->codec_id));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_codec = avcodec_alloc_context3(codec);
|
||||||
|
if (!m_codec) return false;
|
||||||
|
if (avcodec_parameters_to_context(m_codec, par) < 0) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: could not copy codec params");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hwDeviceCtx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
|
||||||
|
if (!m_hwDeviceCtx) {
|
||||||
|
qCWarning(
|
||||||
|
videoLog, "VideoPlayer: could not allocate cuda hwdevice context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AVCUDADeviceContext* cudaHw = reinterpret_cast<AVCUDADeviceContext*>(
|
||||||
|
reinterpret_cast<AVHWDeviceContext*>(m_hwDeviceCtx->data)->hwctx);
|
||||||
|
if (m_cuCtx) cudaHw->cuda_ctx = m_cuCtx;
|
||||||
|
if (av_hwdevice_ctx_init(m_hwDeviceCtx) < 0) {
|
||||||
|
qCWarning(
|
||||||
|
videoLog, "VideoPlayer: could not create cuda hwdevice context");
|
||||||
|
av_buffer_unref(&m_hwDeviceCtx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
|
||||||
|
m_codec->get_format = chooseFormat;
|
||||||
|
|
||||||
|
if (avcodec_open2(m_codec, codec, nullptr) < 0) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: could not open codec");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayer::closeDecoder() {
|
||||||
|
if (m_codec) {
|
||||||
|
avcodec_free_context(&m_codec);
|
||||||
|
m_codec = nullptr;
|
||||||
|
}
|
||||||
|
if (m_hwDeviceCtx) {
|
||||||
|
av_buffer_unref(&m_hwDeviceCtx);
|
||||||
|
m_hwDeviceCtx = nullptr;
|
||||||
|
}
|
||||||
|
if (m_fmt) {
|
||||||
|
avformat_close_input(&m_fmt);
|
||||||
|
m_fmt = nullptr;
|
||||||
|
}
|
||||||
|
m_streamIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayer::decodeLoop(const QString& path) {
|
||||||
|
if (!openDecoder(path)) {
|
||||||
|
m_threadRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate;
|
||||||
|
if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate;
|
||||||
|
if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1};
|
||||||
|
const int64_t intervalUs =
|
||||||
|
av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000});
|
||||||
|
|
||||||
|
AVFrame* frame = av_frame_alloc();
|
||||||
|
if (!frame) {
|
||||||
|
m_threadRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVPacket pkt{};
|
||||||
|
|
||||||
|
while (m_threadRunning) {
|
||||||
|
int ret = av_read_frame(m_fmt, &pkt);
|
||||||
|
if (ret == AVERROR_EOF) {
|
||||||
|
if (avformat_seek_file(
|
||||||
|
m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: loop seek failed; stopping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
avcodec_flush_buffers(m_codec);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ret < 0) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: read error; stopping");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (pkt.stream_index != m_streamIndex) {
|
||||||
|
av_packet_unref(&pkt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_send_packet(m_codec, &pkt) < 0) {
|
||||||
|
av_packet_unref(&pkt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
av_packet_unref(&pkt);
|
||||||
|
|
||||||
|
while (m_threadRunning && avcodec_receive_frame(m_codec, frame) == 0) {
|
||||||
|
bool wasEmpty = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
if (!m_pending) m_pending = av_frame_alloc();
|
||||||
|
av_frame_unref(m_pending);
|
||||||
|
if (av_frame_ref(m_pending, frame) >= 0) {
|
||||||
|
wasEmpty = !m_haveFrame;
|
||||||
|
m_haveFrame = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wasEmpty && m_readyCb) m_readyCb();
|
||||||
|
if (m_frameCb) m_frameCb();
|
||||||
|
av_frame_unref(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::microseconds(intervalUs));
|
||||||
|
}
|
||||||
|
|
||||||
|
av_frame_free(&frame);
|
||||||
|
closeDecoder();
|
||||||
|
m_threadRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoPlayer::start(const QString& path) {
|
||||||
|
stop();
|
||||||
|
|
||||||
|
if (!ensureCudaContext()) return false;
|
||||||
|
|
||||||
|
m_threadRunning = true;
|
||||||
|
m_thread = std::thread([this, path]() { decodeLoop(path); });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayer::stop() {
|
||||||
|
m_threadRunning = false;
|
||||||
|
if (m_thread.joinable()) m_thread.join();
|
||||||
|
|
||||||
|
closeDecoder();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
if (m_pending) {
|
||||||
|
av_frame_unref(m_pending);
|
||||||
|
m_pending = nullptr;
|
||||||
|
}
|
||||||
|
if (m_displayed) {
|
||||||
|
av_frame_unref(m_displayed);
|
||||||
|
m_displayed = nullptr;
|
||||||
|
}
|
||||||
|
m_haveFrame = false;
|
||||||
|
m_size = QSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize VideoPlayer::frameSize() const {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
return m_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayer::deleteTexture() {
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
CUcontext prev = nullptr;
|
||||||
|
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
|
||||||
|
if (m_resY) {
|
||||||
|
cuGraphicsUnregisterResource(m_resY);
|
||||||
|
m_resY = nullptr;
|
||||||
|
}
|
||||||
|
if (m_resUV) {
|
||||||
|
cuGraphicsUnregisterResource(m_resUV);
|
||||||
|
m_resUV = nullptr;
|
||||||
|
}
|
||||||
|
if (m_texY) {
|
||||||
|
glDeleteTextures(1, &m_texY);
|
||||||
|
m_texY = 0;
|
||||||
|
}
|
||||||
|
if (m_texUV) {
|
||||||
|
glDeleteTextures(1, &m_texUV);
|
||||||
|
m_texUV = 0;
|
||||||
|
}
|
||||||
|
if (m_cuCtx) cuCtxPopCurrent(&prev);
|
||||||
|
m_texSize = QSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoPlayer::ensureCudaContext() {
|
||||||
|
if (m_cuCtx) return true;
|
||||||
|
|
||||||
|
if (cuInit(0) != CUDA_SUCCESS) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: cuInit failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CUdevice dev = 0;
|
||||||
|
if (cuDeviceGet(&dev, 0) != CUDA_SUCCESS) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: cuDeviceGet failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cuCtxCreate_v4(&m_cuCtx, nullptr, 0, dev) != CUDA_SUCCESS) {
|
||||||
|
m_cuCtx = nullptr;
|
||||||
|
qCWarning(videoLog, "VideoPlayer: cuCtxCreate failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CUcontext popped = nullptr;
|
||||||
|
cuCtxPopCurrent(&popped);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) {
|
||||||
|
if (!ensureCudaContext()) return false;
|
||||||
|
CUcontext prev = nullptr;
|
||||||
|
cuCtxPushCurrent(m_cuCtx);
|
||||||
|
|
||||||
|
const QSize sz = QSize(int(w), int(h));
|
||||||
|
if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) {
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_resY) cuGraphicsUnregisterResource(m_resY);
|
||||||
|
if (m_resUV) cuGraphicsUnregisterResource(m_resUV);
|
||||||
|
m_resY = m_resUV = nullptr;
|
||||||
|
if (m_texY) glDeleteTextures(1, &m_texY);
|
||||||
|
if (m_texUV) glDeleteTextures(1, &m_texUV);
|
||||||
|
m_texY = m_texUV = 0;
|
||||||
|
|
||||||
|
const bool wide = compBytes == 2;
|
||||||
|
auto makeTex =
|
||||||
|
[&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) {
|
||||||
|
glGenTextures(1, &tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
internal,
|
||||||
|
GLint(tw),
|
||||||
|
GLint(th),
|
||||||
|
0,
|
||||||
|
fmt,
|
||||||
|
wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE,
|
||||||
|
nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
};
|
||||||
|
makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h);
|
||||||
|
makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2);
|
||||||
|
|
||||||
|
CUresult r = cuGraphicsGLRegisterImage(
|
||||||
|
&m_resY,
|
||||||
|
m_texY,
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
|
||||||
|
if (r != CUDA_SUCCESS) {
|
||||||
|
const char* es = nullptr;
|
||||||
|
cuGetErrorString(r, &es);
|
||||||
|
qCWarning(
|
||||||
|
videoLog,
|
||||||
|
"VideoPlayer: register luma tex failed (%s)",
|
||||||
|
es ? es : "?");
|
||||||
|
m_texSize = QSize();
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
r = cuGraphicsGLRegisterImage(
|
||||||
|
&m_resUV,
|
||||||
|
m_texUV,
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
|
||||||
|
if (r != CUDA_SUCCESS) {
|
||||||
|
qCWarning(videoLog, "VideoPlayer: register chroma tex failed");
|
||||||
|
cuGraphicsUnregisterResource(m_resY);
|
||||||
|
m_resY = nullptr;
|
||||||
|
m_texSize = QSize();
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_texSize = sz;
|
||||||
|
m_texComp = compBytes;
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoPlayer::importCurrentFrame() {
|
||||||
|
AVFrame* frame = nullptr;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false;
|
||||||
|
frame = m_pending;
|
||||||
|
m_pending = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->format != AV_PIX_FMT_CUDA || !frame->hw_frames_ctx) {
|
||||||
|
static int logged = 0;
|
||||||
|
if (logged++ < 3)
|
||||||
|
qCWarning(
|
||||||
|
videoLog,
|
||||||
|
"VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)",
|
||||||
|
av_get_pix_fmt_name(AVPixelFormat(frame->format)),
|
||||||
|
frame->hw_frames_ctx != nullptr);
|
||||||
|
av_frame_unref(frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVHWFramesContext* hf =
|
||||||
|
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
|
||||||
|
const AVPixelFormat sw = hf->sw_format;
|
||||||
|
const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE ||
|
||||||
|
sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE;
|
||||||
|
const int compBytes = tenBit ? 2 : 1;
|
||||||
|
const size_t compSize = size_t(compBytes);
|
||||||
|
|
||||||
|
const unsigned w = unsigned(frame->width);
|
||||||
|
const unsigned h = unsigned(frame->height);
|
||||||
|
if (w == 0 || h == 0) {
|
||||||
|
av_frame_unref(frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pitchY = size_t(frame->linesize[0]);
|
||||||
|
size_t pitchUV = frame->linesize[1] > 0 ? size_t(frame->linesize[1])
|
||||||
|
: pitchY;
|
||||||
|
if (pitchY == 0) pitchY = size_t(w) * compSize;
|
||||||
|
|
||||||
|
switch (frame->colorspace) {
|
||||||
|
case AVCOL_SPC_BT470BG:
|
||||||
|
case AVCOL_SPC_SMPTE170M:
|
||||||
|
m_matrix = 0;
|
||||||
|
break;
|
||||||
|
case AVCOL_SPC_BT709:
|
||||||
|
m_matrix = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_matrix = h >= 720 ? 1 : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0;
|
||||||
|
m_size = QSize(int(w), int(h));
|
||||||
|
|
||||||
|
if (!ensureTextures(w, h, compBytes)) {
|
||||||
|
av_frame_unref(frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CUcontext prev = nullptr;
|
||||||
|
cuCtxPushCurrent(m_cuCtx);
|
||||||
|
CUgraphicsResource res[2] = {m_resY, m_resUV};
|
||||||
|
CUresult r = cuGraphicsMapResources(2, res, nullptr);
|
||||||
|
if (r != CUDA_SUCCESS) {
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
av_frame_unref(frame);
|
||||||
|
qCWarning(videoLog, "VideoPlayer: cuGraphicsMapResources failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CUarray arrY = nullptr, arrUV = nullptr;
|
||||||
|
cuGraphicsSubResourceGetMappedArray(&arrY, m_resY, 0, 0);
|
||||||
|
cuGraphicsSubResourceGetMappedArray(&arrUV, m_resUV, 0, 0);
|
||||||
|
|
||||||
|
const size_t dstPitch = size_t(w) * compSize;
|
||||||
|
CUDA_MEMCPY2D mc{};
|
||||||
|
mc.srcMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||||
|
mc.dstMemoryType = CU_MEMORYTYPE_ARRAY;
|
||||||
|
mc.srcDevice = CUdeviceptr(frame->data[0]);
|
||||||
|
mc.srcPitch = pitchY;
|
||||||
|
mc.dstArray = arrY;
|
||||||
|
mc.dstPitch = dstPitch;
|
||||||
|
mc.WidthInBytes = std::min(pitchY, dstPitch);
|
||||||
|
mc.Height = h;
|
||||||
|
r = cuMemcpy2D(&mc);
|
||||||
|
if (r == CUDA_SUCCESS) {
|
||||||
|
mc.srcDevice = CUdeviceptr(frame->data[1]);
|
||||||
|
mc.srcPitch = pitchUV;
|
||||||
|
mc.dstArray = arrUV;
|
||||||
|
mc.dstPitch = dstPitch;
|
||||||
|
mc.WidthInBytes = std::min(pitchUV, dstPitch);
|
||||||
|
mc.Height = h / 2;
|
||||||
|
r = cuMemcpy2D(&mc);
|
||||||
|
}
|
||||||
|
|
||||||
|
cuGraphicsUnmapResources(2, res, nullptr);
|
||||||
|
cuCtxPopCurrent(&prev);
|
||||||
|
|
||||||
|
if (r != CUDA_SUCCESS) {
|
||||||
|
av_frame_unref(frame);
|
||||||
|
qCWarning(videoLog, "VideoPlayer: cuMemcpy2D failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(m_mtx);
|
||||||
|
if (m_displayed) av_frame_unref(m_displayed);
|
||||||
|
m_displayed = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EGL/egl.h>
|
||||||
|
|
||||||
|
#include <cuda.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavutil/buffer.h>
|
||||||
|
#include <libavutil/frame.h>
|
||||||
|
#include <libavutil/hwcontext.h>
|
||||||
|
#include <libavutil/hwcontext_cuda.h>
|
||||||
|
#include <libavutil/pixfmt.h>
|
||||||
|
#include <libavutil/pixdesc.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoPlayer {
|
||||||
|
public:
|
||||||
|
explicit VideoPlayer(EGLDisplay display);
|
||||||
|
~VideoPlayer();
|
||||||
|
|
||||||
|
void setReadyCallback(std::function<void()> callback);
|
||||||
|
void setFrameCallback(std::function<void()> callback);
|
||||||
|
|
||||||
|
bool start(const QString& path);
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
|
||||||
|
bool importCurrentFrame();
|
||||||
|
|
||||||
|
void deleteTexture();
|
||||||
|
|
||||||
|
[[nodiscard]] uint32_t textureId() const { return m_texY; }
|
||||||
|
[[nodiscard]] uint32_t chromaTextureId() const { return m_texUV; }
|
||||||
|
[[nodiscard]] int colorMatrix() const { return m_matrix; }
|
||||||
|
[[nodiscard]] int colorRange() const { return m_range; }
|
||||||
|
[[nodiscard]] QSize frameSize() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void decodeLoop(const QString& path);
|
||||||
|
bool openDecoder(const QString& path);
|
||||||
|
void closeDecoder();
|
||||||
|
|
||||||
|
bool ensureTextures(unsigned w, unsigned h, int compBytes);
|
||||||
|
|
||||||
|
bool ensureCudaContext();
|
||||||
|
|
||||||
|
EGLDisplay m_eglDisplay;
|
||||||
|
std::function<void()> m_readyCb;
|
||||||
|
std::function<void()> m_frameCb;
|
||||||
|
|
||||||
|
std::thread m_thread;
|
||||||
|
std::atomic<bool> m_threadRunning{false};
|
||||||
|
|
||||||
|
mutable std::mutex m_mtx;
|
||||||
|
AVFrame* m_pending = nullptr;
|
||||||
|
bool m_haveFrame = false;
|
||||||
|
|
||||||
|
AVFrame* m_displayed = nullptr;
|
||||||
|
|
||||||
|
AVFormatContext* m_fmt = nullptr;
|
||||||
|
AVCodecContext* m_codec = nullptr;
|
||||||
|
AVBufferRef* m_hwDeviceCtx = nullptr;
|
||||||
|
int m_streamIndex = -1;
|
||||||
|
|
||||||
|
CUcontext m_cuCtx = nullptr;
|
||||||
|
CUgraphicsResource m_resY = nullptr;
|
||||||
|
CUgraphicsResource m_resUV = nullptr;
|
||||||
|
QSize m_texSize;
|
||||||
|
|
||||||
|
uint32_t m_texY = 0;
|
||||||
|
uint32_t m_texUV = 0;
|
||||||
|
|
||||||
|
int m_matrix = 1;
|
||||||
|
int m_range = 0;
|
||||||
|
int m_texComp = 1;
|
||||||
|
QSize m_size;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,11 @@
|
|||||||
#include <QtQml/qqml.h>
|
#include <QtQml/qqml.h>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#ifdef ZHAVE_VIDEO
|
||||||
|
#include "videoplayer.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
#include <qwayland-wlr-layer-shell-unstable-v1.h>
|
||||||
#include <wayland-client-protocol.h>
|
#include <wayland-client-protocol.h>
|
||||||
@@ -39,6 +44,10 @@ class WallpaperService : public QObject {
|
|||||||
|
|
||||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
|
||||||
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
|
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
|
||||||
|
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QSize largestBufferSize READ largestBufferSize NOTIFY
|
||||||
|
largestBufferSizeChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit WallpaperService(QObject* parent = nullptr);
|
explicit WallpaperService(QObject* parent = nullptr);
|
||||||
@@ -47,6 +56,11 @@ class WallpaperService : public QObject {
|
|||||||
[[nodiscard]] QString path() const;
|
[[nodiscard]] QString path() const;
|
||||||
void setPath(const QString& path);
|
void setPath(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] QSize largestBufferSize() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QString dbPath() const;
|
||||||
|
void setDbPath(const QString& path);
|
||||||
|
|
||||||
void addScreen(const QString& screen);
|
void addScreen(const QString& screen);
|
||||||
void removeScreen(const QString& screen);
|
void removeScreen(const QString& screen);
|
||||||
|
|
||||||
@@ -57,9 +71,13 @@ class WallpaperService : public QObject {
|
|||||||
|
|
||||||
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
|
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
|
||||||
|
|
||||||
|
Q_INVOKABLE void invalidateCache();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void pathChanged();
|
void pathChanged();
|
||||||
void cropsChanged();
|
void cropsChanged();
|
||||||
|
void dbPathChanged();
|
||||||
|
void largestBufferSizeChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class WallpaperSurface;
|
friend class WallpaperSurface;
|
||||||
@@ -77,7 +95,6 @@ class WallpaperService : public QObject {
|
|||||||
void applyPath(const QString& path);
|
void applyPath(const QString& path);
|
||||||
|
|
||||||
void startVideo(const QString& path);
|
void startVideo(const QString& path);
|
||||||
void stopVideo();
|
|
||||||
void teardownMpv();
|
void teardownMpv();
|
||||||
|
|
||||||
void initMpvRender();
|
void initMpvRender();
|
||||||
@@ -87,6 +104,23 @@ class WallpaperService : public QObject {
|
|||||||
static void* getMpvProcAddress(void* ctx, const char* name);
|
static void* getMpvProcAddress(void* ctx, const char* name);
|
||||||
static void mpvUpdate(void* ctx);
|
static void mpvUpdate(void* ctx);
|
||||||
|
|
||||||
|
#ifdef ZHAVE_VIDEO
|
||||||
|
bool startPlayerVideo(const QString& path);
|
||||||
|
void stopPlayerVideo();
|
||||||
|
[[nodiscard]] uint32_t createVideoProgram();
|
||||||
|
void drawVideoTexture(
|
||||||
|
uint32_t lumaTex,
|
||||||
|
uint32_t chromaTex,
|
||||||
|
int matrix,
|
||||||
|
int range,
|
||||||
|
const QRectF& rect,
|
||||||
|
float sizeW,
|
||||||
|
float sizeH,
|
||||||
|
float srcW,
|
||||||
|
float srcH,
|
||||||
|
float alpha);
|
||||||
|
#endif
|
||||||
|
|
||||||
void bindGlobals();
|
void bindGlobals();
|
||||||
static void registryGlobal(
|
static void registryGlobal(
|
||||||
void* data,
|
void* data,
|
||||||
@@ -111,16 +145,18 @@ class WallpaperService : public QObject {
|
|||||||
float sizeH,
|
float sizeH,
|
||||||
float alpha);
|
float alpha);
|
||||||
|
|
||||||
bool decode(const QString& path);
|
[[nodiscard]] QImage decodeImage(
|
||||||
|
const QString& path, const QSize& decodeSize) const;
|
||||||
|
|
||||||
bool ensureDecodeSize();
|
[[nodiscard]] QByteArray cachedBlob(const QString& path) const;
|
||||||
|
|
||||||
[[nodiscard]] QSize largestBufferSize();
|
void ensureImage();
|
||||||
|
|
||||||
|
void notifyBufferSizes();
|
||||||
|
|
||||||
void queue(WallpaperSurface* surface);
|
void queue(WallpaperSurface* surface);
|
||||||
void queueAll();
|
void queueAll();
|
||||||
|
|
||||||
void startDrawing();
|
|
||||||
void loop();
|
void loop();
|
||||||
|
|
||||||
void present();
|
void present();
|
||||||
@@ -143,6 +179,7 @@ class WallpaperService : public QObject {
|
|||||||
void addSurface(const QString& screen);
|
void addSurface(const QString& screen);
|
||||||
|
|
||||||
QString m_path;
|
QString m_path;
|
||||||
|
QString m_dbPath;
|
||||||
QStringList m_screens;
|
QStringList m_screens;
|
||||||
QVariantMap m_crops;
|
QVariantMap m_crops;
|
||||||
|
|
||||||
@@ -160,9 +197,15 @@ class WallpaperService : public QObject {
|
|||||||
|
|
||||||
QProcess* m_transcode = nullptr;
|
QProcess* m_transcode = nullptr;
|
||||||
|
|
||||||
QSize m_sourceSize;
|
|
||||||
QSize m_decodeSize;
|
QSize m_decodeSize;
|
||||||
|
|
||||||
|
QImage m_stagedImage;
|
||||||
|
QString m_stagedPath;
|
||||||
|
bool m_stagedReady = false;
|
||||||
|
bool m_decoding = false;
|
||||||
|
QString m_decodePath;
|
||||||
|
bool m_replacementReady = false;
|
||||||
|
|
||||||
wl_display* m_display = nullptr;
|
wl_display* m_display = nullptr;
|
||||||
wl_registry* m_registry = nullptr;
|
wl_registry* m_registry = nullptr;
|
||||||
wl_compositor* m_compositor = nullptr;
|
wl_compositor* m_compositor = nullptr;
|
||||||
@@ -178,10 +221,29 @@ class WallpaperService : public QObject {
|
|||||||
int m_sizeUniform = -1;
|
int m_sizeUniform = -1;
|
||||||
int m_cropUniform = -1;
|
int m_cropUniform = -1;
|
||||||
int m_alphaUniform = -1;
|
int m_alphaUniform = -1;
|
||||||
|
int m_sourceUniform = -1;
|
||||||
|
|
||||||
|
uint32_t m_videoProgram = 0;
|
||||||
|
uint32_t m_vao = 0;
|
||||||
|
int m_videoSizeUniform = -1;
|
||||||
|
int m_videoSrcSizeUniform = -1;
|
||||||
|
int m_videoCropUniform = -1;
|
||||||
|
int m_videoAlphaUniform = -1;
|
||||||
|
int m_videoLumaUniform = -1;
|
||||||
|
int m_videoChromaUniform = -1;
|
||||||
|
int m_videoMatrixUniform = -1;
|
||||||
|
int m_videoRangeUniform = -1;
|
||||||
|
int m_videoChromaOffUniform = -1;
|
||||||
|
|
||||||
|
#ifdef ZHAVE_VIDEO
|
||||||
|
std::unique_ptr<VideoPlayer> m_player;
|
||||||
|
bool m_usePlayer = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
QImage m_previousImage;
|
QImage m_previousImage;
|
||||||
QVariantMap m_previousCrops;
|
QVariantMap m_previousCrops;
|
||||||
uint32_t m_previousTexture = 0;
|
uint32_t m_previousTexture = 0;
|
||||||
|
std::vector<uint32_t> m_orphanTextures;
|
||||||
bool m_fading = false;
|
bool m_fading = false;
|
||||||
bool m_fadeClockStarted = false;
|
bool m_fadeClockStarted = false;
|
||||||
bool m_firstFadeDone = false;
|
bool m_firstFadeDone = false;
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ void WallpaperSurface::updateBuffer() {
|
|||||||
|
|
||||||
requestFrame();
|
requestFrame();
|
||||||
m_service->queue(this);
|
m_service->queue(this);
|
||||||
|
m_service->notifyBufferSizes();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ZShell::internal
|
} // namespace ZShell::internal
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ qml_module(ZShell-models
|
|||||||
URI ZShell.Models
|
URI ZShell.Models
|
||||||
SOURCES
|
SOURCES
|
||||||
filesystemmodel.hpp filesystemmodel.cpp
|
filesystemmodel.hpp filesystemmodel.cpp
|
||||||
|
wallpaperdb.hpp wallpaperdb.cpp
|
||||||
|
wallpaperutil.hpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
|
Qt::Quick
|
||||||
|
Qt::Qml
|
||||||
Qt::Concurrent
|
Qt::Concurrent
|
||||||
|
Qt::Sql
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <qtconcurrentrun.h>
|
#include <qtconcurrentrun.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace ZShell::models {
|
namespace ZShell::models {
|
||||||
|
|
||||||
@@ -39,136 +40,43 @@ bool hasVideoExtension(const QString& path) {
|
|||||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
QString videoThumbnailPath(const QString& path) {
|
const QStringList kHardwareCodecs = {
|
||||||
const QFileInfo info(path);
|
"av1",
|
||||||
|
"hevc",
|
||||||
QString key = QStringLiteral("%1_%2_%3")
|
"h264",
|
||||||
.arg(info.baseName())
|
"vp9",
|
||||||
.arg(info.size())
|
"vp8",
|
||||||
.arg(info.lastModified().toMSecsSinceEpoch() / 1000);
|
"vc1",
|
||||||
key.replace(QRegularExpression("[^A-Za-z0-9._-]"), "_");
|
"mpeg2",
|
||||||
|
"mpeg2video",
|
||||||
return QDir::homePath() + "/.cache/zshell/imagecache/video-previews/" +
|
"mpeg4",
|
||||||
key + "_preview.jpg";
|
"mjpeg",
|
||||||
}
|
|
||||||
|
|
||||||
bool generateVideoThumbnail(const QString& src, const QString& cachePath) {
|
|
||||||
if (QFile::exists(cachePath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString scale = QStringLiteral(
|
|
||||||
"scale=1920:1080:force_original_aspect_ratio=decrease,"
|
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2");
|
|
||||||
|
|
||||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
|
||||||
QProcess process;
|
|
||||||
process.start(program, args);
|
|
||||||
process.waitForFinished(15000);
|
|
||||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
double duration = 0.0;
|
bool probeHardwareCodec(const QString& path) {
|
||||||
const auto probe = runProcess(
|
QProcess process;
|
||||||
|
process.start(
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
{
|
{
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
"-show_entries",
|
"-show_entries",
|
||||||
"format=duration",
|
"stream=codec_name",
|
||||||
"-of",
|
"-of",
|
||||||
"default=nw=1:nk=1",
|
"default=nw=1:nk=1",
|
||||||
src,
|
path,
|
||||||
});
|
});
|
||||||
if (probe.first == 0) {
|
process.waitForFinished(5000);
|
||||||
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
if (process.exitCode() != 0) {
|
||||||
: 8;
|
|
||||||
|
|
||||||
QTemporaryDir tmpDir;
|
|
||||||
if (!tmpDir.isValid()) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int bestIndex = -1;
|
const QString codec =
|
||||||
double bestChroma = -1.0;
|
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||||
double bestLuma = -1.0;
|
return kHardwareCodecs.contains(codec);
|
||||||
|
|
||||||
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 false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir().mkpath(QFileInfo(cachePath).absolutePath());
|
|
||||||
return QFile::copy(
|
|
||||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)),
|
|
||||||
cachePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemEntry::FileSystemEntry(
|
FileSystemEntry::FileSystemEntry(
|
||||||
@@ -177,7 +85,6 @@ FileSystemEntry::FileSystemEntry(
|
|||||||
, m_fileInfo(path)
|
, m_fileInfo(path)
|
||||||
, m_path(path)
|
, m_path(path)
|
||||||
, m_relativePath(relativePath)
|
, m_relativePath(relativePath)
|
||||||
, m_thumbnailPath(path)
|
|
||||||
, m_isImageInitialised(false)
|
, m_isImageInitialised(false)
|
||||||
, m_mimeTypeInitialised(false) {}
|
, m_mimeTypeInitialised(false) {}
|
||||||
|
|
||||||
@@ -227,16 +134,7 @@ bool FileSystemEntry::isVideo() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString FileSystemEntry::thumbnailPath() const {
|
QString FileSystemEntry::thumbnailPath() const {
|
||||||
return m_thumbnailPath;
|
return m_path;
|
||||||
}
|
|
||||||
|
|
||||||
void FileSystemEntry::setThumbnailPath(const QString& path) {
|
|
||||||
if (m_thumbnailPath == path) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_thumbnailPath = path;
|
|
||||||
emit thumbnailPathChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString FileSystemEntry::mimeType() const {
|
QString FileSystemEntry::mimeType() const {
|
||||||
@@ -404,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileSystemModel::requireHardwareCodec() const {
|
||||||
|
return m_requireHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||||
|
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_requireHardwareCodec = requireHardwareCodec;
|
||||||
|
emit requireHardwareCodecChanged();
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileSystemModel::isHardwareVideo(const QString& path) {
|
||||||
|
const QFileInfo info(path);
|
||||||
|
const QString key =
|
||||||
|
path + '|' + QByteArray::number(info.size()) + '|' +
|
||||||
|
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||||
|
|
||||||
|
const auto it = m_codecCache.constFind(key);
|
||||||
|
if (it != m_codecCache.constEnd()) {
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool hardware = probeHardwareCodec(path);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||||
|
m_codecCache.insert(key, hardware);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hardware;
|
||||||
|
}
|
||||||
|
|
||||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||||
}
|
}
|
||||||
@@ -434,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
|||||||
const auto paths = watcher->result();
|
const auto paths = watcher->result();
|
||||||
if (currentDir == m_dir && showHidden == m_showHidden &&
|
if (currentDir == m_dir && showHidden == m_showHidden &&
|
||||||
!paths.isEmpty()) {
|
!paths.isEmpty()) {
|
||||||
// Ignore if dir or showHidden has changed
|
|
||||||
m_watcher.addPaths(paths);
|
m_watcher.addPaths(paths);
|
||||||
}
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
});
|
});
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
}
|
}
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::update() {
|
void FileSystemModel::update() {
|
||||||
@@ -488,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
const auto showHidden = m_showHidden;
|
const auto showHidden = m_showHidden;
|
||||||
const auto filter = m_filter;
|
const auto filter = m_filter;
|
||||||
const auto nameFilters = m_nameFilters;
|
const auto nameFilters = m_nameFilters;
|
||||||
|
const auto requireHardware = m_requireHardwareCodec;
|
||||||
|
|
||||||
QSet<QString> oldPaths;
|
QSet<QString> oldPaths;
|
||||||
for (const auto& entry : std::as_const(m_entries)) {
|
for (const auto& entry : std::as_const(m_entries)) {
|
||||||
@@ -495,7 +432,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto future = QtConcurrent::run(
|
const auto future = QtConcurrent::run(
|
||||||
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||||
const auto flags = recursive ? QDirIterator::Subdirectories
|
const auto flags = recursive ? QDirIterator::Subdirectories
|
||||||
: QDirIterator::NoIteratorFlags;
|
: QDirIterator::NoIteratorFlags;
|
||||||
|
|
||||||
@@ -553,18 +490,15 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
|
|
||||||
QString path = iter->next();
|
QString path = iter->next();
|
||||||
|
|
||||||
if (filter == Images) {
|
if (hasVideoExtension(path)) {
|
||||||
|
if (requireHardware && !isHardwareVideo(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (filter == Images || filter == Media) {
|
||||||
QImageReader reader(path);
|
QImageReader reader(path);
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (filter == Media) {
|
|
||||||
if (!hasVideoExtension(path)) {
|
|
||||||
QImageReader reader(path);
|
|
||||||
if (!reader.canRead()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newPaths.insert(path);
|
newPaths.insert(path);
|
||||||
@@ -605,31 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watcher->setFuture(future);
|
watcher->setFuture(future);
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileSystemModel::startThumbnailGeneration(FileSystemEntry* entry) {
|
|
||||||
const QPointer<FileSystemEntry> guard(entry);
|
|
||||||
const QString source = entry->path();
|
|
||||||
const QString cachePath = videoThumbnailPath(source);
|
|
||||||
|
|
||||||
const auto future = QtConcurrent::run([source, cachePath]() {
|
|
||||||
return generateVideoThumbnail(source, cachePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
const auto watcher = new QFutureWatcher<bool>(this);
|
|
||||||
connect(
|
|
||||||
watcher,
|
|
||||||
&QFutureWatcher<bool>::finished,
|
|
||||||
this,
|
|
||||||
[guard, cachePath, watcher]() {
|
|
||||||
if (guard && watcher->result()) {
|
|
||||||
guard->setThumbnailPath(cachePath);
|
|
||||||
}
|
|
||||||
watcher->deleteLater();
|
|
||||||
});
|
|
||||||
watcher->setFuture(future);
|
|
||||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemModel::applyChanges(
|
void FileSystemModel::applyChanges(
|
||||||
@@ -642,7 +551,6 @@ void FileSystemModel::applyChanges(
|
|||||||
}
|
}
|
||||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||||
|
|
||||||
// Batch remove old entries
|
|
||||||
int start = -1;
|
int start = -1;
|
||||||
int end = -1;
|
int end = -1;
|
||||||
for (int idx : std::as_const(removedIndices)) {
|
for (int idx : std::as_const(removedIndices)) {
|
||||||
@@ -670,14 +578,10 @@ void FileSystemModel::applyChanges(
|
|||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new entries
|
|
||||||
QList<FileSystemEntry*> newEntries;
|
QList<FileSystemEntry*> newEntries;
|
||||||
for (const auto& path : addedPaths) {
|
for (const auto& path : addedPaths) {
|
||||||
auto* entry =
|
auto* entry =
|
||||||
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||||
if (entry->isVideo()) {
|
|
||||||
startThumbnailGeneration(entry);
|
|
||||||
}
|
|
||||||
newEntries << entry;
|
newEntries << entry;
|
||||||
}
|
}
|
||||||
std::sort(
|
std::sort(
|
||||||
@@ -687,7 +591,6 @@ void FileSystemModel::applyChanges(
|
|||||||
return compareEntries(a, b);
|
return compareEntries(a, b);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch insert new entries
|
|
||||||
int insertStart = -1;
|
int insertStart = -1;
|
||||||
QList<FileSystemEntry*> batchItems;
|
QList<FileSystemEntry*> batchItems;
|
||||||
for (const auto& entry : std::as_const(newEntries)) {
|
for (const auto& entry : std::as_const(newEntries)) {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include <qdir.h>
|
#include <qdir.h>
|
||||||
#include <qfilesystemwatcher.h>
|
#include <qfilesystemwatcher.h>
|
||||||
#include <qfuture.h>
|
#include <qfuture.h>
|
||||||
|
#include <qhash.h>
|
||||||
#include <qimagereader.h>
|
#include <qimagereader.h>
|
||||||
|
#include <qmutex.h>
|
||||||
#include <qmimedatabase.h>
|
#include <qmimedatabase.h>
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
#include <qqmlintegration.h>
|
#include <qqmlintegration.h>
|
||||||
@@ -29,8 +31,7 @@ class FileSystemEntry : public QObject {
|
|||||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(QString thumbnailPath READ thumbnailPath CONSTANT)
|
||||||
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
|
||||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -52,20 +53,16 @@ class FileSystemEntry : public QObject {
|
|||||||
[[nodiscard]] QString thumbnailPath() const;
|
[[nodiscard]] QString thumbnailPath() const;
|
||||||
[[nodiscard]] QString mimeType() const;
|
[[nodiscard]] QString mimeType() const;
|
||||||
|
|
||||||
void setThumbnailPath(const QString& path);
|
|
||||||
|
|
||||||
void updateRelativePath(const QDir& dir);
|
void updateRelativePath(const QDir& dir);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void relativePathChanged();
|
void relativePathChanged();
|
||||||
void thumbnailPathChanged();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const QFileInfo m_fileInfo;
|
const QFileInfo m_fileInfo;
|
||||||
|
|
||||||
const QString m_path;
|
const QString m_path;
|
||||||
QString m_relativePath;
|
QString m_relativePath;
|
||||||
QString m_thumbnailPath;
|
|
||||||
|
|
||||||
mutable bool m_isImage;
|
mutable bool m_isImage;
|
||||||
mutable bool m_isImageInitialised;
|
mutable bool m_isImageInitialised;
|
||||||
@@ -94,6 +91,9 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
||||||
nameFiltersChanged)
|
nameFiltersChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||||
|
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||||
|
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
||||||
@@ -132,6 +132,9 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
[[nodiscard]] QStringList nameFilters() const;
|
[[nodiscard]] QStringList nameFilters() const;
|
||||||
void setNameFilters(const QStringList& nameFilters);
|
void setNameFilters(const QStringList& nameFilters);
|
||||||
|
|
||||||
|
[[nodiscard]] bool requireHardwareCodec() const;
|
||||||
|
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@@ -142,6 +145,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void sortReverseChanged();
|
void sortReverseChanged();
|
||||||
void filterChanged();
|
void filterChanged();
|
||||||
void nameFiltersChanged();
|
void nameFiltersChanged();
|
||||||
|
void requireHardwareCodecChanged();
|
||||||
void entriesChanged();
|
void entriesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -157,6 +161,10 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
bool m_sortReverse;
|
bool m_sortReverse;
|
||||||
Filter m_filter;
|
Filter m_filter;
|
||||||
QStringList m_nameFilters;
|
QStringList m_nameFilters;
|
||||||
|
bool m_requireHardwareCodec = false;
|
||||||
|
|
||||||
|
QHash<QString, bool> m_codecCache;
|
||||||
|
QMutex m_codecMutex;
|
||||||
|
|
||||||
void watchDirIfRecursive(const QString& path);
|
void watchDirIfRecursive(const QString& path);
|
||||||
void update();
|
void update();
|
||||||
@@ -165,7 +173,7 @@ class FileSystemModel : public QAbstractListModel {
|
|||||||
void updateEntriesForDir(const QString& dir);
|
void updateEntriesForDir(const QString& dir);
|
||||||
void applyChanges(
|
void applyChanges(
|
||||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||||
void startThumbnailGeneration(FileSystemEntry* entry);
|
[[nodiscard]] bool isHardwareVideo(const QString& path);
|
||||||
[[nodiscard]] bool compareEntries(
|
[[nodiscard]] bool compareEntries(
|
||||||
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,746 @@
|
|||||||
|
#include "wallpaperdb.hpp"
|
||||||
|
#include "wallpaperutil.hpp"
|
||||||
|
|
||||||
|
#include <QtQuick/qquickimageprovider.h>
|
||||||
|
#include <qbytearray.h>
|
||||||
|
#include <qdiriterator.h>
|
||||||
|
#include <qelapsedtimer.h>
|
||||||
|
#include <qfuturewatcher.h>
|
||||||
|
#include <qimage.h>
|
||||||
|
#include <qimagereader.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qsqldatabase.h>
|
||||||
|
#include <qsqlerror.h>
|
||||||
|
#include <qsqlquery.h>
|
||||||
|
#include <qtconcurrentrun.h>
|
||||||
|
#include <quuid.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(dbLog, "ZShell.Internal.wallpaper.db", QtWarningMsg)
|
||||||
|
|
||||||
|
namespace ZShell::models {
|
||||||
|
|
||||||
|
class WallpaperImageProvider : public QQuickImageProvider {
|
||||||
|
public:
|
||||||
|
explicit WallpaperImageProvider(const QString& dbPath)
|
||||||
|
: QQuickImageProvider(QQuickImageProvider::Image), m_dbPath(dbPath) {}
|
||||||
|
|
||||||
|
QImage requestImage(
|
||||||
|
const QString& id, QSize* size, const QSize& requestedSize) override {
|
||||||
|
const QString path =
|
||||||
|
QString::fromUtf8(QByteArray::fromHex(id.toUtf8()));
|
||||||
|
if (path.isEmpty() || !openThreadDb()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(QSqlDatabase::database(m_threadUuid));
|
||||||
|
query.prepare("SELECT cache FROM wallpapers WHERE path = ?");
|
||||||
|
query.addBindValue(path);
|
||||||
|
|
||||||
|
if (!query.exec() || !query.next()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray bytes = query.value(0).toByteArray();
|
||||||
|
if (bytes.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image = QImage::fromData(bytes, "JPEG");
|
||||||
|
if (image.isNull()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size) {
|
||||||
|
*size = image.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedSize.isValid() && !requestedSize.isEmpty() &&
|
||||||
|
(image.width() > requestedSize.width() ||
|
||||||
|
image.height() > requestedSize.height())) {
|
||||||
|
image = image.scaled(
|
||||||
|
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool openThreadDb() {
|
||||||
|
if (!m_threadUuid.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_dbPath.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
|
||||||
|
auto db = QSqlDatabase::addDatabase("QSQLITE", uuid);
|
||||||
|
db.setConnectOptions("QSQLITE_OPEN_READONLY");
|
||||||
|
db.setDatabaseName(m_dbPath);
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec("PRAGMA journal_mode=WAL");
|
||||||
|
|
||||||
|
m_threadUuid = uuid;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString m_dbPath;
|
||||||
|
thread_local static QString m_threadUuid;
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local QString WallpaperImageProvider::m_threadUuid;
|
||||||
|
|
||||||
|
WallpaperEntry::WallpaperEntry(
|
||||||
|
const QString& path, const QString& relativePath, QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_fileInfo(path)
|
||||||
|
, m_path(path)
|
||||||
|
, m_relativePath(relativePath) {}
|
||||||
|
|
||||||
|
QString WallpaperEntry::path() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::relativePath() const {
|
||||||
|
return m_relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::name() const {
|
||||||
|
return m_fileInfo.fileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::baseName() const {
|
||||||
|
return m_fileInfo.baseName();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::parentDir() const {
|
||||||
|
return m_fileInfo.absolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::suffix() const {
|
||||||
|
return m_fileInfo.completeSuffix();
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 WallpaperEntry::size() const {
|
||||||
|
return m_fileInfo.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::isImage() const {
|
||||||
|
return !hasVideoExtension(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::isVideo() const {
|
||||||
|
return hasVideoExtension(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::hardware() const {
|
||||||
|
return m_hardware;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperEntry::cached() const {
|
||||||
|
return !m_thumbnailPath.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperEntry::thumbnailPath() const {
|
||||||
|
if (!m_thumbnailPath.isEmpty()) {
|
||||||
|
return m_thumbnailPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isImage() ? m_path : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperEntry::setRelativePath(const QString& path) {
|
||||||
|
if (m_relativePath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_relativePath = path;
|
||||||
|
emit relativePathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperEntry::setHardware(bool hardware) {
|
||||||
|
if (m_hardware == hardware) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hardware = hardware;
|
||||||
|
emit hardwareChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperEntry::setThumbnailPath(const QString& path) {
|
||||||
|
if (m_thumbnailPath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_thumbnailPath = path;
|
||||||
|
emit thumbnailPathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperDb::WallpaperDb(QObject* parent) : QObject(parent) {
|
||||||
|
m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
m_dbPath = QDir::homePath() + "/.local/state/zshell/wallpapers.sqlite";
|
||||||
|
|
||||||
|
m_queueTimer.setInterval(300);
|
||||||
|
m_queueTimer.setTimerType(Qt::CoarseTimer);
|
||||||
|
QObject::connect(
|
||||||
|
&m_queueTimer, &QTimer::timeout, this, &WallpaperDb::processQueue);
|
||||||
|
|
||||||
|
m_watchTimer.setInterval(400);
|
||||||
|
m_watchTimer.setSingleShot(true);
|
||||||
|
QObject::connect(
|
||||||
|
&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
|
||||||
|
m_watchTimer.start();
|
||||||
|
});
|
||||||
|
QObject::connect(
|
||||||
|
&m_watchTimer, &QTimer::timeout, this, &WallpaperDb::scanDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperDb::~WallpaperDb() {
|
||||||
|
if (QSqlDatabase::contains(m_uuid)) {
|
||||||
|
{
|
||||||
|
auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (db.isValid()) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlDatabase::removeDatabase(m_uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperDb::path() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setPath(const QString& path) {
|
||||||
|
if (m_path == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_path = path;
|
||||||
|
m_dir = QDir(path);
|
||||||
|
emit pathChanged();
|
||||||
|
|
||||||
|
for (const auto& dir : m_watcher.directories()) {
|
||||||
|
m_watcher.removePath(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
scanDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperDb::dbPath() const {
|
||||||
|
return m_dbPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setDbPath(const QString& path) {
|
||||||
|
if (m_dbPath == path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dbPath = path;
|
||||||
|
emit dbPathChanged();
|
||||||
|
|
||||||
|
if (!m_path.isEmpty()) {
|
||||||
|
scanDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperDb::requireHardwareCodec() const {
|
||||||
|
return m_requireHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||||
|
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_requireHardwareCodec = requireHardwareCodec;
|
||||||
|
emit requireHardwareCodecChanged();
|
||||||
|
|
||||||
|
rebuildEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlListProperty<WallpaperEntry> WallpaperDb::entries() {
|
||||||
|
return QQmlListProperty<WallpaperEntry>(this, &m_entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallpaperDb::thumbnailUrl(const QString& path) {
|
||||||
|
return QStringLiteral("image:
|
||||||
|
+ QString::fromUtf8(path.toUtf8().toHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize WallpaperDb::cacheSize() const {
|
||||||
|
return m_cacheSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::setCacheSize(const QSize& cacheSize) {
|
||||||
|
if (m_cacheSize == cacheSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QSize prev = m_cacheSize;
|
||||||
|
const bool hadPrevious = !prev.isEmpty();
|
||||||
|
|
||||||
|
m_cacheSize = cacheSize;
|
||||||
|
emit cacheSizeChanged();
|
||||||
|
|
||||||
|
if (hadPrevious && qint64(cacheSize.width()) * cacheSize.height() >
|
||||||
|
qint64(prev.width()) * prev.height()) {
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
|
||||||
|
for (auto* entry : std::as_const(m_byPath)) {
|
||||||
|
if (!entry->cached()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL"
|
||||||
|
" WHERE path = ?");
|
||||||
|
query.addBindValue(entry->path());
|
||||||
|
query.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->setThumbnailPath(QString());
|
||||||
|
queueMetadata(entry, entry->isVideo());
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_queue.isEmpty() && !m_queueTimer.isActive()) {
|
||||||
|
m_queueTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::registerProvider() {
|
||||||
|
if (m_provider != nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* engine = qmlEngine(this);
|
||||||
|
if (engine == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_provider = new WallpaperImageProvider(m_dbPath);
|
||||||
|
engine->addImageProvider(QStringLiteral("wallpapers"), m_provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::openDb() {
|
||||||
|
if (m_dbPath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QSqlDatabase::contains(m_uuid)) {
|
||||||
|
{
|
||||||
|
auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (db.isOpen() && db.databaseName() == m_dbPath) {
|
||||||
|
registerProvider();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.isValid()) {
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlDatabase::removeDatabase(m_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
||||||
|
|
||||||
|
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
|
||||||
|
db.setDatabaseName(m_dbPath);
|
||||||
|
|
||||||
|
if (!db.open()) {
|
||||||
|
qWarning(dbLog) << "WallpaperDb: could not open database" << m_dbPath
|
||||||
|
<< db.lastError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.exec("PRAGMA journal_mode=WAL");
|
||||||
|
query.exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS wallpapers("
|
||||||
|
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||||
|
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
|
||||||
|
|
||||||
|
query.exec("SELECT cache FROM wallpapers LIMIT 1");
|
||||||
|
if (query.lastError().isValid()) {
|
||||||
|
query.exec("DROP TABLE IF EXISTS wallpapers");
|
||||||
|
query.exec(
|
||||||
|
"CREATE TABLE wallpapers("
|
||||||
|
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||||
|
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
|
||||||
|
} else {
|
||||||
|
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::scanDir() {
|
||||||
|
openDb();
|
||||||
|
|
||||||
|
watchDir(m_dir.absolutePath());
|
||||||
|
|
||||||
|
if (!m_dir.exists()) {
|
||||||
|
if (!m_entries.isEmpty()) {
|
||||||
|
m_entries.clear();
|
||||||
|
emit entriesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<ScanResult>(this);
|
||||||
|
QObject::connect(
|
||||||
|
watcher, &QFutureWatcher<ScanResult>::finished, this, [this, watcher]() {
|
||||||
|
applyScan(watcher->result());
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
const QDir dir = m_dir;
|
||||||
|
|
||||||
|
QHash<QString, QPair<qint64, bool>> knownCodecs;
|
||||||
|
for (const auto* entry : std::as_const(m_byPath)) {
|
||||||
|
if (entry->isVideo()) {
|
||||||
|
knownCodecs.insert(
|
||||||
|
entry->path(), qMakePair(entry->size(), entry->hardware()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([dir, knownCodecs]() {
|
||||||
|
ScanResult result;
|
||||||
|
result.dir = dir;
|
||||||
|
|
||||||
|
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const QString path = it.next();
|
||||||
|
const QFileInfo info(path);
|
||||||
|
|
||||||
|
if (info.isDir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVideoExtension(path)) {
|
||||||
|
FileEntry file;
|
||||||
|
file.info = info;
|
||||||
|
|
||||||
|
const auto known = knownCodecs.constFind(path);
|
||||||
|
if (known != knownCodecs.constEnd() &&
|
||||||
|
known.value().first == info.size()) {
|
||||||
|
file.hardware = known.value().second;
|
||||||
|
} else {
|
||||||
|
file.hardware = probeHardwareCodec(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.files.append(file);
|
||||||
|
} else {
|
||||||
|
QImageReader reader(path);
|
||||||
|
if (reader.canRead()) {
|
||||||
|
FileEntry file;
|
||||||
|
file.info = info;
|
||||||
|
result.files.append(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::watchDir(const QString& dir) {
|
||||||
|
if (dir.isEmpty() || m_watcher.directories().contains(dir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QDir(dir).exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_watcher.addPath(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::applyScan(const ScanResult& result) {
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: scan of %s found %d files (cache target %dx%d)",
|
||||||
|
m_dir.absolutePath().toUtf8().constData(),
|
||||||
|
int(result.files.size()),
|
||||||
|
m_cacheSize.width(),
|
||||||
|
m_cacheSize.height());
|
||||||
|
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
|
||||||
|
QSet<QString> seen;
|
||||||
|
for (const auto& file : result.files) {
|
||||||
|
seen.insert(file.info.absoluteFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = m_byPath.begin(); it != m_byPath.end();) {
|
||||||
|
if (!seen.contains(it.key())) {
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare("DELETE FROM wallpapers WHERE path = ?");
|
||||||
|
query.addBindValue(it.key());
|
||||||
|
query.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete it.value();
|
||||||
|
it = m_byPath.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& file : result.files) {
|
||||||
|
const QFileInfo& info = file.info;
|
||||||
|
const QString path = info.absoluteFilePath();
|
||||||
|
|
||||||
|
const qint64 size = info.size();
|
||||||
|
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
|
||||||
|
const bool isVideo = hasVideoExtension(path);
|
||||||
|
|
||||||
|
watchDir(info.absolutePath());
|
||||||
|
|
||||||
|
const auto existing = m_byPath.constFind(path);
|
||||||
|
if (existing != m_byPath.constEnd()) {
|
||||||
|
if (existing.value()->size() == size) {
|
||||||
|
existing.value()->setHardware(file.hardware);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperEntry* entry = m_byPath.value(path);
|
||||||
|
if (!entry) {
|
||||||
|
entry =
|
||||||
|
new WallpaperEntry(path, m_dir.relativeFilePath(path), this);
|
||||||
|
m_byPath.insert(path, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->setHardware(file.hardware);
|
||||||
|
|
||||||
|
bool reused = false;
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"SELECT cache, scheme_thumb, size, mtime FROM wallpapers"
|
||||||
|
" WHERE path = ?");
|
||||||
|
query.addBindValue(path);
|
||||||
|
query.exec();
|
||||||
|
|
||||||
|
if (query.next()) {
|
||||||
|
const auto cache = query.value(0).toByteArray();
|
||||||
|
const auto scheme = query.value(1).toByteArray();
|
||||||
|
const auto storedSize = query.value(2).toLongLong();
|
||||||
|
const auto storedMtime = query.value(3).toLongLong();
|
||||||
|
|
||||||
|
if (storedSize == size && storedMtime == mtime &&
|
||||||
|
!cache.isEmpty()) {
|
||||||
|
reused = true;
|
||||||
|
entry->setThumbnailPath(thumbnailUrl(path));
|
||||||
|
|
||||||
|
if (scheme.isEmpty()) {
|
||||||
|
queueMetadata(entry, isVideo, cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reused) {
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"INSERT OR REPLACE INTO wallpapers"
|
||||||
|
" (path, kind, hardware, size, mtime, cache)"
|
||||||
|
" VALUES (?, ?, ?, ?, ?, NULL)");
|
||||||
|
query.addBindValue(path);
|
||||||
|
query.addBindValue(isVideo ? "video" : "image");
|
||||||
|
query.addBindValue(
|
||||||
|
isVideo ? (file.hardware ? 1 : 0) : QVariant());
|
||||||
|
query.addBindValue(size);
|
||||||
|
query.addBindValue(mtime);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning(dbLog)
|
||||||
|
<< "WallpaperDb: could not store the row for" << path
|
||||||
|
<< query.lastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueMetadata(entry, isVideo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::queueMetadata(
|
||||||
|
WallpaperEntry* entry, bool isVideo, const QByteArray& existingCache) {
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: queued cache job for %s (%s%s)",
|
||||||
|
entry->path().toUtf8().constData(),
|
||||||
|
isVideo ? "video" : "image",
|
||||||
|
existingCache.isEmpty() ? "" : ", existing blob");
|
||||||
|
|
||||||
|
m_queue.append(MetadataJob{entry, entry->path(), isVideo, existingCache});
|
||||||
|
|
||||||
|
if (!m_queueTimer.isActive()) {
|
||||||
|
m_queueTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::processQueue() {
|
||||||
|
if (m_jobRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_queue.isEmpty()) {
|
||||||
|
m_queueTimer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_cacheSize.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetadataJob job = m_queue.takeFirst();
|
||||||
|
m_jobRunning = true;
|
||||||
|
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: caching %s (%s, %d jobs left in queue)",
|
||||||
|
job.path.toUtf8().constData(),
|
||||||
|
job.isVideo ? "video" : "image",
|
||||||
|
int(m_queue.size()));
|
||||||
|
|
||||||
|
runMetadataJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||||
|
struct CacheResult {
|
||||||
|
QString path;
|
||||||
|
QByteArray cache;
|
||||||
|
QByteArray scheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QSize cacheSize = m_cacheSize;
|
||||||
|
|
||||||
|
QElapsedTimer clock;
|
||||||
|
clock.start();
|
||||||
|
|
||||||
|
auto* watcher = new QFutureWatcher<CacheResult>(this);
|
||||||
|
QObject::connect(
|
||||||
|
watcher,
|
||||||
|
&QFutureWatcher<CacheResult>::finished,
|
||||||
|
this,
|
||||||
|
[this, watcher, clock]() {
|
||||||
|
const auto result = watcher->result();
|
||||||
|
|
||||||
|
qCInfo(
|
||||||
|
dbLog,
|
||||||
|
"WallpaperDb: %s produced %d bytes of cache in %dms",
|
||||||
|
result.path.toUtf8().constData(),
|
||||||
|
int(result.cache.size()),
|
||||||
|
int(clock.elapsed()));
|
||||||
|
|
||||||
|
if (QSqlDatabase::contains(m_uuid)) {
|
||||||
|
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||||
|
if (db.isOpen()) {
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(
|
||||||
|
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?"
|
||||||
|
" WHERE path = ?");
|
||||||
|
query.addBindValue(result.cache);
|
||||||
|
query.addBindValue(result.scheme);
|
||||||
|
query.addBindValue(result.path);
|
||||||
|
if (!query.exec()) {
|
||||||
|
qWarning(dbLog)
|
||||||
|
<< "WallpaperDb: could not store the cache for"
|
||||||
|
<< result.path << query.lastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.cache.isEmpty()) {
|
||||||
|
if (auto* entry = m_byPath.value(result.path)) {
|
||||||
|
entry->setThumbnailPath(thumbnailUrl(result.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit cacheStored(result.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_jobRunning = false;
|
||||||
|
rebuildEntries();
|
||||||
|
watcher->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([job, cacheSize]() {
|
||||||
|
CacheResult result;
|
||||||
|
result.path = job.path;
|
||||||
|
|
||||||
|
QByteArray cache = job.existingCache;
|
||||||
|
if (cache.isEmpty()) {
|
||||||
|
cache = job.isVideo ? videoCacheBytes(job.path, cacheSize)
|
||||||
|
: imageCacheBytes(job.path, cacheSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.cache = cache;
|
||||||
|
result.scheme = cache.isEmpty() ? QByteArray()
|
||||||
|
: schemeThumbBytes(cache, 128);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperDb::rebuildEntries() {
|
||||||
|
QList<WallpaperEntry*> next;
|
||||||
|
next.reserve(m_byPath.size());
|
||||||
|
|
||||||
|
for (auto* entry : std::as_const(m_byPath)) {
|
||||||
|
if (entryAllowed(entry)) {
|
||||||
|
next.append(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(
|
||||||
|
next.begin(),
|
||||||
|
next.end(),
|
||||||
|
[](const WallpaperEntry* a, const WallpaperEntry* b) {
|
||||||
|
return a->relativePath() < b->relativePath();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (next == m_entries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries = next;
|
||||||
|
emit entriesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
|
||||||
|
if (!entry->isVideo()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_requireHardwareCodec) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry->hardware();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZShell::models
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qbytearray.h>
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qfileinfo.h>
|
||||||
|
#include <qfilesystemwatcher.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qlist.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qsize.h>
|
||||||
|
#include <qtimer.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace ZShell::models {
|
||||||
|
|
||||||
|
class WallpaperImageProvider;
|
||||||
|
|
||||||
|
class WallpaperEntry : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_UNCREATABLE(
|
||||||
|
"WallpaperEntry instances can only be retrieved from a WallpaperDb")
|
||||||
|
|
||||||
|
Q_PROPERTY(QString path READ path CONSTANT)
|
||||||
|
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||||
|
Q_PROPERTY(QString name READ name CONSTANT)
|
||||||
|
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||||
|
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||||
|
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||||
|
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||||
|
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||||
|
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||||
|
Q_PROPERTY(bool hardware READ hardware NOTIFY hardwareChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WallpaperEntry(
|
||||||
|
const QString& path,
|
||||||
|
const QString& relativePath,
|
||||||
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QString path() const;
|
||||||
|
[[nodiscard]] QString relativePath() const;
|
||||||
|
[[nodiscard]] QString name() const;
|
||||||
|
[[nodiscard]] QString baseName() const;
|
||||||
|
[[nodiscard]] QString parentDir() const;
|
||||||
|
[[nodiscard]] QString suffix() const;
|
||||||
|
[[nodiscard]] qint64 size() const;
|
||||||
|
[[nodiscard]] bool isImage() const;
|
||||||
|
[[nodiscard]] bool isVideo() const;
|
||||||
|
[[nodiscard]] bool hardware() const;
|
||||||
|
[[nodiscard]] bool cached() const;
|
||||||
|
[[nodiscard]] QString thumbnailPath() const;
|
||||||
|
|
||||||
|
void setRelativePath(const QString& path);
|
||||||
|
void setHardware(bool hardware);
|
||||||
|
void setThumbnailPath(const QString& path);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void relativePathChanged();
|
||||||
|
void hardwareChanged();
|
||||||
|
void thumbnailPathChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QFileInfo m_fileInfo;
|
||||||
|
|
||||||
|
const QString m_path;
|
||||||
|
QString m_relativePath;
|
||||||
|
QString m_thumbnailPath;
|
||||||
|
bool m_hardware = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class WallpaperDb : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||||
|
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||||
|
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QQmlListProperty<ZShell::models::WallpaperEntry> entries READ entries
|
||||||
|
NOTIFY entriesChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QSize cacheSize READ cacheSize WRITE setCacheSize NOTIFY
|
||||||
|
cacheSizeChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WallpaperDb(QObject* parent = nullptr);
|
||||||
|
~WallpaperDb() override;
|
||||||
|
|
||||||
|
[[nodiscard]] QString path() const;
|
||||||
|
void setPath(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] QString dbPath() const;
|
||||||
|
void setDbPath(const QString& path);
|
||||||
|
|
||||||
|
[[nodiscard]] bool requireHardwareCodec() const;
|
||||||
|
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlListProperty<WallpaperEntry> entries();
|
||||||
|
|
||||||
|
[[nodiscard]] QSize cacheSize() const;
|
||||||
|
void setCacheSize(const QSize& cacheSize);
|
||||||
|
|
||||||
|
[[nodiscard]] static QString thumbnailUrl(const QString& path);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pathChanged();
|
||||||
|
void dbPathChanged();
|
||||||
|
void requireHardwareCodecChanged();
|
||||||
|
void entriesChanged();
|
||||||
|
void cacheSizeChanged();
|
||||||
|
|
||||||
|
void cacheStored(const QString& path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FileEntry {
|
||||||
|
QFileInfo info;
|
||||||
|
bool hardware = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScanResult {
|
||||||
|
QVector<FileEntry> files;
|
||||||
|
QDir dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MetadataJob {
|
||||||
|
WallpaperEntry* entry;
|
||||||
|
QString path;
|
||||||
|
bool isVideo;
|
||||||
|
QByteArray existingCache;
|
||||||
|
};
|
||||||
|
|
||||||
|
void openDb();
|
||||||
|
void registerProvider();
|
||||||
|
void scanDir();
|
||||||
|
void watchDir(const QString& dir);
|
||||||
|
void applyScan(const ScanResult& result);
|
||||||
|
void queueMetadata(
|
||||||
|
WallpaperEntry* entry,
|
||||||
|
bool isVideo,
|
||||||
|
const QByteArray& existingCache = {});
|
||||||
|
void processQueue();
|
||||||
|
void runMetadataJob(const MetadataJob& job);
|
||||||
|
void rebuildEntries();
|
||||||
|
|
||||||
|
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
|
||||||
|
|
||||||
|
QDir m_dir;
|
||||||
|
QString m_path;
|
||||||
|
QString m_dbPath;
|
||||||
|
QString m_uuid;
|
||||||
|
WallpaperImageProvider* m_provider = nullptr;
|
||||||
|
|
||||||
|
bool m_requireHardwareCodec = false;
|
||||||
|
QSize m_cacheSize;
|
||||||
|
|
||||||
|
QList<WallpaperEntry*> m_entries;
|
||||||
|
QHash<QString, WallpaperEntry*> m_byPath;
|
||||||
|
|
||||||
|
QVector<MetadataJob> m_queue;
|
||||||
|
QTimer m_queueTimer;
|
||||||
|
bool m_jobRunning = false;
|
||||||
|
|
||||||
|
QFileSystemWatcher m_watcher;
|
||||||
|
QTimer m_watchTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::models
|
||||||
@@ -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
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import sqlite3
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -50,6 +52,30 @@ VIDEO_EXTENSIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def scheme_thumb_from_db(db_path: str, wallpaper_path: str) -> bytes | None:
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||||
|
except sqlite3.Error:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT scheme_thumb FROM wallpapers WHERE path = ?",
|
||||||
|
(wallpaper_path,),
|
||||||
|
).fetchone()
|
||||||
|
except sqlite3.Error:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row and row[0]:
|
||||||
|
return bytes(row[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _complete_scheme_name(incomplete):
|
def _complete_scheme_name(incomplete):
|
||||||
schemes = [
|
schemes = [
|
||||||
"fruit-salad",
|
"fruit-salad",
|
||||||
@@ -181,6 +207,7 @@ def generate(
|
|||||||
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
OUTPUT = Path(HOME + "/.local/state/zshell/scheme.json")
|
||||||
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
SEQ_STATE = Path(HOME + "/.local/state/zshell/sequences.txt")
|
||||||
THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails")
|
THUMB_DIR = Path(HOME + "/.cache/zshell/imagecache/thumbnails")
|
||||||
|
DB_PATH = Path(HOME + "/.local/state/zshell/wallpapers.sqlite")
|
||||||
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
WALL_DIR_PATH = Path(HOME + "/.local/state/zshell/wallpaper_path.json")
|
||||||
|
|
||||||
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
TEMPLATE_DIR = Path(HOME + "/.config/zshell/templates")
|
||||||
@@ -808,13 +835,20 @@ def generate(
|
|||||||
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
seed = hex_to_hct(colors.get("primary", "#000000").lstrip("#"))
|
||||||
else:
|
else:
|
||||||
image_path = image_path or Path(WALL_PATH)
|
image_path = image_path or Path(WALL_PATH)
|
||||||
|
|
||||||
|
blob = scheme_thumb_from_db(str(DB_PATH), str(image_path))
|
||||||
|
if blob is not None:
|
||||||
|
open_thumb = lambda: io.BytesIO(blob)
|
||||||
|
else:
|
||||||
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
|
thumb_path = generate_thumbnail(image_path, THUMB_DIR)
|
||||||
seed = seed_from_image(thumb_path)
|
open_thumb = lambda: thumb_path
|
||||||
|
|
||||||
|
seed = seed_from_image(open_thumb())
|
||||||
name = "dynamic"
|
name = "dynamic"
|
||||||
flavor = "default"
|
flavor = "default"
|
||||||
|
|
||||||
if smart:
|
if smart:
|
||||||
effective_mode = smart_mode(thumb_path)
|
effective_mode = smart_mode(open_thumb())
|
||||||
elif mode is not None:
|
elif mode is not None:
|
||||||
effective_mode = mode
|
effective_mode = mode
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user