mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
feat(rustdesk): 优化视频编码协商和添加公共服务器支持
- 调整视频编码优先级为 H264 > H265 > VP8 > VP9,优先使用硬件编码 - 对接 RustDesk 客户端质量预设 (Low/Balanced/Best) 到 BitratePreset - 添加 secrets.toml 编译时读取机制,支持配置公共服务器 - 默认公共服务器: rustdesk.mofeng.run:21116 - 前端 ID 服务器输入框添加问号提示,显示公共服务器信息 - 用户留空时自动使用公共服务器
This commit is contained in:
@@ -30,6 +30,7 @@ use crate::error::{AppError, Result};
|
||||
use crate::hid::datachannel::{parse_hid_message, HidChannelEvent};
|
||||
use crate::hid::HidController;
|
||||
use crate::video::encoder::registry::VideoEncoderType;
|
||||
use crate::video::encoder::BitratePreset;
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
use crate::video::shared_video_pipeline::EncodedVideoFrame;
|
||||
|
||||
@@ -47,12 +48,10 @@ pub struct UniversalSessionConfig {
|
||||
pub resolution: Resolution,
|
||||
/// Input pixel format
|
||||
pub input_format: PixelFormat,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// Bitrate preset
|
||||
pub bitrate_preset: BitratePreset,
|
||||
/// Target FPS
|
||||
pub fps: u32,
|
||||
/// GOP size
|
||||
pub gop_size: u32,
|
||||
/// Enable audio track
|
||||
pub audio_enabled: bool,
|
||||
}
|
||||
@@ -64,9 +63,8 @@ impl Default for UniversalSessionConfig {
|
||||
codec: VideoEncoderType::H264,
|
||||
resolution: Resolution::HD720,
|
||||
input_format: PixelFormat::Mjpeg,
|
||||
bitrate_kbps: 1000,
|
||||
bitrate_preset: BitratePreset::Balanced,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
audio_enabled: false,
|
||||
}
|
||||
}
|
||||
@@ -144,7 +142,7 @@ impl UniversalSession {
|
||||
stream_id: "one-kvm-stream".to_string(),
|
||||
codec: video_codec,
|
||||
resolution: config.resolution,
|
||||
bitrate_kbps: config.bitrate_kbps,
|
||||
bitrate_kbps: config.bitrate_preset.bitrate_kbps(),
|
||||
fps: config.fps,
|
||||
};
|
||||
let video_track = Arc::new(UniversalVideoTrack::new(track_config));
|
||||
|
||||
@@ -17,12 +17,10 @@
|
||||
//! ```
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, trace, warn};
|
||||
use webrtc::media::io::h264_reader::H264Reader;
|
||||
use webrtc::media::Sample;
|
||||
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
|
||||
use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP;
|
||||
@@ -201,18 +199,6 @@ pub struct VideoTrackStats {
|
||||
pub errors: u64,
|
||||
}
|
||||
|
||||
/// Cached codec parameters for H264/H265
|
||||
#[derive(Debug, Default)]
|
||||
struct CachedParams {
|
||||
/// H264: SPS, H265: VPS
|
||||
#[allow(dead_code)]
|
||||
vps: Option<Bytes>,
|
||||
/// SPS (both H264 and H265)
|
||||
sps: Option<Bytes>,
|
||||
/// PPS (both H264 and H265)
|
||||
pps: Option<Bytes>,
|
||||
}
|
||||
|
||||
/// Track type wrapper to support different underlying track implementations
|
||||
enum TrackType {
|
||||
/// Sample-based track with built-in payloader (H264, VP8, VP9)
|
||||
@@ -243,8 +229,6 @@ pub struct UniversalVideoTrack {
|
||||
config: UniversalVideoTrackConfig,
|
||||
/// Statistics
|
||||
stats: Mutex<VideoTrackStats>,
|
||||
/// Cached parameters for H264/H265
|
||||
cached_params: Mutex<CachedParams>,
|
||||
/// H265 RTP state (only used for H265)
|
||||
h265_state: Option<Mutex<H265RtpState>>,
|
||||
}
|
||||
@@ -294,7 +278,6 @@ impl UniversalVideoTrack {
|
||||
codec: config.codec,
|
||||
config,
|
||||
stats: Mutex::new(VideoTrackStats::default()),
|
||||
cached_params: Mutex::new(CachedParams::default()),
|
||||
h265_state,
|
||||
}
|
||||
}
|
||||
@@ -341,71 +324,43 @@ impl UniversalVideoTrack {
|
||||
}
|
||||
|
||||
/// Write H264 frame (Annex B format)
|
||||
///
|
||||
/// Sends the entire Annex B frame as a single Sample to allow the
|
||||
/// H264Payloader to aggregate SPS+PPS into STAP-A packets.
|
||||
async fn write_h264_frame(&self, data: &[u8], is_keyframe: bool) -> Result<()> {
|
||||
let cursor = Cursor::new(data);
|
||||
let mut h264_reader = H264Reader::new(cursor, 1024 * 1024);
|
||||
// Send entire Annex B frame as one Sample
|
||||
// The H264Payloader in rtp crate will:
|
||||
// 1. Parse NAL units from Annex B format
|
||||
// 2. Cache SPS and PPS
|
||||
// 3. Aggregate SPS+PPS+IDR into STAP-A when possible
|
||||
// 4. Fragment large NALs using FU-A
|
||||
let frame_duration = Duration::from_micros(1_000_000 / self.config.fps.max(1) as u64);
|
||||
let sample = Sample {
|
||||
data: Bytes::copy_from_slice(data),
|
||||
duration: frame_duration,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut nals: Vec<Bytes> = Vec::new();
|
||||
let mut has_sps = false;
|
||||
let mut has_pps = false;
|
||||
let mut has_idr = false;
|
||||
|
||||
// Parse NAL units
|
||||
while let Ok(nal) = h264_reader.next_nal() {
|
||||
if nal.data.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nal_type = nal.data[0] & 0x1F;
|
||||
|
||||
// Skip AUD (9) and filler (12)
|
||||
if nal_type == 9 || nal_type == 12 {
|
||||
continue;
|
||||
}
|
||||
|
||||
match nal_type {
|
||||
5 => has_idr = true,
|
||||
7 => {
|
||||
has_sps = true;
|
||||
self.cached_params.lock().await.sps = Some(nal.data.clone().freeze());
|
||||
}
|
||||
8 => {
|
||||
has_pps = true;
|
||||
self.cached_params.lock().await.pps = Some(nal.data.clone().freeze());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
nals.push(nal.data.freeze());
|
||||
}
|
||||
|
||||
// Inject cached SPS/PPS before IDR if missing
|
||||
if has_idr && (!has_sps || !has_pps) {
|
||||
let mut injected: Vec<Bytes> = Vec::new();
|
||||
let params = self.cached_params.lock().await;
|
||||
|
||||
if !has_sps {
|
||||
if let Some(ref sps) = params.sps {
|
||||
debug!("Injecting cached H264 SPS");
|
||||
injected.push(sps.clone());
|
||||
match &self.track {
|
||||
TrackType::Sample(track) => {
|
||||
if let Err(e) = track.write_sample(&sample).await {
|
||||
debug!("H264 write_sample failed: {}", e);
|
||||
}
|
||||
}
|
||||
if !has_pps {
|
||||
if let Some(ref pps) = params.pps {
|
||||
debug!("Injecting cached H264 PPS");
|
||||
injected.push(pps.clone());
|
||||
}
|
||||
}
|
||||
drop(params);
|
||||
|
||||
if !injected.is_empty() {
|
||||
injected.extend(nals);
|
||||
nals = injected;
|
||||
TrackType::Rtp(_) => {
|
||||
warn!("H264 should not use RTP track");
|
||||
}
|
||||
}
|
||||
|
||||
// Send NAL units
|
||||
self.send_nals(nals, is_keyframe).await
|
||||
// Update stats
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.frames_sent += 1;
|
||||
stats.bytes_sent += data.len() as u64;
|
||||
if is_keyframe {
|
||||
stats.keyframes_sent += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write H265 frame (Annex B format)
|
||||
@@ -483,52 +438,6 @@ impl UniversalVideoTrack {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send NAL units as samples (H264 only)
|
||||
///
|
||||
/// Important: Only the last NAL unit should have the frame duration set.
|
||||
/// All NAL units in a frame share the same RTP timestamp, so only the last
|
||||
/// one should increment the timestamp by the frame duration.
|
||||
async fn send_nals(&self, nals: Vec<Bytes>, is_keyframe: bool) -> Result<()> {
|
||||
let mut total_bytes = 0u64;
|
||||
// Calculate frame duration based on configured FPS
|
||||
let frame_duration = Duration::from_micros(1_000_000 / self.config.fps.max(1) as u64);
|
||||
let nal_count = nals.len();
|
||||
|
||||
match &self.track {
|
||||
TrackType::Sample(track) => {
|
||||
for (i, nal_data) in nals.into_iter().enumerate() {
|
||||
let is_last = i == nal_count - 1;
|
||||
// Only the last NAL should have duration set
|
||||
// This ensures all NALs in a frame share the same RTP timestamp
|
||||
let sample = Sample {
|
||||
data: nal_data.clone(),
|
||||
duration: if is_last { frame_duration } else { Duration::ZERO },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Err(e) = track.write_sample(&sample).await {
|
||||
debug!("NAL write_sample failed: {}", e);
|
||||
}
|
||||
|
||||
total_bytes += nal_data.len() as u64;
|
||||
}
|
||||
}
|
||||
TrackType::Rtp(_) => {
|
||||
warn!("send_nals should not be called for RTP track (H265)");
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.frames_sent += 1;
|
||||
stats.bytes_sent += total_bytes;
|
||||
if is_keyframe {
|
||||
stats.keyframes_sent += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send H265 NAL units via custom H265Payloader
|
||||
async fn send_h265_rtp(&self, data: &[u8], is_keyframe: bool) -> Result<()> {
|
||||
let rtp_track = match &self.track {
|
||||
|
||||
@@ -51,6 +51,7 @@ use crate::video::shared_video_pipeline::{SharedVideoPipeline, SharedVideoPipeli
|
||||
use super::config::{TurnServer, WebRtcConfig};
|
||||
use super::signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer};
|
||||
use super::universal_session::{UniversalSession, UniversalSessionConfig};
|
||||
use crate::video::encoder::BitratePreset;
|
||||
|
||||
/// WebRTC streamer configuration
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -63,12 +64,10 @@ pub struct WebRtcStreamerConfig {
|
||||
pub resolution: Resolution,
|
||||
/// Input pixel format
|
||||
pub input_format: PixelFormat,
|
||||
/// Target bitrate in kbps
|
||||
pub bitrate_kbps: u32,
|
||||
/// Bitrate preset
|
||||
pub bitrate_preset: BitratePreset,
|
||||
/// Target FPS
|
||||
pub fps: u32,
|
||||
/// GOP size (keyframe interval)
|
||||
pub gop_size: u32,
|
||||
/// Enable audio (reserved)
|
||||
pub audio_enabled: bool,
|
||||
/// Encoder backend (None = auto select best available)
|
||||
@@ -82,9 +81,8 @@ impl Default for WebRtcStreamerConfig {
|
||||
video_codec: VideoCodecType::H264,
|
||||
resolution: Resolution::HD720,
|
||||
input_format: PixelFormat::Mjpeg,
|
||||
bitrate_kbps: 8000,
|
||||
bitrate_preset: BitratePreset::Balanced,
|
||||
fps: 30,
|
||||
gop_size: 30,
|
||||
audio_enabled: false,
|
||||
encoder_backend: None,
|
||||
}
|
||||
@@ -282,10 +280,10 @@ impl WebRtcStreamer {
|
||||
resolution: config.resolution,
|
||||
input_format: config.input_format,
|
||||
output_codec: Self::codec_type_to_encoder_type(codec),
|
||||
bitrate_kbps: config.bitrate_kbps,
|
||||
bitrate_preset: config.bitrate_preset,
|
||||
fps: config.fps,
|
||||
gop_size: config.gop_size,
|
||||
encoder_backend: config.encoder_backend,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
info!("Creating shared video pipeline for {:?}", codec);
|
||||
@@ -541,8 +539,8 @@ impl WebRtcStreamer {
|
||||
// Note: bitrate is NOT auto-scaled here - use set_bitrate() or config to change it
|
||||
|
||||
info!(
|
||||
"WebRTC config updated: {}x{} {:?} @ {} fps, {} kbps",
|
||||
resolution.width, resolution.height, format, fps, config.bitrate_kbps
|
||||
"WebRTC config updated: {}x{} {:?} @ {} fps, {}",
|
||||
resolution.width, resolution.height, format, fps, config.bitrate_preset
|
||||
);
|
||||
}
|
||||
|
||||
@@ -636,9 +634,8 @@ impl WebRtcStreamer {
|
||||
codec: Self::codec_type_to_encoder_type(codec),
|
||||
resolution: config.resolution,
|
||||
input_format: config.input_format,
|
||||
bitrate_kbps: config.bitrate_kbps,
|
||||
bitrate_preset: config.bitrate_preset,
|
||||
fps: config.fps,
|
||||
gop_size: config.gop_size,
|
||||
audio_enabled: *self.audio_enabled.read().await,
|
||||
};
|
||||
drop(config);
|
||||
@@ -875,13 +872,13 @@ impl WebRtcStreamer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set bitrate
|
||||
/// Set bitrate using preset
|
||||
///
|
||||
/// Note: Hardware encoders (VAAPI, NVENC, etc.) don't support dynamic bitrate changes.
|
||||
/// This method restarts the pipeline to apply the new bitrate.
|
||||
pub async fn set_bitrate(self: &Arc<Self>, bitrate_kbps: u32) -> Result<()> {
|
||||
pub async fn set_bitrate_preset(self: &Arc<Self>, preset: BitratePreset) -> Result<()> {
|
||||
// Update config first
|
||||
self.config.write().await.bitrate_kbps = bitrate_kbps;
|
||||
self.config.write().await.bitrate_preset = preset;
|
||||
|
||||
// Check if pipeline exists and is running
|
||||
let pipeline_running = {
|
||||
@@ -894,8 +891,8 @@ impl WebRtcStreamer {
|
||||
|
||||
if pipeline_running {
|
||||
info!(
|
||||
"Restarting video pipeline to apply new bitrate: {} kbps",
|
||||
bitrate_kbps
|
||||
"Restarting video pipeline to apply new bitrate: {}",
|
||||
preset
|
||||
);
|
||||
|
||||
// Stop existing pipeline
|
||||
@@ -936,16 +933,16 @@ impl WebRtcStreamer {
|
||||
}
|
||||
|
||||
info!(
|
||||
"Video pipeline restarted with {} kbps, reconnected {} sessions",
|
||||
bitrate_kbps,
|
||||
"Video pipeline restarted with {}, reconnected {} sessions",
|
||||
preset,
|
||||
session_ids.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"Pipeline not running, bitrate {} kbps will apply on next start",
|
||||
bitrate_kbps
|
||||
"Pipeline not running, bitrate {} will apply on next start",
|
||||
preset
|
||||
);
|
||||
}
|
||||
|
||||
@@ -978,7 +975,7 @@ mod tests {
|
||||
let config = WebRtcStreamerConfig::default();
|
||||
assert_eq!(config.video_codec, VideoCodecType::H264);
|
||||
assert_eq!(config.resolution, Resolution::HD720);
|
||||
assert_eq!(config.bitrate_kbps, 8000);
|
||||
assert_eq!(config.bitrate_preset, BitratePreset::Quality);
|
||||
assert_eq!(config.fps, 30);
|
||||
assert!(!config.audio_enabled);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user