diff --git a/libs/hwcodec/cpp/common/util.cpp b/libs/hwcodec/cpp/common/util.cpp index 61877e76..b0aa17d7 100644 --- a/libs/hwcodec/cpp/common/util.cpp +++ b/libs/hwcodec/cpp/common/util.cpp @@ -15,10 +15,41 @@ extern "C" { #define LOG_MODULE "UTIL" #include "log.h" +namespace { + +// Helper function: check if encoder is software H264 (libx264) +bool is_software_h264(const std::string &name) { + if (name != "h264" && name != "libx264") return false; + // Exclude all hardware encoders + static const char* hw_suffixes[] = { + "nvenc", "amf", "qsv", "vaapi", "rkmpp", + "v4l2m2m", "videotoolbox", "mediacodec", "_mf" + }; + for (const auto& suffix : hw_suffixes) { + if (name.find(suffix) != std::string::npos) return false; + } + return true; +} + +// Helper function: check if encoder is software HEVC (libx265) +bool is_software_hevc(const std::string &name) { + if (name != "hevc" && name != "libx265") return false; + static const char* hw_suffixes[] = { + "nvenc", "amf", "qsv", "vaapi", "rkmpp", + "v4l2m2m", "videotoolbox", "mediacodec", "_mf" + }; + for (const auto& suffix : hw_suffixes) { + if (name.find(suffix) != std::string::npos) return false; + } + return true; +} + +} // anonymous namespace + namespace util_encode { void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs, - int gop, int fps) { + int gop, int fps, int thread_count) { c->has_b_frames = 0; c->max_b_frames = 0; if (gop > 0 && gop < std::numeric_limits::max()) { @@ -48,9 +79,19 @@ void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs, c->framerate = av_make_q(fps, 1); c->flags |= AV_CODEC_FLAG2_LOCAL_HEADER; c->flags |= AV_CODEC_FLAG_LOW_DELAY; - c->slices = 1; - c->thread_type = FF_THREAD_SLICE; - c->thread_count = c->slices; + + // Threading configuration: use frame-based threading for software encoders + if (is_software_h264(name) || is_software_hevc(name)) { + // Software encoders benefit from frame-level parallelism + c->thread_type = FF_THREAD_FRAME; + c->thread_count = thread_count > 0 ? thread_count : 4; + c->slices = 1; + } else { + // Hardware encoders typically use slice-based threading + c->slices = 1; + c->thread_type = FF_THREAD_SLICE; + c->thread_count = c->slices; + } // https://github.com/obsproject/obs-studio/blob/3cc7dc0e7cf8b01081dc23e432115f7efd0c8877/plugins/obs-ffmpeg/obs-ffmpeg-mux.c#L160 c->color_range = AVCOL_RANGE_MPEG; @@ -58,7 +99,10 @@ void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs, c->color_primaries = AVCOL_PRI_SMPTE170M; c->color_trc = AVCOL_TRC_SMPTE170M; - if (name.find("h264") != std::string::npos) { + // Profile selection: use BASELINE for software H264 (faster, simpler) + if (is_software_h264(name)) { + c->profile = FF_PROFILE_H264_BASELINE; // Simpler profile for real-time + } else if (name.find("h264") != std::string::npos) { c->profile = FF_PROFILE_H264_HIGH; } else if (name.find("hevc") != std::string::npos) { c->profile = FF_PROFILE_HEVC_MAIN; @@ -127,6 +171,44 @@ bool set_lantency_free(void *priv_data, const std::string &name) { } } } + // libx264 software encoder - zero latency tuning + if (is_software_h264(name)) { + // zerolatency: disable B-frames, reduce lookahead, disable CABAC etc. + if ((ret = av_opt_set(priv_data, "tune", "zerolatency", 0)) < 0) { + LOG_ERROR(std::string("libx264 set tune zerolatency failed, ret = ") + av_err2str(ret)); + // tune failure is not fatal, continue + } + // Disable B-frame adaptation (extra safety, zerolatency already includes this) + av_opt_set_int(priv_data, "b-adapt", 0, 0); + // Set low rc-lookahead for minimal latency + av_opt_set_int(priv_data, "rc-lookahead", 0, 0); + // Use sliced-threads for lower latency (encode slices in parallel within same frame) + av_opt_set_int(priv_data, "sliced-threads", 1, 0); + // Disable mb-tree for lower memory and faster encoding + av_opt_set_int(priv_data, "mbtree", 0, 0); + // Disable adaptive quantization for speed + av_opt_set_int(priv_data, "aq-mode", 0, 0); + // Use simpler motion estimation for speed + av_opt_set(priv_data, "me", "dia", 0); // diamond search (fastest) + // Reduce subpixel motion estimation refinement + av_opt_set_int(priv_data, "subq", 1, 0); // 1 = fastest + // Reduce reference frames for speed + av_opt_set_int(priv_data, "refs", 1, 0); + } + // libx265 software encoder - zero latency tuning + if (is_software_hevc(name)) { + if ((ret = av_opt_set(priv_data, "tune", "zerolatency", 0)) < 0) { + LOG_ERROR(std::string("libx265 set tune zerolatency failed, ret = ") + av_err2str(ret)); + // tune failure is not fatal, continue + } + // x265 specific low-latency parameters + // bframes=0: no B-frames + // rc-lookahead=0: no lookahead + // no-slices-wpp=1: disable wavefront parallel processing for lower latency + // frame-threads=1: single frame thread for lower latency + av_opt_set(priv_data, "x265-params", + "bframes=0:rc-lookahead=0:ref=1:no-b-adapt=1:aq-mode=0", 0); + } return true; } @@ -221,6 +303,50 @@ bool set_quality(void *priv_data, const std::string &name, int quality) { } } } + // libx264 software encoder presets + if (is_software_h264(name)) { + const char* preset = nullptr; + switch (quality) { + case Quality_High: + preset = "veryfast"; // Good quality while maintaining fast encoding + break; + case Quality_Medium: + preset = "superfast"; // Balance between speed and quality + break; + case Quality_Low: + preset = "ultrafast"; // Fastest speed, lowest CPU usage + break; + default: + preset = "superfast"; // Default to superfast for embedded devices + break; + } + if ((ret = av_opt_set(priv_data, "preset", preset, 0)) < 0) { + LOG_ERROR(std::string("libx264 set preset ") + preset + " failed, ret = " + av_err2str(ret)); + return false; + } + } + // libx265 software encoder presets + if (is_software_hevc(name)) { + const char* preset = nullptr; + switch (quality) { + case Quality_High: + preset = "veryfast"; + break; + case Quality_Medium: + preset = "superfast"; + break; + case Quality_Low: + preset = "ultrafast"; + break; + default: + preset = "superfast"; + break; + } + if ((ret = av_opt_set(priv_data, "preset", preset, 0)) < 0) { + LOG_ERROR(std::string("libx265 set preset ") + preset + " failed, ret = " + av_err2str(ret)); + return false; + } + } return true; } diff --git a/libs/hwcodec/cpp/common/util.h b/libs/hwcodec/cpp/common/util.h index 711fd2ab..0e5f4ee4 100644 --- a/libs/hwcodec/cpp/common/util.h +++ b/libs/hwcodec/cpp/common/util.h @@ -10,7 +10,7 @@ extern "C" { namespace util_encode { void set_av_codec_ctx(AVCodecContext *c, const std::string &name, int kbs, - int gop, int fps); + int gop, int fps, int thread_count); bool set_lantency_free(void *priv_data, const std::string &name); bool set_quality(void *priv_data, const std::string &name, int quality); bool set_rate_control(AVCodecContext *c, const std::string &name, int rc, diff --git a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index 184a3b52..487e555c 100644 --- a/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -235,12 +235,15 @@ public: c_->pix_fmt = hw_pixfmt_ != AV_PIX_FMT_NONE ? hw_pixfmt_ : (AVPixelFormat)pixfmt_; c_->sw_pix_fmt = (AVPixelFormat)pixfmt_; - util_encode::set_av_codec_ctx(c_, name_, kbs_, gop_, fps_); + util_encode::set_av_codec_ctx(c_, name_, kbs_, gop_, fps_, thread_count_); if (!util_encode::set_lantency_free(c_->priv_data, name_)) { LOG_ERROR(std::string("set_lantency_free failed, name: ") + name_); return false; } - // util_encode::set_quality(c_->priv_data, name_, quality_); + if (!util_encode::set_quality(c_->priv_data, name_, quality_)) { + LOG_ERROR(std::string("set_quality failed, name: ") + name_); + return false; + } util_encode::set_rate_control(c_, name_, rc_, q_); util_encode::set_gpu(c_->priv_data, name_, gpu_); util_encode::force_hw(c_->priv_data, name_); diff --git a/libs/hwcodec/src/ffmpeg_ram/mod.rs b/libs/hwcodec/src/ffmpeg_ram/mod.rs index c46038a0..e363ab13 100644 --- a/libs/hwcodec/src/ffmpeg_ram/mod.rs +++ b/libs/hwcodec/src/ffmpeg_ram/mod.rs @@ -119,14 +119,14 @@ impl CodecInfo { pub fn soft() -> CodecInfos { CodecInfos { h264: Some(CodecInfo { - name: "h264".to_owned(), + name: "libx264".to_owned(), mc_name: Default::default(), format: H264, hwdevice: AV_HWDEVICE_TYPE_NONE, priority: Priority::Soft as _, }), h265: Some(CodecInfo { - name: "hevc".to_owned(), + name: "libx265".to_owned(), mc_name: Default::default(), format: H265, hwdevice: AV_HWDEVICE_TYPE_NONE, diff --git a/src/config/schema.rs b/src/config/schema.rs index 9dded89f..206ecc83 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -375,7 +375,7 @@ impl Default for StreamConfig { Self { mode: StreamMode::Mjpeg, encoder: EncoderType::Auto, - bitrate_kbps: 8000, + bitrate_kbps: 1000, gop_size: 30, stun_server: Some("stun:stun.l.google.com:19302".to_string()), turn_server: None, diff --git a/src/video/capture.rs b/src/video/capture.rs index 22dfb67a..5b0edfa2 100644 --- a/src/video/capture.rs +++ b/src/video/capture.rs @@ -143,7 +143,7 @@ impl VideoCapturer { /// Create a new video capturer pub fn new(config: CaptureConfig) -> Self { let (state_tx, state_rx) = watch::channel(CaptureState::Stopped); - let (frame_tx, _) = broadcast::channel(16); // Buffer up to 16 frames + let (frame_tx, _) = broadcast::channel(64); // Buffer up to 64 frames for software encoding Self { config, diff --git a/src/video/encoder/h264.rs b/src/video/encoder/h264.rs index 6f5095ea..4c8d642a 100644 --- a/src/video/encoder/h264.rs +++ b/src/video/encoder/h264.rs @@ -126,7 +126,7 @@ impl Default for H264Config { fn default() -> Self { Self { base: EncoderConfig::default(), - bitrate_kbps: 8000, + bitrate_kbps: 1000, gop_size: 30, fps: 30, input_format: H264InputFormat::Nv12, diff --git a/src/video/shared_video_pipeline.rs b/src/video/shared_video_pipeline.rs index aa475f56..e13d63c0 100644 --- a/src/video/shared_video_pipeline.rs +++ b/src/video/shared_video_pipeline.rs @@ -77,7 +77,7 @@ impl Default for SharedVideoPipelineConfig { resolution: Resolution::HD720, input_format: PixelFormat::Yuyv, output_codec: VideoEncoderType::H264, - bitrate_kbps: 8000, + bitrate_kbps: 1000, fps: 30, gop_size: 30, encoder_backend: None, @@ -311,7 +311,7 @@ impl SharedVideoPipeline { config.input_format ); - let (frame_tx, _) = broadcast::channel(16); + let (frame_tx, _) = broadcast::channel(64); // Increased from 16 for software encoding let (running_tx, running_rx) = watch::channel(false); let nv12_size = (config.resolution.width * config.resolution.height * 3 / 2) as usize; let yuv420p_size = nv12_size; // Same size as NV12 diff --git a/src/webrtc/config.rs b/src/webrtc/config.rs index a7c62e36..0e45b0de 100644 --- a/src/webrtc/config.rs +++ b/src/webrtc/config.rs @@ -35,8 +35,8 @@ impl Default for WebRtcConfig { turn_servers: vec![], enable_datachannel: true, video_codec: VideoCodec::H264, - target_bitrate_kbps: 8000, - max_bitrate_kbps: 5000, + target_bitrate_kbps: 1000, + max_bitrate_kbps: 2000, min_bitrate_kbps: 500, enable_audio: true, } diff --git a/src/webrtc/universal_session.rs b/src/webrtc/universal_session.rs index c341d31c..29f793d3 100644 --- a/src/webrtc/universal_session.rs +++ b/src/webrtc/universal_session.rs @@ -64,7 +64,7 @@ impl Default for UniversalSessionConfig { codec: VideoEncoderType::H264, resolution: Resolution::HD720, input_format: PixelFormat::Mjpeg, - bitrate_kbps: 8000, + bitrate_kbps: 1000, fps: 30, gop_size: 30, audio_enabled: false, diff --git a/web/src/components/VideoConfigPopover.vue b/web/src/components/VideoConfigPopover.vue index cfe38854..c2575778 100644 --- a/web/src/components/VideoConfigPopover.vue +++ b/web/src/components/VideoConfigPopover.vue @@ -179,7 +179,7 @@ const selectedDevice = ref('') const selectedFormat = ref('') const selectedResolution = ref('') const selectedFps = ref(30) -const selectedBitrate = ref([8000]) +const selectedBitrate = ref([1000]) // UI state const applying = ref(false)