mirror of
https://github.com/hyprwm/hyprlock.git
synced 2026-05-19 19:28:08 +02:00
Merge a48820ac25 into 274154f92a
This commit is contained in:
commit
f7d2a20cec
6 changed files with 349 additions and 6 deletions
|
|
@ -89,6 +89,14 @@ pkg_check_modules(
|
|||
hyprutils>=0.11.0
|
||||
sdbus-c++>=2.0.0
|
||||
hyprgraphics>=0.1.6)
|
||||
|
||||
option(VIDEO_BACKEND "Enable video background support via FFmpeg" ON)
|
||||
if(VIDEO_BACKEND)
|
||||
pkg_check_modules(ffmpeg REQUIRED IMPORTED_TARGET libavcodec libavformat libavutil libswscale)
|
||||
message(STATUS "Video backend: enabled")
|
||||
else()
|
||||
message(STATUS "Video backend: disabled")
|
||||
endif()
|
||||
find_library(PAM_FOUND NAMES pam libpam)
|
||||
if(PAM_FOUND)
|
||||
set(PAM_LIB ${PAM_FOUND})
|
||||
|
|
@ -104,10 +112,20 @@ endif()
|
|||
message(STATUS "Found pam at ${PAM_LIB}")
|
||||
|
||||
file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
|
||||
if(NOT VIDEO_BACKEND)
|
||||
list(FILTER SRCFILES EXCLUDE REGEX ".*/VideoBackend\\.cpp$")
|
||||
endif()
|
||||
|
||||
add_executable(hyprlock ${SRCFILES})
|
||||
target_link_libraries(hyprlock PRIVATE ${PAM_LIB} rt Threads::Threads PkgConfig::deps
|
||||
OpenGL::EGL OpenGL::GLES3)
|
||||
|
||||
if(VIDEO_BACKEND)
|
||||
target_compile_definitions(hyprlock PRIVATE HYPRLOCK_HAS_VIDEO)
|
||||
target_link_libraries(hyprlock PRIVATE PkgConfig::ffmpeg)
|
||||
endif()
|
||||
|
||||
# protocols
|
||||
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
|
||||
message(STATUS "Found wayland-protocols at ${WAYLAND_PROTOCOLS_DIR}")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
cmake,
|
||||
pkg-config,
|
||||
cairo,
|
||||
ffmpeg ? null,
|
||||
libdrm,
|
||||
libGL,
|
||||
libxkbcommon,
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
withVideoBackend ? true,
|
||||
version ? "git",
|
||||
shortRev ? "",
|
||||
}:
|
||||
|
|
@ -50,12 +52,12 @@ stdenv.mkDerivation {
|
|||
systemdLibs
|
||||
wayland
|
||||
wayland-protocols
|
||||
];
|
||||
] ++ lib.optionals withVideoBackend [ ffmpeg ];
|
||||
|
||||
cmakeFlags = lib.mapAttrsToList lib.cmakeFeature {
|
||||
HYPRLOCK_COMMIT = shortRev;
|
||||
HYPRLOCK_VERSION_COMMIT = ""; # Intentionally left empty (hyprlock --version will always print the commit)
|
||||
};
|
||||
} ++ lib.optional (!withVideoBackend) (lib.cmakeBool "VIDEO_BACKEND" false);
|
||||
|
||||
meta = {
|
||||
homepage = "https://github.com/hyprwm/hyprlock";
|
||||
|
|
|
|||
177
src/renderer/VideoBackend.cpp
Normal file
177
src/renderer/VideoBackend.cpp
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
#include "VideoBackend.hpp"
|
||||
#include "../helpers/Log.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
CVideoBackend::~CVideoBackend() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool CVideoBackend::isVideoFile(const std::string& path) {
|
||||
static const std::unordered_set<std::string> VIDEO_EXT = {
|
||||
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v",
|
||||
".flv", ".wmv", ".ts", ".m2ts", ".gif"
|
||||
};
|
||||
auto ext = std::filesystem::path(path).extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||
return VIDEO_EXT.count(ext) > 0;
|
||||
}
|
||||
|
||||
bool CVideoBackend::open(const std::string& path) {
|
||||
if (avformat_open_input(&m_formatCtx, path.c_str(), nullptr, nullptr) < 0) {
|
||||
Log::logger->log(Log::ERR, "CVideoBackend: avformat_open_input failed for {}", path);
|
||||
return false;
|
||||
}
|
||||
if (avformat_find_stream_info(m_formatCtx, nullptr) < 0) {
|
||||
Log::logger->log(Log::ERR, "CVideoBackend: avformat_find_stream_info failed for {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const AVCodec* codec = nullptr;
|
||||
m_streamIdx = av_find_best_stream(m_formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
|
||||
if (m_streamIdx < 0 || !codec) {
|
||||
Log::logger->log(Log::ERR, "CVideoBackend: no video stream found in {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_codecCtx = avcodec_alloc_context3(codec);
|
||||
if (!m_codecCtx) {
|
||||
Log::logger->log(Log::ERR, "CVideoBackend: avcodec_alloc_context3 failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
avcodec_parameters_to_context(m_codecCtx, m_formatCtx->streams[m_streamIdx]->codecpar);
|
||||
|
||||
if (avcodec_open2(m_codecCtx, codec, nullptr) < 0) {
|
||||
Log::logger->log(Log::ERR, "CVideoBackend: avcodec_open2 failed for {}", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_frameW = m_codecCtx->width;
|
||||
m_frameH = m_codecCtx->height;
|
||||
m_timeBase = av_q2d(m_formatCtx->streams[m_streamIdx]->time_base);
|
||||
|
||||
// swsCtx is created lazily per-frame via sws_getCachedContext so that
|
||||
// we handle codecs where pix_fmt is only known after the first decode.
|
||||
m_frameData.resize(4 * m_frameW * m_frameH);
|
||||
|
||||
Log::logger->log(Log::INFO, "CVideoBackend: opened {} ({}x{}, timebase={:.6f})",
|
||||
path, m_frameW, m_frameH, m_timeBase);
|
||||
|
||||
startDecodeThread();
|
||||
return true;
|
||||
}
|
||||
|
||||
void CVideoBackend::stop() {
|
||||
m_running = false;
|
||||
if (m_decodeThread.joinable())
|
||||
m_decodeThread.join();
|
||||
if (m_swsCtx) sws_freeContext(m_swsCtx);
|
||||
if (m_codecCtx) avcodec_free_context(&m_codecCtx);
|
||||
if (m_formatCtx) avformat_close_input(&m_formatCtx);
|
||||
m_swsCtx = nullptr;
|
||||
m_codecCtx = nullptr;
|
||||
m_formatCtx = nullptr;
|
||||
}
|
||||
|
||||
bool CVideoBackend::swapFrame(std::vector<uint8_t>& buf) {
|
||||
std::lock_guard<std::mutex> lock(m_frameMutex);
|
||||
if (!m_hasNewFrame)
|
||||
return false;
|
||||
std::swap(m_frameData, buf);
|
||||
m_hasNewFrame = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CVideoBackend::startDecodeThread() {
|
||||
m_running = true;
|
||||
m_startTime = std::chrono::steady_clock::now();
|
||||
|
||||
m_decodeThread = std::thread([this]() {
|
||||
AVPacket* pkt = av_packet_alloc();
|
||||
AVFrame* frame = av_frame_alloc();
|
||||
// Pre-size the tmp buffer so it's never empty when swapping with m_frameData.
|
||||
// An empty vector has data()==null which causes "bad dst image pointers" in sws_scale.
|
||||
std::vector<uint8_t> tmpBuf(4 * m_frameW * m_frameH);
|
||||
|
||||
while (m_running) {
|
||||
int ret = av_read_frame(m_formatCtx, pkt);
|
||||
|
||||
if (ret == AVERROR_EOF) {
|
||||
// Flush decoder's internal buffer
|
||||
avcodec_send_packet(m_codecCtx, nullptr);
|
||||
while (avcodec_receive_frame(m_codecCtx, frame) == 0)
|
||||
av_frame_unref(frame);
|
||||
|
||||
// Loop: seek back to beginning
|
||||
av_seek_frame(m_formatCtx, m_streamIdx, 0, AVSEEK_FLAG_BACKWARD);
|
||||
avcodec_flush_buffers(m_codecCtx);
|
||||
m_startTime = std::chrono::steady_clock::now();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ret < 0)
|
||||
break; // unrecoverable error
|
||||
|
||||
if (pkt->stream_index != m_streamIdx) {
|
||||
av_packet_unref(pkt);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (avcodec_send_packet(m_codecCtx, pkt) < 0) {
|
||||
av_packet_unref(pkt);
|
||||
continue;
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
|
||||
while (avcodec_receive_frame(m_codecCtx, frame) == 0) {
|
||||
if (!m_running)
|
||||
break;
|
||||
|
||||
// Lazily create/update SwsContext to match the frame's actual pixel
|
||||
// format (some codecs only report it after the first frame).
|
||||
m_swsCtx = sws_getCachedContext(m_swsCtx,
|
||||
frame->width, frame->height, (AVPixelFormat)frame->format,
|
||||
m_frameW, m_frameH, AV_PIX_FMT_RGBA,
|
||||
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||||
if (!m_swsCtx) {
|
||||
av_frame_unref(frame);
|
||||
continue;
|
||||
}
|
||||
|
||||
// sws_scale requires 4-element pointer/stride arrays even for
|
||||
// packed formats — passing a 1-element array causes UB reads.
|
||||
uint8_t* dst[4] = {tmpBuf.data(), nullptr, nullptr, nullptr};
|
||||
int stride[4] = {4 * m_frameW, 0, 0, 0};
|
||||
sws_scale(m_swsCtx,
|
||||
(const uint8_t* const*)frame->data, frame->linesize,
|
||||
0, frame->height, dst, stride);
|
||||
|
||||
// Publish frame via O(1) swap (no memcpy)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_frameMutex);
|
||||
std::swap(m_frameData, tmpBuf);
|
||||
m_hasNewFrame = true;
|
||||
}
|
||||
// tmpBuf now holds old frame data — overwritten next iteration
|
||||
|
||||
// PTS-based frame pacing
|
||||
if (frame->pts != AV_NOPTS_VALUE) {
|
||||
double pts_sec = frame->pts * m_timeBase;
|
||||
auto target = m_startTime +
|
||||
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
||||
std::chrono::duration<double>(pts_sec));
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
// Safety cap: never sleep > 5s (guards against bogus PTS values)
|
||||
auto maxTarget = now + std::chrono::seconds(5);
|
||||
if (target > now && target < maxTarget)
|
||||
std::this_thread::sleep_until(target);
|
||||
}
|
||||
|
||||
av_frame_unref(frame);
|
||||
}
|
||||
}
|
||||
|
||||
av_packet_free(&pkt);
|
||||
av_frame_free(&frame);
|
||||
});
|
||||
}
|
||||
62
src/renderer/VideoBackend.hpp
Normal file
62
src/renderer/VideoBackend.hpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <unordered_set>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
}
|
||||
|
||||
// Handles FFmpeg video decoding on a background thread.
|
||||
// CBackground owns one of these when path is a video file.
|
||||
// All GL upload and rendering stays in CBackground.
|
||||
class CVideoBackend {
|
||||
public:
|
||||
~CVideoBackend();
|
||||
|
||||
// Returns true if the file extension is a recognised video format.
|
||||
static bool isVideoFile(const std::string& path);
|
||||
|
||||
// Open the file and start the decode thread. Returns false on failure.
|
||||
bool open(const std::string& path);
|
||||
|
||||
// Stop the decode thread and release all FFmpeg resources.
|
||||
void stop();
|
||||
|
||||
// Swap the latest decoded RGBA frame into buf (O(1), no memcpy).
|
||||
// Returns true if a new frame was available and buf was updated.
|
||||
bool swapFrame(std::vector<uint8_t>& buf);
|
||||
|
||||
int frameW() const { return m_frameW; }
|
||||
int frameH() const { return m_frameH; }
|
||||
bool isRunning() const { return m_running; }
|
||||
|
||||
private:
|
||||
void startDecodeThread();
|
||||
|
||||
AVFormatContext* m_formatCtx = nullptr;
|
||||
AVCodecContext* m_codecCtx = nullptr;
|
||||
SwsContext* m_swsCtx = nullptr;
|
||||
int m_streamIdx = -1;
|
||||
int m_frameW = 0;
|
||||
int m_frameH = 0;
|
||||
double m_timeBase = 0.0;
|
||||
|
||||
std::mutex m_frameMutex;
|
||||
std::vector<uint8_t> m_frameData;
|
||||
bool m_hasNewFrame = false;
|
||||
|
||||
std::chrono::steady_clock::time_point m_startTime;
|
||||
|
||||
std::thread m_decodeThread;
|
||||
std::atomic<bool> m_running{false};
|
||||
};
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
#include "../../helpers/MiscFunctions.hpp"
|
||||
#include "../../core/AnimationManager.hpp"
|
||||
#include "../../config/ConfigManager.hpp"
|
||||
#include <chrono>
|
||||
#include <hyprlang.hpp>
|
||||
#include <filesystem>
|
||||
#include <GLES3/gl32.h>
|
||||
|
|
@ -40,6 +39,7 @@ static std::string runAndGetPath(const std::string& reloadCommand) {
|
|||
return path;
|
||||
}
|
||||
|
||||
|
||||
void CBackground::configure(const std::unordered_map<std::string, std::any>& props, const SP<COutput>& pOutput) {
|
||||
reset();
|
||||
|
||||
|
|
@ -86,10 +86,29 @@ void CBackground::configure(const std::unordered_map<std::string, std::any>& pro
|
|||
Log::logger->log(Log::ERR, "No screencopy support! path=screenshot won't work. Falling back to background color.");
|
||||
resourceID = 0;
|
||||
}
|
||||
} else if (!path.empty())
|
||||
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
|
||||
} else if (!path.empty()) {
|
||||
#ifdef HYPRLOCK_HAS_VIDEO
|
||||
if (CVideoBackend::isVideoFile(path)) {
|
||||
m_videoBackend = makeUnique<CVideoBackend>();
|
||||
if (!m_videoBackend->open(path)) {
|
||||
Log::logger->log(Log::ERR, "CBackground: failed to open '{}' as video, falling back to image", path);
|
||||
m_videoBackend.reset();
|
||||
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
|
||||
} else {
|
||||
m_uploadBuffer.resize(4 * m_videoBackend->frameW() * m_videoBackend->frameH());
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
resourceID = g_asyncResourceManager->requestImage(path, m_imageRevision, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!reloadCommand.empty() && reloadTime > -1) {
|
||||
if (!reloadCommand.empty() && reloadTime > -1
|
||||
#ifdef HYPRLOCK_HAS_VIDEO
|
||||
&& !m_videoBackend
|
||||
#endif
|
||||
) {
|
||||
try {
|
||||
if (!isScreenshot)
|
||||
modificationTime = std::filesystem::last_write_time(absolutePath(path, ""));
|
||||
|
|
@ -100,6 +119,14 @@ void CBackground::configure(const std::unordered_map<std::string, std::any>& pro
|
|||
}
|
||||
|
||||
void CBackground::reset() {
|
||||
#ifdef HYPRLOCK_HAS_VIDEO
|
||||
if (m_videoBackend) {
|
||||
m_videoBackend.reset();
|
||||
m_videoTexture.destroyTexture();
|
||||
m_uploadBuffer.clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (reloadTimer) {
|
||||
reloadTimer->cancel();
|
||||
reloadTimer.reset();
|
||||
|
|
@ -225,6 +252,52 @@ void CBackground::renderToFB(const CTexture& tex, CFramebuffer& fb, int passes,
|
|||
}
|
||||
|
||||
bool CBackground::draw(const SRenderData& data) {
|
||||
#ifdef HYPRLOCK_HAS_VIDEO
|
||||
// ── Video background fast path ────────────────────────────────────────
|
||||
if (m_videoBackend) {
|
||||
if (m_videoBackend->swapFrame(m_uploadBuffer)) {
|
||||
const int W = m_videoBackend->frameW();
|
||||
const int H = m_videoBackend->frameH();
|
||||
|
||||
if (!m_videoTexture.m_bAllocated) {
|
||||
// First frame: allocate the GL texture
|
||||
m_videoTexture.allocate();
|
||||
glBindTexture(GL_TEXTURE_2D, m_videoTexture.m_iTexID);
|
||||
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);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, W, H, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, m_uploadBuffer.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
m_videoTexture.m_vSize = {(double)W, (double)H};
|
||||
m_videoTexture.m_iType = TEXTURE_RGBA;
|
||||
m_videoTexture.m_iTarget = GL_TEXTURE_2D;
|
||||
} else {
|
||||
glBindTexture(GL_TEXTURE_2D, m_videoTexture.m_iTexID);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, W, H,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, m_uploadBuffer.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
if (blurPasses > 0)
|
||||
renderToFB(m_videoTexture, *blurredFB, blurPasses);
|
||||
}
|
||||
|
||||
if (!m_videoTexture.m_bAllocated) {
|
||||
renderRect(color); // solid colour until first frame is ready
|
||||
return true;
|
||||
}
|
||||
|
||||
const CTexture& TEX = (blurPasses > 0 && blurredFB->isAllocated())
|
||||
? blurredFB->m_cTex : m_videoTexture;
|
||||
const auto TEXBOX = getScaledBoxForTextureSize(TEX.m_vSize, viewport);
|
||||
g_pRenderer->renderTexture(TEXBOX, TEX, data.opacity);
|
||||
return true; // always request the next compositor frame
|
||||
}
|
||||
// ── End video path ────────────────────────────────────────────────────
|
||||
#endif
|
||||
|
||||
updatePrimaryAsset();
|
||||
updatePendingAsset();
|
||||
updateScAsset();
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@
|
|||
#include "../../helpers/Color.hpp"
|
||||
#include "../../core/Timer.hpp"
|
||||
#include "../Framebuffer.hpp"
|
||||
#ifdef HYPRLOCK_HAS_VIDEO
|
||||
#include "../VideoBackend.hpp"
|
||||
#endif
|
||||
#include <hyprutils/math/Misc.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <any>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
struct SPreloadedAsset;
|
||||
class COutput;
|
||||
|
|
@ -82,4 +86,11 @@ class CBackground : public IWidget {
|
|||
ASP<CTimer> reloadTimer;
|
||||
std::filesystem::file_time_type modificationTime;
|
||||
size_t m_imageRevision = 0;
|
||||
|
||||
// Video playback
|
||||
#ifdef HYPRLOCK_HAS_VIDEO
|
||||
UP<CVideoBackend> m_videoBackend;
|
||||
CTexture m_videoTexture;
|
||||
std::vector<uint8_t> m_uploadBuffer;
|
||||
#endif
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue