Merge branch 'main' into main

This commit is contained in:
SilentWind
2026-02-10 14:31:17 +08:00
committed by GitHub
95 changed files with 7374 additions and 3732 deletions

View File

@@ -1,12 +1,16 @@
#include "linux.h"
#include "../../log.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <dlfcn.h>
#include <errno.h>
#include <fstream>
#include <signal.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
// Check for NVIDIA driver support by loading CUDA libraries
int linux_support_nv()
@@ -106,6 +110,57 @@ int linux_support_rkmpp() {
// Check for V4L2 Memory-to-Memory (M2M) codec support
// Returns 0 if a M2M capable device is found, -1 otherwise
int linux_support_v4l2m2m() {
auto to_lower = [](std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return value;
};
auto read_text_file = [](const char *path, std::string *out) -> bool {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::getline(file, *out);
return !out->empty();
};
auto allow_video0_probe = []() -> bool {
const char *env = std::getenv("ONE_KVM_V4L2M2M_ALLOW_VIDEO0");
if (env == nullptr) {
return false;
}
if (env[0] == '\0') {
return false;
}
return std::strcmp(env, "0") != 0;
};
auto is_amlogic_vdec = [&]() -> bool {
std::string name;
std::string modalias;
if (read_text_file("/sys/class/video4linux/video0/name", &name)) {
const std::string lowered = to_lower(name);
if (lowered.find("meson") != std::string::npos ||
lowered.find("vdec") != std::string::npos ||
lowered.find("decoder") != std::string::npos ||
lowered.find("video-decoder") != std::string::npos) {
return true;
}
}
if (read_text_file("/sys/class/video4linux/video0/device/modalias", &modalias)) {
const std::string lowered = to_lower(modalias);
if (lowered.find("amlogic") != std::string::npos ||
lowered.find("meson") != std::string::npos ||
lowered.find("gxl-vdec") != std::string::npos ||
lowered.find("gx-vdec") != std::string::npos) {
return true;
}
}
return false;
};
// Check common V4L2 M2M device paths used by various ARM SoCs
// /dev/video10 - Standard on many SoCs
// /dev/video11 - Standard on many SoCs (often decoder)
@@ -124,6 +179,13 @@ int linux_support_v4l2m2m() {
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
if (access(m2m_devices[i], F_OK) == 0) {
if (std::strcmp(m2m_devices[i], "/dev/video0") == 0) {
if (!allow_video0_probe() && is_amlogic_vdec()) {
LOG_TRACE(std::string("V4L2 M2M: Skipping /dev/video0 (Amlogic vdec)"));
continue;
}
}
// Device exists, check if it's an M2M device by trying to open it
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
if (fd >= 0) {

View File

@@ -0,0 +1,50 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// MJPEG -> H26x (H.264 / H.265) hardware pipeline
typedef struct FfmpegHwMjpegH26x FfmpegHwMjpegH26x;
// Create a new MJPEG -> H26x pipeline.
FfmpegHwMjpegH26x* ffmpeg_hw_mjpeg_h26x_new(const char* dec_name,
const char* enc_name,
int width,
int height,
int fps,
int bitrate_kbps,
int gop,
int thread_count);
// Encode one MJPEG frame. Returns 1 if output produced, 0 if no output, <0 on error.
int ffmpeg_hw_mjpeg_h26x_encode(FfmpegHwMjpegH26x* ctx,
const uint8_t* data,
int len,
int64_t pts_ms,
uint8_t** out_data,
int* out_len,
int* out_keyframe);
// Reconfigure bitrate/gop (best-effort, may recreate encoder internally).
int ffmpeg_hw_mjpeg_h26x_reconfigure(FfmpegHwMjpegH26x* ctx,
int bitrate_kbps,
int gop);
// Request next frame to be a keyframe.
int ffmpeg_hw_mjpeg_h26x_request_keyframe(FfmpegHwMjpegH26x* ctx);
// Free pipeline resources.
void ffmpeg_hw_mjpeg_h26x_free(FfmpegHwMjpegH26x* ctx);
// Free packet buffer allocated by ffmpeg_hw_mjpeg_h26x_encode.
void ffmpeg_hw_packet_free(uint8_t* data);
// Get last error message (thread-local).
const char* ffmpeg_hw_last_error(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,468 @@
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/error.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_drm.h>
#include <libavutil/pixdesc.h>
#include <libavutil/opt.h>
}
#include <string>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define LOG_MODULE "FFMPEG_HW"
#include "../common/log.h"
#include "ffmpeg_hw_ffi.h"
namespace {
thread_local std::string g_last_error;
static void set_last_error(const std::string &msg) {
g_last_error = msg;
LOG_ERROR(msg);
}
static std::string make_err(const std::string &ctx, int err) {
return ctx + " (ret=" + std::to_string(err) + "): " + av_err2str(err);
}
static const char* pix_fmt_name(AVPixelFormat fmt) {
const char *name = av_get_pix_fmt_name(fmt);
return name ? name : "unknown";
}
struct FfmpegHwMjpegH26xCtx {
AVCodecContext *dec_ctx = nullptr;
AVCodecContext *enc_ctx = nullptr;
AVPacket *dec_pkt = nullptr;
AVFrame *dec_frame = nullptr;
AVPacket *enc_pkt = nullptr;
AVBufferRef *hw_device_ctx = nullptr;
AVBufferRef *hw_frames_ctx = nullptr;
AVPixelFormat hw_pixfmt = AV_PIX_FMT_NONE;
std::string dec_name;
std::string enc_name;
int width = 0;
int height = 0;
int aligned_width = 0;
int aligned_height = 0;
int fps = 30;
int bitrate_kbps = 2000;
int gop = 60;
int thread_count = 1;
bool force_keyframe = false;
};
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
const enum AVPixelFormat *pix_fmts) {
auto *self = reinterpret_cast<FfmpegHwMjpegH26xCtx *>(ctx->opaque);
if (self && self->hw_pixfmt != AV_PIX_FMT_NONE) {
const enum AVPixelFormat *p;
for (p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
if (*p == self->hw_pixfmt) {
return *p;
}
}
}
return pix_fmts[0];
}
static int init_decoder(FfmpegHwMjpegH26xCtx *ctx) {
const AVCodec *dec = avcodec_find_decoder_by_name(ctx->dec_name.c_str());
if (!dec) {
set_last_error("Decoder not found: " + ctx->dec_name);
return -1;
}
ctx->dec_ctx = avcodec_alloc_context3(dec);
if (!ctx->dec_ctx) {
set_last_error("Failed to allocate decoder context");
return -1;
}
ctx->dec_ctx->width = ctx->width;
ctx->dec_ctx->height = ctx->height;
ctx->dec_ctx->thread_count = ctx->thread_count > 0 ? ctx->thread_count : 1;
ctx->dec_ctx->opaque = ctx;
// Pick HW pixfmt for RKMPP
const AVCodecHWConfig *cfg = nullptr;
for (int i = 0; (cfg = avcodec_get_hw_config(dec, i)); i++) {
if (cfg->device_type == AV_HWDEVICE_TYPE_RKMPP) {
ctx->hw_pixfmt = cfg->pix_fmt;
break;
}
}
if (ctx->hw_pixfmt == AV_PIX_FMT_NONE) {
set_last_error("No RKMPP hw pixfmt for decoder");
return -1;
}
int ret = av_hwdevice_ctx_create(&ctx->hw_device_ctx,
AV_HWDEVICE_TYPE_RKMPP, NULL, NULL, 0);
if (ret < 0) {
set_last_error(make_err("av_hwdevice_ctx_create failed", ret));
return -1;
}
ctx->dec_ctx->hw_device_ctx = av_buffer_ref(ctx->hw_device_ctx);
ctx->dec_ctx->get_format = get_hw_format;
ret = avcodec_open2(ctx->dec_ctx, dec, NULL);
if (ret < 0) {
set_last_error(make_err("avcodec_open2 decoder failed", ret));
return -1;
}
ctx->dec_pkt = av_packet_alloc();
ctx->dec_frame = av_frame_alloc();
ctx->enc_pkt = av_packet_alloc();
if (!ctx->dec_pkt || !ctx->dec_frame || !ctx->enc_pkt) {
set_last_error("Failed to allocate packet/frame");
return -1;
}
return 0;
}
static int init_encoder(FfmpegHwMjpegH26xCtx *ctx, AVBufferRef *frames_ctx) {
const AVCodec *enc = avcodec_find_encoder_by_name(ctx->enc_name.c_str());
if (!enc) {
set_last_error("Encoder not found: " + ctx->enc_name);
return -1;
}
ctx->enc_ctx = avcodec_alloc_context3(enc);
if (!ctx->enc_ctx) {
set_last_error("Failed to allocate encoder context");
return -1;
}
ctx->enc_ctx->width = ctx->width;
ctx->enc_ctx->height = ctx->height;
ctx->enc_ctx->coded_width = ctx->width;
ctx->enc_ctx->coded_height = ctx->height;
ctx->aligned_width = ctx->width;
ctx->aligned_height = ctx->height;
ctx->enc_ctx->time_base = AVRational{1, 1000};
ctx->enc_ctx->framerate = AVRational{ctx->fps, 1};
ctx->enc_ctx->bit_rate = (int64_t)ctx->bitrate_kbps * 1000;
ctx->enc_ctx->gop_size = ctx->gop > 0 ? ctx->gop : ctx->fps;
ctx->enc_ctx->max_b_frames = 0;
ctx->enc_ctx->pix_fmt = AV_PIX_FMT_DRM_PRIME;
ctx->enc_ctx->sw_pix_fmt = AV_PIX_FMT_NV12;
if (frames_ctx) {
AVHWFramesContext *hwfc = reinterpret_cast<AVHWFramesContext *>(frames_ctx->data);
if (hwfc) {
ctx->enc_ctx->pix_fmt = static_cast<AVPixelFormat>(hwfc->format);
ctx->enc_ctx->sw_pix_fmt = static_cast<AVPixelFormat>(hwfc->sw_format);
if (hwfc->width > 0) {
ctx->aligned_width = hwfc->width;
ctx->enc_ctx->coded_width = hwfc->width;
}
if (hwfc->height > 0) {
ctx->aligned_height = hwfc->height;
ctx->enc_ctx->coded_height = hwfc->height;
}
}
ctx->hw_frames_ctx = av_buffer_ref(frames_ctx);
ctx->enc_ctx->hw_frames_ctx = av_buffer_ref(frames_ctx);
}
if (ctx->hw_device_ctx) {
ctx->enc_ctx->hw_device_ctx = av_buffer_ref(ctx->hw_device_ctx);
}
AVDictionary *opts = nullptr;
av_dict_set(&opts, "rc_mode", "CBR", 0);
if (enc->id == AV_CODEC_ID_H264) {
av_dict_set(&opts, "profile", "high", 0);
} else if (enc->id == AV_CODEC_ID_HEVC) {
av_dict_set(&opts, "profile", "main", 0);
}
av_dict_set_int(&opts, "qp_init", 23, 0);
av_dict_set_int(&opts, "qp_max", 48, 0);
av_dict_set_int(&opts, "qp_min", 0, 0);
av_dict_set_int(&opts, "qp_max_i", 48, 0);
av_dict_set_int(&opts, "qp_min_i", 0, 0);
int ret = avcodec_open2(ctx->enc_ctx, enc, &opts);
av_dict_free(&opts);
if (ret < 0) {
std::string detail = "avcodec_open2 encoder failed: ";
detail += ctx->enc_name;
detail += " fmt=" + std::string(pix_fmt_name(ctx->enc_ctx->pix_fmt));
detail += " sw=" + std::string(pix_fmt_name(ctx->enc_ctx->sw_pix_fmt));
detail += " size=" + std::to_string(ctx->enc_ctx->width) + "x" + std::to_string(ctx->enc_ctx->height);
detail += " fps=" + std::to_string(ctx->fps);
set_last_error(make_err(detail, ret));
avcodec_free_context(&ctx->enc_ctx);
ctx->enc_ctx = nullptr;
if (ctx->hw_frames_ctx) {
av_buffer_unref(&ctx->hw_frames_ctx);
ctx->hw_frames_ctx = nullptr;
}
return -1;
}
return 0;
}
static void free_encoder(FfmpegHwMjpegH26xCtx *ctx) {
if (ctx->enc_ctx) {
avcodec_free_context(&ctx->enc_ctx);
ctx->enc_ctx = nullptr;
}
if (ctx->hw_frames_ctx) {
av_buffer_unref(&ctx->hw_frames_ctx);
ctx->hw_frames_ctx = nullptr;
}
}
} // namespace
extern "C" FfmpegHwMjpegH26x* ffmpeg_hw_mjpeg_h26x_new(const char* dec_name,
const char* enc_name,
int width,
int height,
int fps,
int bitrate_kbps,
int gop,
int thread_count) {
if (!dec_name || !enc_name || width <= 0 || height <= 0) {
set_last_error("Invalid parameters for ffmpeg_hw_mjpeg_h26x_new");
return nullptr;
}
auto *ctx = new FfmpegHwMjpegH26xCtx();
ctx->dec_name = dec_name;
ctx->enc_name = enc_name;
ctx->width = width;
ctx->height = height;
ctx->fps = fps > 0 ? fps : 30;
ctx->bitrate_kbps = bitrate_kbps > 0 ? bitrate_kbps : 2000;
ctx->gop = gop > 0 ? gop : ctx->fps;
ctx->thread_count = thread_count > 0 ? thread_count : 1;
if (init_decoder(ctx) != 0) {
ffmpeg_hw_mjpeg_h26x_free(reinterpret_cast<FfmpegHwMjpegH26x*>(ctx));
return nullptr;
}
return reinterpret_cast<FfmpegHwMjpegH26x*>(ctx);
}
extern "C" int ffmpeg_hw_mjpeg_h26x_encode(FfmpegHwMjpegH26x* handle,
const uint8_t* data,
int len,
int64_t pts_ms,
uint8_t** out_data,
int* out_len,
int* out_keyframe) {
if (!handle || !data || len <= 0 || !out_data || !out_len || !out_keyframe) {
set_last_error("Invalid parameters for encode");
return -1;
}
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
*out_data = nullptr;
*out_len = 0;
*out_keyframe = 0;
av_packet_unref(ctx->dec_pkt);
int ret = av_new_packet(ctx->dec_pkt, len);
if (ret < 0) {
set_last_error(make_err("av_new_packet failed", ret));
return -1;
}
memcpy(ctx->dec_pkt->data, data, len);
ctx->dec_pkt->size = len;
ret = avcodec_send_packet(ctx->dec_ctx, ctx->dec_pkt);
if (ret < 0) {
set_last_error(make_err("avcodec_send_packet failed", ret));
return -1;
}
while (true) {
ret = avcodec_receive_frame(ctx->dec_ctx, ctx->dec_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
}
if (ret < 0) {
set_last_error(make_err("avcodec_receive_frame failed", ret));
return -1;
}
if (ctx->dec_frame->format != AV_PIX_FMT_DRM_PRIME) {
set_last_error("Decoder output is not DRM_PRIME");
av_frame_unref(ctx->dec_frame);
return -1;
}
if (!ctx->enc_ctx) {
if (!ctx->dec_frame->hw_frames_ctx) {
set_last_error("Decoder returned frame without hw_frames_ctx");
av_frame_unref(ctx->dec_frame);
return -1;
}
if (init_encoder(ctx, ctx->dec_frame->hw_frames_ctx) != 0) {
av_frame_unref(ctx->dec_frame);
return -1;
}
}
AVFrame *send_frame = ctx->dec_frame;
AVFrame *tmp = nullptr;
if (ctx->force_keyframe) {
tmp = av_frame_clone(send_frame);
if (tmp) {
tmp->pict_type = AV_PICTURE_TYPE_I;
send_frame = tmp;
}
ctx->force_keyframe = false;
}
// Apply visible size crop if aligned buffer is larger than display size
if (ctx->aligned_width > 0 && ctx->width > 0 && ctx->aligned_width > ctx->width) {
send_frame->crop_right = ctx->aligned_width - ctx->width;
}
if (ctx->aligned_height > 0 && ctx->height > 0 && ctx->aligned_height > ctx->height) {
send_frame->crop_bottom = ctx->aligned_height - ctx->height;
}
send_frame->pts = pts_ms; // time_base is ms
ret = avcodec_send_frame(ctx->enc_ctx, send_frame);
if (tmp) {
av_frame_free(&tmp);
}
if (ret < 0) {
std::string detail = "avcodec_send_frame failed";
if (send_frame) {
detail += " frame_fmt=";
detail += pix_fmt_name(static_cast<AVPixelFormat>(send_frame->format));
detail += " w=" + std::to_string(send_frame->width);
detail += " h=" + std::to_string(send_frame->height);
if (send_frame->format == AV_PIX_FMT_DRM_PRIME && send_frame->data[0]) {
const AVDRMFrameDescriptor *drm =
reinterpret_cast<const AVDRMFrameDescriptor *>(send_frame->data[0]);
if (drm && drm->layers[0].format) {
detail += " drm_fmt=0x";
char buf[9];
snprintf(buf, sizeof(buf), "%08x", drm->layers[0].format);
detail += buf;
}
if (drm && drm->objects[0].format_modifier) {
detail += " drm_mod=0x";
char buf[17];
snprintf(buf, sizeof(buf), "%016llx",
(unsigned long long)drm->objects[0].format_modifier);
detail += buf;
}
}
}
set_last_error(make_err(detail, ret));
av_frame_unref(ctx->dec_frame);
return -1;
}
av_packet_unref(ctx->enc_pkt);
ret = avcodec_receive_packet(ctx->enc_ctx, ctx->enc_pkt);
if (ret == AVERROR(EAGAIN)) {
av_frame_unref(ctx->dec_frame);
return 0;
}
if (ret < 0) {
set_last_error(make_err("avcodec_receive_packet failed", ret));
av_frame_unref(ctx->dec_frame);
return -1;
}
if (ctx->enc_pkt->size > 0) {
uint8_t *buf = (uint8_t*)malloc(ctx->enc_pkt->size);
if (!buf) {
set_last_error("malloc for output packet failed");
av_packet_unref(ctx->enc_pkt);
av_frame_unref(ctx->dec_frame);
return -1;
}
memcpy(buf, ctx->enc_pkt->data, ctx->enc_pkt->size);
*out_data = buf;
*out_len = ctx->enc_pkt->size;
*out_keyframe = (ctx->enc_pkt->flags & AV_PKT_FLAG_KEY) ? 1 : 0;
av_packet_unref(ctx->enc_pkt);
av_frame_unref(ctx->dec_frame);
return 1;
}
av_frame_unref(ctx->dec_frame);
}
}
extern "C" int ffmpeg_hw_mjpeg_h26x_reconfigure(FfmpegHwMjpegH26x* handle,
int bitrate_kbps,
int gop) {
if (!handle) {
set_last_error("Invalid handle for reconfigure");
return -1;
}
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
if (!ctx->enc_ctx || !ctx->hw_frames_ctx) {
set_last_error("Encoder not initialized for reconfigure");
return -1;
}
ctx->bitrate_kbps = bitrate_kbps > 0 ? bitrate_kbps : ctx->bitrate_kbps;
ctx->gop = gop > 0 ? gop : ctx->gop;
AVBufferRef *frames_ref = ctx->hw_frames_ctx ? av_buffer_ref(ctx->hw_frames_ctx) : nullptr;
free_encoder(ctx);
if (init_encoder(ctx, frames_ref) != 0) {
if (frames_ref) av_buffer_unref(&frames_ref);
return -1;
}
if (frames_ref) av_buffer_unref(&frames_ref);
return 0;
}
extern "C" int ffmpeg_hw_mjpeg_h26x_request_keyframe(FfmpegHwMjpegH26x* handle) {
if (!handle) {
set_last_error("Invalid handle for request_keyframe");
return -1;
}
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
ctx->force_keyframe = true;
return 0;
}
extern "C" void ffmpeg_hw_mjpeg_h26x_free(FfmpegHwMjpegH26x* handle) {
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
if (!ctx) return;
if (ctx->dec_pkt) av_packet_free(&ctx->dec_pkt);
if (ctx->dec_frame) av_frame_free(&ctx->dec_frame);
if (ctx->enc_pkt) av_packet_free(&ctx->enc_pkt);
if (ctx->dec_ctx) avcodec_free_context(&ctx->dec_ctx);
free_encoder(ctx);
if (ctx->hw_device_ctx) av_buffer_unref(&ctx->hw_device_ctx);
delete ctx;
}
extern "C" void ffmpeg_hw_packet_free(uint8_t* data) {
if (data) {
free(data);
}
}
extern "C" const char* ffmpeg_hw_last_error(void) {
return g_last_error.c_str();
}