This commit is contained in:
mcgi5sr2 2026-05-17 01:41:46 +02:00 committed by GitHub
commit f7d2a20cec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 349 additions and 6 deletions

View file

@ -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}")

View file

@ -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";

View 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);
});
}

View 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};
};

View file

@ -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();

View file

@ -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
};