Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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}")
Expand Down
6 changes: 4 additions & 2 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
cmake,
pkg-config,
cairo,
ffmpeg ? null,
libdrm,
libGL,
libxkbcommon,
Expand All @@ -19,6 +20,7 @@
wayland,
wayland-protocols,
wayland-scanner,
withVideoBackend ? true,
version ? "git",
shortRev ? "",
}:
Expand Down Expand Up @@ -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";
Expand Down
177 changes: 177 additions & 0 deletions src/renderer/VideoBackend.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 62 additions & 0 deletions src/renderer/VideoBackend.hpp
Original file line number Diff line number Diff line change
@@ -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};
};
Loading
Loading