mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
fix: mpp 性能优化和修复
- mjpeg-->h265 mpp 编码速度优化 - 修复 mpp 编码后的视频 rustdesk 无法解码问题 - 更新版本号为 v0.1.2
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "one-kvm"
|
name = "one-kvm"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["SilentWind"]
|
authors = ["SilentWind"]
|
||||||
description = "A open and lightweight IP-KVM solution written in Rust"
|
description = "A open and lightweight IP-KVM solution written in Rust"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ RUN apt-get update && \
|
|||||||
COPY --chmod=755 init.sh /init.sh
|
COPY --chmod=755 init.sh /init.sh
|
||||||
|
|
||||||
# Copy binaries (these are placed by the build script)
|
# 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 resources if they exist
|
||||||
COPY ventoy/ /etc/one-kvm/ventoy/
|
COPY ventoy/ /etc/one-kvm/ventoy/
|
||||||
|
|||||||
48
build/Dockerfile.runtime-full
Normal file
48
build/Dockerfile.runtime-full
Normal file
@@ -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"]
|
||||||
@@ -25,11 +25,13 @@ echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REGISTRY="${REGISTRY:-}" # e.g., docker.io/username or ghcr.io/username
|
REGISTRY="${REGISTRY:-}" # e.g., docker.io/username or ghcr.io/username
|
||||||
IMAGE_NAME="${IMAGE_NAME:-one-kvm}"
|
IMAGE_NAME="${IMAGE_NAME:-}"
|
||||||
TAG="${TAG:-latest}"
|
TAG="${TAG:-latest}"
|
||||||
|
VARIANT="${VARIANT:-minimal}"
|
||||||
|
INCLUDE_THIRD_PARTY=false
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && 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
|
# Full image name with registry
|
||||||
get_full_image_name() {
|
get_full_image_name() {
|
||||||
@@ -77,6 +79,18 @@ while [[ $# -gt 0 ]]; do
|
|||||||
REGISTRY="$2"
|
REGISTRY="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--image-name)
|
||||||
|
IMAGE_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--variant)
|
||||||
|
VARIANT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--full)
|
||||||
|
VARIANT="full"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--build)
|
--build)
|
||||||
BUILD_BINARY=true
|
BUILD_BINARY=true
|
||||||
shift
|
shift
|
||||||
@@ -91,9 +105,12 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo " Use comma to specify multiple: linux/amd64,linux/arm64"
|
echo " Use comma to specify multiple: linux/amd64,linux/arm64"
|
||||||
echo " Default: $DEFAULT_PLATFORM"
|
echo " Default: $DEFAULT_PLATFORM"
|
||||||
echo " --registry REGISTRY Container registry (e.g., docker.io/user, ghcr.io/user)"
|
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 " --push Push image to registry"
|
||||||
echo " --load Load image to local Docker (single platform only)"
|
echo " --load Load image to local Docker (single platform only)"
|
||||||
echo " --tag TAG Image tag (default: latest)"
|
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 " --build Also build the binary with cross (optional)"
|
||||||
echo " --help Show this help"
|
echo " --help Show this help"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -101,6 +118,9 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo " # Build for current platform and load locally"
|
echo " # Build for current platform and load locally"
|
||||||
echo " $0 --platform linux/arm64 --load"
|
echo " $0 --platform linux/arm64 --load"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo " # Build full image (includes gostc + easytier)"
|
||||||
|
echo " $0 --variant full --platform linux/arm64 --load"
|
||||||
|
echo ""
|
||||||
echo " # Build and push single platform"
|
echo " # Build and push single platform"
|
||||||
echo " $0 --platform linux/arm64 --registry docker.io/user --push"
|
echo " $0 --platform linux/arm64 --registry docker.io/user --push"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -115,6 +135,28 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
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
|
# Default platform
|
||||||
if [ -z "$PLATFORMS" ]; then
|
if [ -z "$PLATFORMS" ]; then
|
||||||
PLATFORMS="$DEFAULT_PLATFORM"
|
PLATFORMS="$DEFAULT_PLATFORM"
|
||||||
@@ -176,6 +218,7 @@ download_tools() {
|
|||||||
chmod +x "$staging/ttyd"
|
chmod +x "$staging/ttyd"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$INCLUDE_THIRD_PARTY" = true ]; then
|
||||||
# gostc
|
# gostc
|
||||||
if [ ! -f "$staging/gostc" ]; then
|
if [ ! -f "$staging/gostc" ]; then
|
||||||
curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz
|
curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz
|
||||||
@@ -192,19 +235,21 @@ download_tools() {
|
|||||||
chmod +x "$staging/easytier-core"
|
chmod +x "$staging/easytier-core"
|
||||||
rm -rf /tmp/easytier.zip /tmp/easytier
|
rm -rf /tmp/easytier.zip /tmp/easytier
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build and package for a single platform
|
# Build and package for a single platform
|
||||||
build_for_platform() {
|
build_for_platform() {
|
||||||
local platform="$1"
|
local platform="$1"
|
||||||
local target=$(platform_to_target "$platform")
|
local target=$(platform_to_target "$platform")
|
||||||
local staging="$STAGING_DIR/$target"
|
local staging="$BASE_STAGING_DIR/$VARIANT/$target"
|
||||||
|
|
||||||
echo_info "=========================================="
|
echo_info "=========================================="
|
||||||
echo_info "Processing: $platform ($target)"
|
echo_info "Processing: $platform ($target)"
|
||||||
echo_info "=========================================="
|
echo_info "=========================================="
|
||||||
|
|
||||||
# Create staging directory
|
# Create staging directory
|
||||||
|
rm -rf "$staging"
|
||||||
mkdir -p "$staging/ventoy"
|
mkdir -p "$staging/ventoy"
|
||||||
|
|
||||||
# Build binary if requested
|
# Build binary if requested
|
||||||
@@ -252,7 +297,11 @@ build_for_platform() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy Dockerfile
|
# 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
|
# Build Docker image
|
||||||
echo_info "Building Docker image..."
|
echo_info "Building Docker image..."
|
||||||
@@ -292,6 +341,7 @@ main() {
|
|||||||
|
|
||||||
echo_info "One-KVM Docker Image Builder"
|
echo_info "One-KVM Docker Image Builder"
|
||||||
echo_info "Image: $full_image:$TAG"
|
echo_info "Image: $full_image:$TAG"
|
||||||
|
echo_info "Variant: $VARIANT"
|
||||||
echo_info "Platforms: $PLATFORMS"
|
echo_info "Platforms: $PLATFORMS"
|
||||||
if [ -n "$REGISTRY" ]; then
|
if [ -n "$REGISTRY" ]; then
|
||||||
echo_info "Registry: $REGISTRY"
|
echo_info "Registry: $REGISTRY"
|
||||||
|
|||||||
@@ -419,10 +419,10 @@ mod ffmpeg {
|
|||||||
builder.include(rkrga_dir.join("im2d_api"));
|
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 {
|
} else {
|
||||||
println!(
|
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
|
target_arch
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#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,
|
const char* enc_name,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -17,7 +19,8 @@ FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name,
|
|||||||
int gop,
|
int gop,
|
||||||
int thread_count);
|
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,
|
const uint8_t* data,
|
||||||
int len,
|
int len,
|
||||||
int64_t pts_ms,
|
int64_t pts_ms,
|
||||||
@@ -25,16 +28,21 @@ int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* ctx,
|
|||||||
int* out_len,
|
int* out_len,
|
||||||
int* out_keyframe);
|
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 bitrate_kbps,
|
||||||
int gop);
|
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);
|
void ffmpeg_hw_packet_free(uint8_t* data);
|
||||||
|
|
||||||
|
// Get last error message (thread-local).
|
||||||
const char* ffmpeg_hw_last_error(void);
|
const char* ffmpeg_hw_last_error(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ static const char* pix_fmt_name(AVPixelFormat fmt) {
|
|||||||
return name ? name : "unknown";
|
return name ? name : "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FfmpegHwMjpegH264Ctx {
|
struct FfmpegHwMjpegH26xCtx {
|
||||||
AVCodecContext *dec_ctx = nullptr;
|
AVCodecContext *dec_ctx = nullptr;
|
||||||
AVCodecContext *enc_ctx = nullptr;
|
AVCodecContext *enc_ctx = nullptr;
|
||||||
AVPacket *dec_pkt = nullptr;
|
AVPacket *dec_pkt = nullptr;
|
||||||
@@ -48,6 +48,8 @@ struct FfmpegHwMjpegH264Ctx {
|
|||||||
std::string enc_name;
|
std::string enc_name;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
|
int aligned_width = 0;
|
||||||
|
int aligned_height = 0;
|
||||||
int fps = 30;
|
int fps = 30;
|
||||||
int bitrate_kbps = 2000;
|
int bitrate_kbps = 2000;
|
||||||
int gop = 60;
|
int gop = 60;
|
||||||
@@ -57,7 +59,7 @@ struct FfmpegHwMjpegH264Ctx {
|
|||||||
|
|
||||||
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
|
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
|
||||||
const enum AVPixelFormat *pix_fmts) {
|
const enum AVPixelFormat *pix_fmts) {
|
||||||
auto *self = reinterpret_cast<FfmpegHwMjpegH264Ctx *>(ctx->opaque);
|
auto *self = reinterpret_cast<FfmpegHwMjpegH26xCtx *>(ctx->opaque);
|
||||||
if (self && self->hw_pixfmt != AV_PIX_FMT_NONE) {
|
if (self && self->hw_pixfmt != AV_PIX_FMT_NONE) {
|
||||||
const enum AVPixelFormat *p;
|
const enum AVPixelFormat *p;
|
||||||
for (p = pix_fmts; *p != AV_PIX_FMT_NONE; 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];
|
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());
|
const AVCodec *dec = avcodec_find_decoder_by_name(ctx->dec_name.c_str());
|
||||||
if (!dec) {
|
if (!dec) {
|
||||||
set_last_error("Decoder not found: " + ctx->dec_name);
|
set_last_error("Decoder not found: " + ctx->dec_name);
|
||||||
@@ -127,7 +129,7 @@ static int init_decoder(FfmpegHwMjpegH264Ctx *ctx) {
|
|||||||
return 0;
|
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());
|
const AVCodec *enc = avcodec_find_encoder_by_name(ctx->enc_name.c_str());
|
||||||
if (!enc) {
|
if (!enc) {
|
||||||
set_last_error("Encoder not found: " + ctx->enc_name);
|
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->width = ctx->width;
|
||||||
ctx->enc_ctx->height = ctx->height;
|
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->time_base = AVRational{1, 1000};
|
||||||
ctx->enc_ctx->framerate = AVRational{ctx->fps, 1};
|
ctx->enc_ctx->framerate = AVRational{ctx->fps, 1};
|
||||||
ctx->enc_ctx->bit_rate = (int64_t)ctx->bitrate_kbps * 1000;
|
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) {
|
if (hwfc) {
|
||||||
ctx->enc_ctx->pix_fmt = static_cast<AVPixelFormat>(hwfc->format);
|
ctx->enc_ctx->pix_fmt = static_cast<AVPixelFormat>(hwfc->format);
|
||||||
ctx->enc_ctx->sw_pix_fmt = static_cast<AVPixelFormat>(hwfc->sw_format);
|
ctx->enc_ctx->sw_pix_fmt = static_cast<AVPixelFormat>(hwfc->sw_format);
|
||||||
if (hwfc->width > 0) ctx->enc_ctx->width = hwfc->width;
|
if (hwfc->width > 0) {
|
||||||
if (hwfc->height > 0) ctx->enc_ctx->height = hwfc->height;
|
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->hw_frames_ctx = av_buffer_ref(frames_ctx);
|
||||||
ctx->enc_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;
|
AVDictionary *opts = nullptr;
|
||||||
av_dict_set(&opts, "rc_mode", "CBR", 0);
|
av_dict_set(&opts, "rc_mode", "CBR", 0);
|
||||||
|
if (enc->id == AV_CODEC_ID_H264) {
|
||||||
av_dict_set(&opts, "profile", "high", 0);
|
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_init", 23, 0);
|
||||||
av_dict_set_int(&opts, "qp_max", 48, 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_min", 0, 0);
|
||||||
@@ -195,7 +211,7 @@ static int init_encoder(FfmpegHwMjpegH264Ctx *ctx, AVBufferRef *frames_ctx) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void free_encoder(FfmpegHwMjpegH264Ctx *ctx) {
|
static void free_encoder(FfmpegHwMjpegH26xCtx *ctx) {
|
||||||
if (ctx->enc_ctx) {
|
if (ctx->enc_ctx) {
|
||||||
avcodec_free_context(&ctx->enc_ctx);
|
avcodec_free_context(&ctx->enc_ctx);
|
||||||
ctx->enc_ctx = nullptr;
|
ctx->enc_ctx = nullptr;
|
||||||
@@ -208,7 +224,7 @@ static void free_encoder(FfmpegHwMjpegH264Ctx *ctx) {
|
|||||||
|
|
||||||
} // namespace
|
} // 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,
|
const char* enc_name,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -217,11 +233,11 @@ extern "C" FfmpegHwMjpegH264* ffmpeg_hw_mjpeg_h264_new(const char* dec_name,
|
|||||||
int gop,
|
int gop,
|
||||||
int thread_count) {
|
int thread_count) {
|
||||||
if (!dec_name || !enc_name || width <= 0 || height <= 0) {
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *ctx = new FfmpegHwMjpegH264Ctx();
|
auto *ctx = new FfmpegHwMjpegH26xCtx();
|
||||||
ctx->dec_name = dec_name;
|
ctx->dec_name = dec_name;
|
||||||
ctx->enc_name = enc_name;
|
ctx->enc_name = enc_name;
|
||||||
ctx->width = width;
|
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;
|
ctx->thread_count = thread_count > 0 ? thread_count : 1;
|
||||||
|
|
||||||
if (init_decoder(ctx) != 0) {
|
if (init_decoder(ctx) != 0) {
|
||||||
ffmpeg_hw_mjpeg_h264_free(reinterpret_cast<FfmpegHwMjpegH264*>(ctx));
|
ffmpeg_hw_mjpeg_h26x_free(reinterpret_cast<FfmpegHwMjpegH26x*>(ctx));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reinterpret_cast<FfmpegHwMjpegH264*>(ctx);
|
return reinterpret_cast<FfmpegHwMjpegH26x*>(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,
|
const uint8_t* data,
|
||||||
int len,
|
int len,
|
||||||
int64_t pts_ms,
|
int64_t pts_ms,
|
||||||
@@ -251,7 +267,7 @@ extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* handle,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *ctx = reinterpret_cast<FfmpegHwMjpegH264Ctx*>(handle);
|
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
|
||||||
*out_data = nullptr;
|
*out_data = nullptr;
|
||||||
*out_len = 0;
|
*out_len = 0;
|
||||||
*out_keyframe = 0;
|
*out_keyframe = 0;
|
||||||
@@ -310,6 +326,14 @@ extern "C" int ffmpeg_hw_mjpeg_h264_encode(FfmpegHwMjpegH264* handle,
|
|||||||
ctx->force_keyframe = false;
|
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
|
send_frame->pts = pts_ms; // time_base is ms
|
||||||
|
|
||||||
ret = avcodec_send_frame(ctx->enc_ctx, send_frame);
|
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 bitrate_kbps,
|
||||||
int gop) {
|
int gop) {
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
set_last_error("Invalid handle for reconfigure");
|
set_last_error("Invalid handle for reconfigure");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
auto *ctx = reinterpret_cast<FfmpegHwMjpegH264Ctx*>(handle);
|
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
|
||||||
if (!ctx->enc_ctx || !ctx->hw_frames_ctx) {
|
if (!ctx->enc_ctx || !ctx->hw_frames_ctx) {
|
||||||
set_last_error("Encoder not initialized for reconfigure");
|
set_last_error("Encoder not initialized for reconfigure");
|
||||||
return -1;
|
return -1;
|
||||||
@@ -407,18 +431,18 @@ extern "C" int ffmpeg_hw_mjpeg_h264_reconfigure(FfmpegHwMjpegH264* handle,
|
|||||||
return 0;
|
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) {
|
if (!handle) {
|
||||||
set_last_error("Invalid handle for request_keyframe");
|
set_last_error("Invalid handle for request_keyframe");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
auto *ctx = reinterpret_cast<FfmpegHwMjpegH264Ctx*>(handle);
|
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
|
||||||
ctx->force_keyframe = true;
|
ctx->force_keyframe = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" void ffmpeg_hw_mjpeg_h264_free(FfmpegHwMjpegH264* handle) {
|
extern "C" void ffmpeg_hw_mjpeg_h26x_free(FfmpegHwMjpegH26x* handle) {
|
||||||
auto *ctx = reinterpret_cast<FfmpegHwMjpegH264Ctx*>(handle);
|
auto *ctx = reinterpret_cast<FfmpegHwMjpegH26xCtx*>(handle);
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
if (ctx->dec_pkt) av_packet_free(&ctx->dec_pkt);
|
if (ctx->dec_pkt) av_packet_free(&ctx->dec_pkt);
|
||||||
@@ -10,7 +10,7 @@ use std::{
|
|||||||
include!(concat!(env!("OUT_DIR"), "/ffmpeg_hw_ffi.rs"));
|
include!(concat!(env!("OUT_DIR"), "/ffmpeg_hw_ffi.rs"));
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HwMjpegH264Config {
|
pub struct HwMjpegH26xConfig {
|
||||||
pub decoder: String,
|
pub decoder: String,
|
||||||
pub encoder: String,
|
pub encoder: String,
|
||||||
pub width: i32,
|
pub width: i32,
|
||||||
@@ -21,19 +21,19 @@ pub struct HwMjpegH264Config {
|
|||||||
pub thread_count: i32,
|
pub thread_count: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HwMjpegH264Pipeline {
|
pub struct HwMjpegH26xPipeline {
|
||||||
ctx: *mut FfmpegHwMjpegH264,
|
ctx: *mut FfmpegHwMjpegH26x,
|
||||||
config: HwMjpegH264Config,
|
config: HwMjpegH26xConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for HwMjpegH264Pipeline {}
|
unsafe impl Send for HwMjpegH26xPipeline {}
|
||||||
|
|
||||||
impl HwMjpegH264Pipeline {
|
impl HwMjpegH26xPipeline {
|
||||||
pub fn new(config: HwMjpegH264Config) -> Result<Self, String> {
|
pub fn new(config: HwMjpegH26xConfig) -> Result<Self, String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let dec = CString::new(config.decoder.as_str()).map_err(|_| "decoder name invalid".to_string())?;
|
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 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(),
|
dec.as_ptr(),
|
||||||
enc.as_ptr(),
|
enc.as_ptr(),
|
||||||
config.width,
|
config.width,
|
||||||
@@ -55,7 +55,7 @@ impl HwMjpegH264Pipeline {
|
|||||||
let mut out_data: *mut u8 = std::ptr::null_mut();
|
let mut out_data: *mut u8 = std::ptr::null_mut();
|
||||||
let mut out_len: c_int = 0;
|
let mut out_len: c_int = 0;
|
||||||
let mut out_key: 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,
|
self.ctx,
|
||||||
data.as_ptr(),
|
data.as_ptr(),
|
||||||
data.len() as c_int,
|
data.len() as c_int,
|
||||||
@@ -80,7 +80,7 @@ impl HwMjpegH264Pipeline {
|
|||||||
|
|
||||||
pub fn reconfigure(&mut self, bitrate_kbps: i32, gop: i32) -> Result<(), String> {
|
pub fn reconfigure(&mut self, bitrate_kbps: i32, gop: i32) -> Result<(), String> {
|
||||||
unsafe {
|
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 {
|
if ret != 0 {
|
||||||
return Err(last_error_message());
|
return Err(last_error_message());
|
||||||
}
|
}
|
||||||
@@ -92,15 +92,15 @@ impl HwMjpegH264Pipeline {
|
|||||||
|
|
||||||
pub fn request_keyframe(&mut self) {
|
pub fn request_keyframe(&mut self) {
|
||||||
unsafe {
|
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) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
ffmpeg_hw_mjpeg_h264_free(self.ctx);
|
ffmpeg_hw_mjpeg_h26x_free(self.ctx);
|
||||||
}
|
}
|
||||||
self.ctx = std::ptr::null_mut();
|
self.ctx = std::ptr::null_mut();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -623,7 +623,7 @@ impl Connection {
|
|||||||
self.negotiated_codec = Some(negotiated);
|
self.negotiated_codec = Some(negotiated);
|
||||||
info!("Negotiated video codec: {:?}", 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
|
let response_bytes = response
|
||||||
.write_to_bytes()
|
.write_to_bytes()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to encode: {}", e))?;
|
||||||
@@ -673,7 +673,11 @@ impl Connection {
|
|||||||
Some(misc::Union::RefreshVideo(refresh)) => {
|
Some(misc::Union::RefreshVideo(refresh)) => {
|
||||||
if *refresh {
|
if *refresh {
|
||||||
debug!("Video refresh requested");
|
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)) => {
|
Some(misc::Union::VideoReceived(received)) => {
|
||||||
@@ -1064,7 +1068,7 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create login response with dynamically detected encoder capabilities
|
/// 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 {
|
if success {
|
||||||
// Dynamically detect available encoders
|
// Dynamically detect available encoders
|
||||||
let registry = EncoderRegistry::global();
|
let registry = EncoderRegistry::global();
|
||||||
@@ -1080,11 +1084,21 @@ impl Connection {
|
|||||||
h264_available, h265_available, vp8_available, vp9_available
|
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();
|
let mut display_info = DisplayInfo::new();
|
||||||
display_info.x = 0;
|
display_info.x = 0;
|
||||||
display_info.y = 0;
|
display_info.y = 0;
|
||||||
display_info.width = 1920;
|
display_info.width = display_width as i32;
|
||||||
display_info.height = 1080;
|
display_info.height = display_height as i32;
|
||||||
display_info.name = "KVM Display".to_string();
|
display_info.name = "KVM Display".to_string();
|
||||||
display_info.online = true;
|
display_info.online = true;
|
||||||
display_info.cursor_embedded = false;
|
display_info.cursor_embedded = false;
|
||||||
@@ -1582,6 +1596,9 @@ async fn run_video_streaming(
|
|||||||
config.bitrate_preset
|
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
|
// Inner loop: receives frames from current subscription
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ pub struct VideoFrameAdapter {
|
|||||||
seq: u32,
|
seq: u32,
|
||||||
/// Timestamp offset
|
/// Timestamp offset
|
||||||
timestamp_base: u64,
|
timestamp_base: u64,
|
||||||
|
/// Cached H264 SPS/PPS (Annex B NAL without start code)
|
||||||
|
h264_sps: Option<Bytes>,
|
||||||
|
h264_pps: Option<Bytes>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoFrameAdapter {
|
impl VideoFrameAdapter {
|
||||||
@@ -51,6 +54,8 @@ impl VideoFrameAdapter {
|
|||||||
codec,
|
codec,
|
||||||
seq: 0,
|
seq: 0,
|
||||||
timestamp_base: 0,
|
timestamp_base: 0,
|
||||||
|
h264_sps: None,
|
||||||
|
h264_pps: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +73,7 @@ impl VideoFrameAdapter {
|
|||||||
is_keyframe: bool,
|
is_keyframe: bool,
|
||||||
timestamp_ms: u64,
|
timestamp_ms: u64,
|
||||||
) -> Message {
|
) -> Message {
|
||||||
|
let data = self.prepare_h264_frame(data, is_keyframe);
|
||||||
// Calculate relative timestamp
|
// Calculate relative timestamp
|
||||||
if self.seq == 0 {
|
if self.seq == 0 {
|
||||||
self.timestamp_base = timestamp_ms;
|
self.timestamp_base = timestamp_ms;
|
||||||
@@ -100,6 +106,41 @@ impl VideoFrameAdapter {
|
|||||||
msg
|
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
|
/// Convert encoded video data to RustDesk Message
|
||||||
pub fn encode_frame(&mut self, data: &[u8], is_keyframe: bool, timestamp_ms: u64) -> 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)
|
self.encode_frame_from_bytes(Bytes::copy_from_slice(data), is_keyframe, timestamp_ms)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use hwcodec::ffmpeg::AVPixelFormat;
|
use hwcodec::ffmpeg::AVPixelFormat;
|
||||||
use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
|
use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
|
||||||
use tracing::warn;
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
use crate::video::convert::Nv12Converter;
|
use crate::video::convert::Nv12Converter;
|
||||||
@@ -72,6 +72,9 @@ impl MjpegRkmppDecoder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
self.last_pixfmt = Some(frame.pixfmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
//!
|
//!
|
||||||
//! This module provides video decoding capabilities.
|
//! This module provides video decoding capabilities.
|
||||||
|
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
|
||||||
pub mod mjpeg_rkmpp;
|
|
||||||
pub mod mjpeg_turbo;
|
pub mod mjpeg_turbo;
|
||||||
|
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
|
||||||
pub use mjpeg_rkmpp::MjpegRkmppDecoder;
|
|
||||||
pub use mjpeg_turbo::MjpegTurboDecoder;
|
pub use mjpeg_turbo::MjpegTurboDecoder;
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ const JPEG_VALIDATE_INTERVAL: u64 = 30;
|
|||||||
|
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
use crate::video::convert::{Nv12Converter, PixelConverter};
|
use crate::video::convert::{Nv12Converter, PixelConverter};
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
|
||||||
use crate::video::decoder::MjpegRkmppDecoder;
|
|
||||||
use crate::video::decoder::MjpegTurboDecoder;
|
use crate::video::decoder::MjpegTurboDecoder;
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
#[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::buffer::Type as BufferType;
|
||||||
use v4l::io::traits::CaptureStream;
|
use v4l::io::traits::CaptureStream;
|
||||||
use v4l::prelude::*;
|
use v4l::prelude::*;
|
||||||
@@ -177,7 +175,7 @@ struct EncoderThreadState {
|
|||||||
yuv420p_converter: Option<PixelConverter>,
|
yuv420p_converter: Option<PixelConverter>,
|
||||||
encoder_needs_yuv420p: bool,
|
encoder_needs_yuv420p: bool,
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||||
ffmpeg_hw_pipeline: Option<HwMjpegH264Pipeline>,
|
ffmpeg_hw_pipeline: Option<HwMjpegH26xPipeline>,
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||||
ffmpeg_hw_enabled: bool,
|
ffmpeg_hw_enabled: bool,
|
||||||
fps: u32,
|
fps: u32,
|
||||||
@@ -319,16 +317,12 @@ impl VideoEncoderTrait for VP9EncoderWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum MjpegDecoderKind {
|
enum MjpegDecoderKind {
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
|
||||||
Rkmpp(MjpegRkmppDecoder),
|
|
||||||
Turbo(MjpegTurboDecoder),
|
Turbo(MjpegTurboDecoder),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MjpegDecoderKind {
|
impl MjpegDecoderKind {
|
||||||
fn decode(&mut self, data: &[u8]) -> Result<Vec<u8>> {
|
fn decode(&mut self, data: &[u8]) -> Result<Vec<u8>> {
|
||||||
match self {
|
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),
|
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_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"))]
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||||
if needs_mjpeg_decode && is_rkmpp_encoder && config.output_codec == VideoEncoderType::H264 {
|
if needs_mjpeg_decode
|
||||||
info!("Initializing FFmpeg HW MJPEG->H264 pipeline (no fallback)");
|
&& is_rkmpp_encoder
|
||||||
let hw_config = HwMjpegH264Config {
|
&& 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(),
|
decoder: "mjpeg_rkmpp".to_string(),
|
||||||
encoder: selected_codec_name.clone(),
|
encoder: selected_codec_name.clone(),
|
||||||
width: config.resolution.width as i32,
|
width: config.resolution.width as i32,
|
||||||
@@ -530,14 +526,14 @@ impl SharedVideoPipeline {
|
|||||||
gop: config.gop_size() as i32,
|
gop: config.gop_size() as i32,
|
||||||
thread_count: 1,
|
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 };
|
let detail = if e.is_empty() { ffmpeg_hw_last_error() } else { e };
|
||||||
AppError::VideoError(format!(
|
AppError::VideoError(format!(
|
||||||
"FFmpeg HW MJPEG->H264 init failed: {}",
|
"FFmpeg HW MJPEG->{} init failed: {}",
|
||||||
detail
|
config.output_codec, detail
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
info!("Using FFmpeg HW MJPEG->H264 pipeline");
|
info!("Using FFmpeg HW MJPEG->{} pipeline", config.output_codec);
|
||||||
return Ok(EncoderThreadState {
|
return Ok(EncoderThreadState {
|
||||||
encoder: None,
|
encoder: None,
|
||||||
mjpeg_decoder: None,
|
mjpeg_decoder: None,
|
||||||
@@ -555,35 +551,12 @@ impl SharedVideoPipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let pipeline_input_format = if needs_mjpeg_decode {
|
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!(
|
info!(
|
||||||
"MJPEG input detected, using TurboJPEG decoder ({} -> RGB24)",
|
"MJPEG input detected, using TurboJPEG decoder ({} -> RGB24)",
|
||||||
config.input_format
|
config.input_format
|
||||||
);
|
);
|
||||||
let decoder = MjpegTurboDecoder::new(config.resolution)?;
|
let decoder = MjpegTurboDecoder::new(config.resolution)?;
|
||||||
(Some(MjpegDecoderKind::Turbo(decoder)), PixelFormat::Rgb24)
|
(Some(MjpegDecoderKind::Turbo(decoder)), PixelFormat::Rgb24)
|
||||||
} else {
|
|
||||||
return Err(AppError::VideoError(
|
|
||||||
"MJPEG input requires RKMPP or software encoder".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
(None, config.input_format)
|
(None, config.input_format)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -794,6 +794,11 @@ impl VideoStreamManager {
|
|||||||
self.webrtc_streamer.set_bitrate_preset(preset).await
|
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
|
/// Publish event to event bus
|
||||||
async fn publish_event(&self, event: SystemEvent) {
|
async fn publish_event(&self, event: SystemEvent) {
|
||||||
if let Some(ref events) = *self.events.read().await {
|
if let Some(ref events) = *self.events.read().await {
|
||||||
|
|||||||
@@ -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 ===
|
// === Audio Management ===
|
||||||
|
|
||||||
/// Check if audio is enabled
|
/// Check if audio is enabled
|
||||||
|
|||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user