diff --git a/Cargo.toml b/Cargo.toml index aa8d66ec..7d5589a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "one-kvm" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["SilentWind"] description = "A open and lightweight IP-KVM solution written in Rust" diff --git a/build/Dockerfile.runtime b/build/Dockerfile.runtime index 44617adf..ccdc1658 100644 --- a/build/Dockerfile.runtime +++ b/build/Dockerfile.runtime @@ -39,7 +39,7 @@ RUN apt-get update && \ COPY --chmod=755 init.sh /init.sh # Copy binaries (these are placed by the build script) -COPY --chmod=755 one-kvm ttyd gostc easytier-core /usr/bin/ +COPY --chmod=755 one-kvm ttyd /usr/bin/ # Copy ventoy resources if they exist COPY ventoy/ /etc/one-kvm/ventoy/ diff --git a/build/Dockerfile.runtime-full b/build/Dockerfile.runtime-full new file mode 100644 index 00000000..32428305 --- /dev/null +++ b/build/Dockerfile.runtime-full @@ -0,0 +1,48 @@ +# One-KVM Runtime Image (full) +# This Dockerfile only packages pre-compiled binaries (no compilation) +# Used after cross-compiling with `cross build` +# Using Debian 11 for maximum compatibility (GLIBC 2.31) + +ARG TARGETPLATFORM=linux/amd64 + +FROM debian:11-slim + +ARG TARGETPLATFORM + +# Install runtime dependencies in a single layer +# All codec libraries (libx264, libx265, libopus) are now statically linked +# Only hardware acceleration drivers and core system libraries remain dynamic +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + # Core runtime (all platforms) - no codec libs needed + ca-certificates \ + libudev1 \ + libasound2 \ + # v4l2 is handled by kernel, minimal userspace needed + libv4l-0 \ + && \ + # Platform-specific hardware acceleration + if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + apt-get install -y --no-install-recommends \ + libva2 libva-drm2 libva-x11-2 libx11-6 libxcb1 libxau6 libxdmcp6 libmfx1; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + apt-get install -y --no-install-recommends \ + libdrm2 libva2; \ + elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ + apt-get install -y --no-install-recommends \ + libdrm2 libva2; \ + fi && \ + rm -rf /var/lib/apt/lists/* && \ + mkdir -p /etc/one-kvm/ventoy + +# Copy init script +COPY --chmod=755 init.sh /init.sh + +# Copy binaries (these are placed by the build script) +COPY --chmod=755 one-kvm ttyd gostc easytier-core /usr/bin/ + +# Copy ventoy resources if they exist +COPY ventoy/ /etc/one-kvm/ventoy/ + +# Entrypoint +CMD ["/init.sh"] diff --git a/build/package-docker.sh b/build/package-docker.sh index ceb1217d..94c9ba82 100755 --- a/build/package-docker.sh +++ b/build/package-docker.sh @@ -25,11 +25,13 @@ echo_error() { echo -e "${RED}[ERROR]${NC} $1"; } # Configuration REGISTRY="${REGISTRY:-}" # e.g., docker.io/username or ghcr.io/username -IMAGE_NAME="${IMAGE_NAME:-one-kvm}" +IMAGE_NAME="${IMAGE_NAME:-}" TAG="${TAG:-latest}" +VARIANT="${VARIANT:-minimal}" +INCLUDE_THIRD_PARTY=false SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -STAGING_DIR="$PROJECT_ROOT/build-staging" +BASE_STAGING_DIR="$PROJECT_ROOT/build-staging" # Full image name with registry get_full_image_name() { @@ -77,6 +79,18 @@ while [[ $# -gt 0 ]]; do REGISTRY="$2" shift 2 ;; + --image-name) + IMAGE_NAME="$2" + shift 2 + ;; + --variant) + VARIANT="$2" + shift 2 + ;; + --full) + VARIANT="full" + shift + ;; --build) BUILD_BINARY=true shift @@ -91,9 +105,12 @@ while [[ $# -gt 0 ]]; do echo " Use comma to specify multiple: linux/amd64,linux/arm64" echo " Default: $DEFAULT_PLATFORM" echo " --registry REGISTRY Container registry (e.g., docker.io/user, ghcr.io/user)" + echo " --image-name NAME Override image name (default: one-kvm or one-kvm-full)" echo " --push Push image to registry" echo " --load Load image to local Docker (single platform only)" echo " --tag TAG Image tag (default: latest)" + echo " --variant VARIANT Image variant: minimal or full (default: minimal)" + echo " --full Shortcut for --variant full" echo " --build Also build the binary with cross (optional)" echo " --help Show this help" echo "" @@ -101,6 +118,9 @@ while [[ $# -gt 0 ]]; do echo " # Build for current platform and load locally" echo " $0 --platform linux/arm64 --load" echo "" + echo " # Build full image (includes gostc + easytier)" + echo " $0 --variant full --platform linux/arm64 --load" + echo "" echo " # Build and push single platform" echo " $0 --platform linux/arm64 --registry docker.io/user --push" echo "" @@ -115,6 +135,28 @@ while [[ $# -gt 0 ]]; do esac done +# Normalize variant and image name +case "$VARIANT" in + minimal) + INCLUDE_THIRD_PARTY=false + ;; + full) + INCLUDE_THIRD_PARTY=true + ;; + *) + echo_error "Unknown variant: $VARIANT (expected: minimal or full)" + exit 1 + ;; +esac + +if [ -z "$IMAGE_NAME" ]; then + if [ "$VARIANT" = "full" ]; then + IMAGE_NAME="one-kvm-full" + else + IMAGE_NAME="one-kvm" + fi +fi + # Default platform if [ -z "$PLATFORMS" ]; then PLATFORMS="$DEFAULT_PLATFORM" @@ -176,21 +218,23 @@ download_tools() { chmod +x "$staging/ttyd" fi - # gostc - if [ ! -f "$staging/gostc" ]; then - curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz - tar -xzf /tmp/gostc.tar.gz -C "$staging" - chmod +x "$staging/gostc" - rm /tmp/gostc.tar.gz - fi + if [ "$INCLUDE_THIRD_PARTY" = true ]; then + # gostc + if [ ! -f "$staging/gostc" ]; then + curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz + tar -xzf /tmp/gostc.tar.gz -C "$staging" + chmod +x "$staging/gostc" + rm /tmp/gostc.tar.gz + fi - # easytier - if [ ! -f "$staging/easytier-core" ]; then - curl -fsSL "$EASYTIER_URL" -o /tmp/easytier.zip - unzip -o /tmp/easytier.zip -d /tmp/easytier - cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core" - chmod +x "$staging/easytier-core" - rm -rf /tmp/easytier.zip /tmp/easytier + # easytier + if [ ! -f "$staging/easytier-core" ]; then + curl -fsSL "$EASYTIER_URL" -o /tmp/easytier.zip + unzip -o /tmp/easytier.zip -d /tmp/easytier + cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core" + chmod +x "$staging/easytier-core" + rm -rf /tmp/easytier.zip /tmp/easytier + fi fi } @@ -198,13 +242,14 @@ download_tools() { build_for_platform() { local platform="$1" local target=$(platform_to_target "$platform") - local staging="$STAGING_DIR/$target" + local staging="$BASE_STAGING_DIR/$VARIANT/$target" echo_info "==========================================" echo_info "Processing: $platform ($target)" echo_info "==========================================" # Create staging directory + rm -rf "$staging" mkdir -p "$staging/ventoy" # Build binary if requested @@ -252,7 +297,11 @@ build_for_platform() { fi # Copy Dockerfile - cp "$PROJECT_ROOT/build/Dockerfile.runtime" "$staging/Dockerfile" + local dockerfile="$PROJECT_ROOT/build/Dockerfile.runtime" + if [ "$INCLUDE_THIRD_PARTY" = true ]; then + dockerfile="$PROJECT_ROOT/build/Dockerfile.runtime-full" + fi + cp "$dockerfile" "$staging/Dockerfile" # Build Docker image echo_info "Building Docker image..." @@ -292,6 +341,7 @@ main() { echo_info "One-KVM Docker Image Builder" echo_info "Image: $full_image:$TAG" + echo_info "Variant: $VARIANT" echo_info "Platforms: $PLATFORMS" if [ -n "$REGISTRY" ]; then echo_info "Registry: $REGISTRY" diff --git a/libs/hwcodec/build.rs b/libs/hwcodec/build.rs index 7da2da2f..ba799ba8 100644 --- a/libs/hwcodec/build.rs +++ b/libs/hwcodec/build.rs @@ -419,10 +419,10 @@ mod ffmpeg { builder.include(rkrga_dir.join("im2d_api")); } } - builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h264.cpp")); + builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp")); } else { println!( - "cargo:info=Skipping ffmpeg_hw_mjpeg_h264.cpp (RKMPP) for arch {}", + "cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}", target_arch ); } diff --git a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h index dc7179a5..ac4cba21 100644 --- a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h +++ b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_ffi.h @@ -6,9 +6,11 @@ extern "C" { #endif -typedef struct FfmpegHwMjpegH264 FfmpegHwMjpegH264; +// MJPEG -> H26x (H.264 / H.265) hardware pipeline +typedef struct FfmpegHwMjpegH26x FfmpegHwMjpegH26x; -FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name, +// Create a new MJPEG -> H26x pipeline. +FfmpegHwMjpegH26x* ffmpeg_hw_mjpeg_h26x_new(const char* dec_name, const char* enc_name, int width, int height, @@ -17,7 +19,8 @@ FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name, int gop, int thread_count); -int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* ctx, +// 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, @@ -25,16 +28,21 @@ int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* ctx, int* out_len, int* out_keyframe); -int ffmpeg_hw_mjpeg_h264_reconfigure(FfmpegHwMjpegH264* ctx, +// Reconfigure bitrate/gop (best-effort, may recreate encoder internally). +int ffmpeg_hw_mjpeg_h26x_reconfigure(FfmpegHwMjpegH26x* ctx, int bitrate_kbps, int gop); -int ffmpeg_hw_mjpeg_h264_request_keyframe(FfmpegHwMjpegH264* ctx); +// Request next frame to be a keyframe. +int ffmpeg_hw_mjpeg_h26x_request_keyframe(FfmpegHwMjpegH26x* ctx); -void ffmpeg_hw_mjpeg_h264_free(FfmpegHwMjpegH264* 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 diff --git a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h264.cpp b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h26x.cpp similarity index 85% rename from libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h264.cpp rename to libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h26x.cpp index cbdf3736..d19aafca 100644 --- a/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h264.cpp +++ b/libs/hwcodec/cpp/ffmpeg_hw/ffmpeg_hw_mjpeg_h26x.cpp @@ -35,7 +35,7 @@ static const char* pix_fmt_name(AVPixelFormat fmt) { return name ? name : "unknown"; } -struct FfmpegHwMjpegH264Ctx { +struct FfmpegHwMjpegH26xCtx { AVCodecContext *dec_ctx = nullptr; AVCodecContext *enc_ctx = nullptr; AVPacket *dec_pkt = nullptr; @@ -48,6 +48,8 @@ struct FfmpegHwMjpegH264Ctx { 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; @@ -57,7 +59,7 @@ struct FfmpegHwMjpegH264Ctx { static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *pix_fmts) { - auto *self = reinterpret_cast(ctx->opaque); + auto *self = reinterpret_cast(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++) { @@ -69,7 +71,7 @@ static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, return pix_fmts[0]; } -static int init_decoder(FfmpegHwMjpegH264Ctx *ctx) { +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); @@ -127,7 +129,7 @@ static int init_decoder(FfmpegHwMjpegH264Ctx *ctx) { return 0; } -static int init_encoder(FfmpegHwMjpegH264Ctx *ctx, AVBufferRef *frames_ctx) { +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); @@ -142,6 +144,10 @@ static int init_encoder(FfmpegHwMjpegH264Ctx *ctx, AVBufferRef *frames_ctx) { 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; @@ -155,8 +161,14 @@ static int init_encoder(FfmpegHwMjpegH264Ctx *ctx, AVBufferRef *frames_ctx) { if (hwfc) { ctx->enc_ctx->pix_fmt = static_cast(hwfc->format); ctx->enc_ctx->sw_pix_fmt = static_cast(hwfc->sw_format); - if (hwfc->width > 0) ctx->enc_ctx->width = hwfc->width; - if (hwfc->height > 0) ctx->enc_ctx->height = hwfc->height; + 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); @@ -167,7 +179,11 @@ static int init_encoder(FfmpegHwMjpegH264Ctx *ctx, AVBufferRef *frames_ctx) { AVDictionary *opts = nullptr; av_dict_set(&opts, "rc_mode", "CBR", 0); - av_dict_set(&opts, "profile", "high", 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); @@ -195,7 +211,7 @@ static int init_encoder(FfmpegHwMjpegH264Ctx *ctx, AVBufferRef *frames_ctx) { return 0; } -static void free_encoder(FfmpegHwMjpegH264Ctx *ctx) { +static void free_encoder(FfmpegHwMjpegH26xCtx *ctx) { if (ctx->enc_ctx) { avcodec_free_context(&ctx->enc_ctx); ctx->enc_ctx = nullptr; @@ -208,7 +224,7 @@ static void free_encoder(FfmpegHwMjpegH264Ctx *ctx) { } // namespace -extern "C" FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name, +extern "C" FfmpegHwMjpegH26x* ffmpeg_hw_mjpeg_h26x_new(const char* dec_name, const char* enc_name, int width, int height, @@ -217,11 +233,11 @@ extern "C" FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name, int gop, int thread_count) { if (!dec_name || !enc_name || width <= 0 || height <= 0) { - set_last_error("Invalid parameters for ffmpeg_hw_mjpeg_h264_new"); + set_last_error("Invalid parameters for ffmpeg_hw_mjpeg_h26x_new"); return nullptr; } - auto *ctx = new FfmpegHwMjpegH264Ctx(); + auto *ctx = new FfmpegHwMjpegH26xCtx(); ctx->dec_name = dec_name; ctx->enc_name = enc_name; ctx->width = width; @@ -232,14 +248,14 @@ extern "C" FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name, ctx->thread_count = thread_count > 0 ? thread_count : 1; if (init_decoder(ctx) != 0) { - ffmpeg_hw_mjpeg_h264_free(reinterpret_cast(ctx)); + ffmpeg_hw_mjpeg_h26x_free(reinterpret_cast(ctx)); return nullptr; } - return reinterpret_cast(ctx); + return reinterpret_cast(ctx); } -extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* handle, +extern "C" int ffmpeg_hw_mjpeg_h26x_encode(FfmpegHwMjpegH26x* handle, const uint8_t* data, int len, int64_t pts_ms, @@ -251,7 +267,7 @@ extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* handle, return -1; } - auto *ctx = reinterpret_cast(handle); + auto *ctx = reinterpret_cast(handle); *out_data = nullptr; *out_len = 0; *out_keyframe = 0; @@ -310,6 +326,14 @@ extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* handle, 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); @@ -379,14 +403,14 @@ extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* handle, } } -extern "C" int ffmpeg_hw_mjpeg_h264_reconfigure(FfmpegHwMjpegH264* handle, +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(handle); + auto *ctx = reinterpret_cast(handle); if (!ctx->enc_ctx || !ctx->hw_frames_ctx) { set_last_error("Encoder not initialized for reconfigure"); return -1; @@ -407,18 +431,18 @@ extern "C" int ffmpeg_hw_mjpeg_h264_reconfigure(FfmpegHwMjpegH264* handle, return 0; } -extern "C" int ffmpeg_hw_mjpeg_h264_request_keyframe(FfmpegHwMjpegH264* handle) { +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(handle); + auto *ctx = reinterpret_cast(handle); ctx->force_keyframe = true; return 0; } -extern "C" void ffmpeg_hw_mjpeg_h264_free(FfmpegHwMjpegH264* handle) { - auto *ctx = reinterpret_cast(handle); +extern "C" void ffmpeg_hw_mjpeg_h26x_free(FfmpegHwMjpegH26x* handle) { + auto *ctx = reinterpret_cast(handle); if (!ctx) return; if (ctx->dec_pkt) av_packet_free(&ctx->dec_pkt); diff --git a/libs/hwcodec/src/ffmpeg_hw/mod.rs b/libs/hwcodec/src/ffmpeg_hw/mod.rs index 737a7d1b..222c9d14 100644 --- a/libs/hwcodec/src/ffmpeg_hw/mod.rs +++ b/libs/hwcodec/src/ffmpeg_hw/mod.rs @@ -10,7 +10,7 @@ use std::{ include!(concat!(env!("OUT_DIR"), "/ffmpeg_hw_ffi.rs")); #[derive(Debug, Clone)] -pub struct HwMjpegH264Config { +pub struct HwMjpegH26xConfig { pub decoder: String, pub encoder: String, pub width: i32, @@ -21,19 +21,19 @@ pub struct HwMjpegH264Config { pub thread_count: i32, } -pub struct HwMjpegH264Pipeline { - ctx: *mut FfmpegHwMjpegH264, - config: HwMjpegH264Config, +pub struct HwMjpegH26xPipeline { + ctx: *mut FfmpegHwMjpegH26x, + config: HwMjpegH26xConfig, } -unsafe impl Send for HwMjpegH264Pipeline {} +unsafe impl Send for HwMjpegH26xPipeline {} -impl HwMjpegH264Pipeline { - pub fn new(config: HwMjpegH264Config) -> Result { +impl HwMjpegH26xPipeline { + pub fn new(config: HwMjpegH26xConfig) -> Result { unsafe { let dec = CString::new(config.decoder.as_str()).map_err(|_| "decoder name invalid".to_string())?; let enc = CString::new(config.encoder.as_str()).map_err(|_| "encoder name invalid".to_string())?; - let ctx = ffmpeg_hw_mjpeg_h264_new( + let ctx = ffmpeg_hw_mjpeg_h26x_new( dec.as_ptr(), enc.as_ptr(), config.width, @@ -55,7 +55,7 @@ impl HwMjpegH264Pipeline { let mut out_data: *mut u8 = std::ptr::null_mut(); let mut out_len: c_int = 0; let mut out_key: c_int = 0; - let ret = ffmpeg_hw_mjpeg_h264_encode( + let ret = ffmpeg_hw_mjpeg_h26x_encode( self.ctx, data.as_ptr(), data.len() as c_int, @@ -80,7 +80,7 @@ impl HwMjpegH264Pipeline { pub fn reconfigure(&mut self, bitrate_kbps: i32, gop: i32) -> Result<(), String> { unsafe { - let ret = ffmpeg_hw_mjpeg_h264_reconfigure(self.ctx, bitrate_kbps, gop); + let ret = ffmpeg_hw_mjpeg_h26x_reconfigure(self.ctx, bitrate_kbps, gop); if ret != 0 { return Err(last_error_message()); } @@ -92,15 +92,15 @@ impl HwMjpegH264Pipeline { pub fn request_keyframe(&mut self) { unsafe { - let _ = ffmpeg_hw_mjpeg_h264_request_keyframe(self.ctx); + let _ = ffmpeg_hw_mjpeg_h26x_request_keyframe(self.ctx); } } } -impl Drop for HwMjpegH264Pipeline { +impl Drop for HwMjpegH26xPipeline { fn drop(&mut self) { unsafe { - ffmpeg_hw_mjpeg_h264_free(self.ctx); + ffmpeg_hw_mjpeg_h26x_free(self.ctx); } self.ctx = std::ptr::null_mut(); } diff --git a/src/rustdesk/connection.rs b/src/rustdesk/connection.rs index 6eda2206..7c3b3fbf 100644 --- a/src/rustdesk/connection.rs +++ b/src/rustdesk/connection.rs @@ -623,7 +623,7 @@ impl Connection { self.negotiated_codec = Some(negotiated); info!("Negotiated video codec: {:?}", negotiated); - let response = self.create_login_response(true); + let response = self.create_login_response(true).await; let response_bytes = response .write_to_bytes() .map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?; @@ -673,7 +673,11 @@ impl Connection { Some(misc::Union::RefreshVideo(refresh)) => { if *refresh { debug!("Video refresh requested"); - // TODO: Request keyframe from encoder + if let Some(ref video_manager) = self.video_manager { + if let Err(e) = video_manager.request_keyframe().await { + warn!("Failed to request keyframe: {}", e); + } + } } } Some(misc::Union::VideoReceived(received)) => { @@ -1064,7 +1068,7 @@ impl Connection { } /// Create login response with dynamically detected encoder capabilities - fn create_login_response(&self, success: bool) -> HbbMessage { + async fn create_login_response(&self, success: bool) -> HbbMessage { if success { // Dynamically detect available encoders let registry = EncoderRegistry::global(); @@ -1080,11 +1084,21 @@ impl Connection { h264_available, h265_available, vp8_available, vp9_available ); + let mut display_width = self.screen_width; + let mut display_height = self.screen_height; + if let Some(ref video_manager) = self.video_manager { + let video_info = video_manager.get_video_info().await; + if let Some((width, height)) = video_info.resolution { + display_width = width; + display_height = height; + } + } + let mut display_info = DisplayInfo::new(); display_info.x = 0; display_info.y = 0; - display_info.width = 1920; - display_info.height = 1080; + display_info.width = display_width as i32; + display_info.height = display_height as i32; display_info.name = "KVM Display".to_string(); display_info.online = true; display_info.cursor_embedded = false; @@ -1582,6 +1596,9 @@ async fn run_video_streaming( config.bitrate_preset ); } + if let Err(e) = video_manager.request_keyframe().await { + debug!("Failed to request keyframe for connection {}: {}", conn_id, e); + } // Inner loop: receives frames from current subscription loop { diff --git a/src/rustdesk/frame_adapters.rs b/src/rustdesk/frame_adapters.rs index 14e4d321..fbee2c1e 100644 --- a/src/rustdesk/frame_adapters.rs +++ b/src/rustdesk/frame_adapters.rs @@ -42,6 +42,9 @@ pub struct VideoFrameAdapter { seq: u32, /// Timestamp offset timestamp_base: u64, + /// Cached H264 SPS/PPS (Annex B NAL without start code) + h264_sps: Option, + h264_pps: Option, } impl VideoFrameAdapter { @@ -51,6 +54,8 @@ impl VideoFrameAdapter { codec, seq: 0, timestamp_base: 0, + h264_sps: None, + h264_pps: None, } } @@ -68,6 +73,7 @@ impl VideoFrameAdapter { is_keyframe: bool, timestamp_ms: u64, ) -> Message { + let data = self.prepare_h264_frame(data, is_keyframe); // Calculate relative timestamp if self.seq == 0 { self.timestamp_base = timestamp_ms; @@ -100,6 +106,41 @@ impl VideoFrameAdapter { msg } + fn prepare_h264_frame(&mut self, data: Bytes, is_keyframe: bool) -> Bytes { + if self.codec != VideoCodec::H264 { + return data; + } + + // Parse SPS/PPS from Annex B data (without start codes) + let (sps, pps) = crate::webrtc::rtp::extract_sps_pps(&data); + let mut has_sps = false; + let mut has_pps = false; + + if let Some(sps) = sps { + self.h264_sps = Some(Bytes::from(sps)); + has_sps = true; + } + if let Some(pps) = pps { + self.h264_pps = Some(Bytes::from(pps)); + has_pps = true; + } + + // Inject cached SPS/PPS before IDR when missing + if is_keyframe && (!has_sps || !has_pps) { + if let (Some(ref sps), Some(ref pps)) = (self.h264_sps.as_ref(), self.h264_pps.as_ref()) { + let mut out = Vec::with_capacity(8 + sps.len() + pps.len() + data.len()); + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(sps); + out.extend_from_slice(&[0, 0, 0, 1]); + out.extend_from_slice(pps); + out.extend_from_slice(&data); + return Bytes::from(out); + } + } + + data + } + /// Convert encoded video data to RustDesk Message pub fn encode_frame(&mut self, data: &[u8], is_keyframe: bool, timestamp_ms: u64) -> Message { self.encode_frame_from_bytes(Bytes::copy_from_slice(data), is_keyframe, timestamp_ms) diff --git a/src/video/decoder/mjpeg_rkmpp.rs b/src/video/decoder/mjpeg_rkmpp.rs index c95ada1e..686a722c 100644 --- a/src/video/decoder/mjpeg_rkmpp.rs +++ b/src/video/decoder/mjpeg_rkmpp.rs @@ -2,7 +2,7 @@ use hwcodec::ffmpeg::AVPixelFormat; use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder}; -use tracing::warn; +use tracing::{info, warn}; use crate::error::{AppError, Result}; use crate::video::convert::Nv12Converter; @@ -72,6 +72,9 @@ impl MjpegRkmppDecoder { ); } } else { + if frame.pixfmt == AVPixelFormat::AV_PIX_FMT_NV16 { + info!("mjpeg_rkmpp output pixfmt NV16 on first frame; converting to NV12"); + } self.last_pixfmt = Some(frame.pixfmt); } diff --git a/src/video/decoder/mod.rs b/src/video/decoder/mod.rs index 55a1569f..32b47d91 100644 --- a/src/video/decoder/mod.rs +++ b/src/video/decoder/mod.rs @@ -2,10 +2,6 @@ //! //! This module provides video decoding capabilities. -#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] -pub mod mjpeg_rkmpp; pub mod mjpeg_turbo; -#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] -pub use mjpeg_rkmpp::MjpegRkmppDecoder; pub use mjpeg_turbo::MjpegTurboDecoder; diff --git a/src/video/shared_video_pipeline.rs b/src/video/shared_video_pipeline.rs index 013c733a..eb6c8be3 100644 --- a/src/video/shared_video_pipeline.rs +++ b/src/video/shared_video_pipeline.rs @@ -33,11 +33,9 @@ const JPEG_VALIDATE_INTERVAL: u64 = 30; use crate::error::{AppError, Result}; use crate::video::convert::{Nv12Converter, PixelConverter}; -#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] -use crate::video::decoder::MjpegRkmppDecoder; use crate::video::decoder::MjpegTurboDecoder; #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] -use hwcodec::ffmpeg_hw::{last_error_message as ffmpeg_hw_last_error, HwMjpegH264Config, HwMjpegH264Pipeline}; +use hwcodec::ffmpeg_hw::{last_error_message as ffmpeg_hw_last_error, HwMjpegH26xConfig, HwMjpegH26xPipeline}; use v4l::buffer::Type as BufferType; use v4l::io::traits::CaptureStream; use v4l::prelude::*; @@ -177,7 +175,7 @@ struct EncoderThreadState { yuv420p_converter: Option, encoder_needs_yuv420p: bool, #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] - ffmpeg_hw_pipeline: Option, + ffmpeg_hw_pipeline: Option, #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] ffmpeg_hw_enabled: bool, fps: u32, @@ -319,16 +317,12 @@ impl VideoEncoderTrait for VP9EncoderWrapper { } enum MjpegDecoderKind { - #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] - Rkmpp(MjpegRkmppDecoder), Turbo(MjpegTurboDecoder), } impl MjpegDecoderKind { fn decode(&mut self, data: &[u8]) -> Result> { match self { - #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] - MjpegDecoderKind::Rkmpp(decoder) => decoder.decode_to_nv12(data), MjpegDecoderKind::Turbo(decoder) => decoder.decode_to_rgb(data), } } @@ -513,14 +507,16 @@ impl SharedVideoPipeline { }; let is_rkmpp_encoder = selected_codec_name.contains("rkmpp"); - let is_software_encoder = selected_codec_name.contains("libx264") - || selected_codec_name.contains("libx265") - || selected_codec_name.contains("libvpx"); - #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] - if needs_mjpeg_decode && is_rkmpp_encoder && config.output_codec == VideoEncoderType::H264 { - info!("Initializing FFmpeg HW MJPEG->H264 pipeline (no fallback)"); - let hw_config = HwMjpegH264Config { + if needs_mjpeg_decode + && is_rkmpp_encoder + && matches!(config.output_codec, VideoEncoderType::H264 | VideoEncoderType::H265) + { + info!( + "Initializing FFmpeg HW MJPEG->{} pipeline (no fallback)", + config.output_codec + ); + let hw_config = HwMjpegH26xConfig { decoder: "mjpeg_rkmpp".to_string(), encoder: selected_codec_name.clone(), width: config.resolution.width as i32, @@ -530,14 +526,14 @@ impl SharedVideoPipeline { gop: config.gop_size() as i32, thread_count: 1, }; - let pipeline = HwMjpegH264Pipeline::new(hw_config).map_err(|e| { + let pipeline = HwMjpegH26xPipeline::new(hw_config).map_err(|e| { let detail = if e.is_empty() { ffmpeg_hw_last_error() } else { e }; AppError::VideoError(format!( - "FFmpeg HW MJPEG->H264 init failed: {}", - detail + "FFmpeg HW MJPEG->{} init failed: {}", + config.output_codec, detail )) })?; - info!("Using FFmpeg HW MJPEG->H264 pipeline"); + info!("Using FFmpeg HW MJPEG->{} pipeline", config.output_codec); return Ok(EncoderThreadState { encoder: None, mjpeg_decoder: None, @@ -555,35 +551,12 @@ impl SharedVideoPipeline { } let pipeline_input_format = if needs_mjpeg_decode { - if is_rkmpp_encoder { - info!( - "MJPEG input detected, using RKMPP decoder ({} -> NV12 with NV16 fallback)", - config.input_format - ); - #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] - { - let decoder = MjpegRkmppDecoder::new(config.resolution)?; - let pipeline_format = PixelFormat::Nv12; - (Some(MjpegDecoderKind::Rkmpp(decoder)), pipeline_format) - } - #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))] - { - return Err(AppError::VideoError( - "RKMPP MJPEG decode is only supported on ARM builds".to_string(), - )); - } - } else if is_software_encoder { - info!( - "MJPEG input detected, using TurboJPEG decoder ({} -> RGB24)", - config.input_format - ); - let decoder = MjpegTurboDecoder::new(config.resolution)?; - (Some(MjpegDecoderKind::Turbo(decoder)), PixelFormat::Rgb24) - } else { - return Err(AppError::VideoError( - "MJPEG input requires RKMPP or software encoder".to_string(), - )); - } + info!( + "MJPEG input detected, using TurboJPEG decoder ({} -> RGB24)", + config.input_format + ); + let decoder = MjpegTurboDecoder::new(config.resolution)?; + (Some(MjpegDecoderKind::Turbo(decoder)), PixelFormat::Rgb24) } else { (None, config.input_format) }; diff --git a/src/video/stream_manager.rs b/src/video/stream_manager.rs index 45151a92..cabe553a 100644 --- a/src/video/stream_manager.rs +++ b/src/video/stream_manager.rs @@ -794,6 +794,11 @@ impl VideoStreamManager { self.webrtc_streamer.set_bitrate_preset(preset).await } + /// Request a keyframe from the shared video pipeline + pub async fn request_keyframe(&self) -> crate::error::Result<()> { + self.webrtc_streamer.request_keyframe().await + } + /// Publish event to event bus async fn publish_event(&self, event: SystemEvent) { if let Some(ref events) = *self.events.read().await { diff --git a/src/webrtc/webrtc_streamer.rs b/src/webrtc/webrtc_streamer.rs index 0f700c51..44ed2b13 100644 --- a/src/webrtc/webrtc_streamer.rs +++ b/src/webrtc/webrtc_streamer.rs @@ -342,6 +342,18 @@ impl WebRtcStreamer { } } + /// Request the encoder to generate a keyframe on next encode + pub async fn request_keyframe(&self) -> Result<()> { + if let Some(ref pipeline) = *self.video_pipeline.read().await { + pipeline.request_keyframe().await; + Ok(()) + } else { + Err(AppError::VideoError( + "Video pipeline not running".to_string(), + )) + } + } + // === Audio Management === /// Check if audio is enabled diff --git a/web/package-lock.json b/web/package-lock.json index 2224bb35..5ef7a268 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "web", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "web", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "@vueuse/core": "^14.1.0", "class-variance-authority": "^0.7.1", diff --git a/web/package.json b/web/package.json index 8f474a59..dce5f60e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "web", "private": true, - "version": "0.1.1", + "version": "0.1.2", "type": "module", "scripts": { "dev": "vite",