mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-19 10:21:51 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
626
src/web/handlers/stream.rs
Normal file
626
src/web/handlers/stream.rs
Normal file
@@ -0,0 +1,626 @@
|
||||
use super::*;
|
||||
|
||||
use crate::video::streamer::StreamerStats;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{header, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
fn stream_mode_label(mode: StreamMode, codec: crate::video::codec::VideoCodecType) -> &'static str {
|
||||
match mode {
|
||||
StreamMode::Mjpeg => "mjpeg",
|
||||
StreamMode::WebRTC => codec_to_id(codec),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stream state
|
||||
pub async fn stream_state(State(state): State<Arc<AppState>>) -> Json<StreamerStats> {
|
||||
Json(state.stream_manager.stats().await)
|
||||
}
|
||||
|
||||
/// Start streaming
|
||||
pub async fn stream_start(State(state): State<Arc<AppState>>) -> Result<Json<LoginResponse>> {
|
||||
state.stream_manager.start().await?;
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
message: Some("Streaming started".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Stop streaming
|
||||
pub async fn stream_stop(State(state): State<Arc<AppState>>) -> Result<Json<LoginResponse>> {
|
||||
state.stream_manager.stop().await?;
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
message: Some("Streaming stopped".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Stream mode request
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetStreamModeRequest {
|
||||
/// Target mode: "mjpeg" or "webrtc"
|
||||
pub mode: String,
|
||||
}
|
||||
|
||||
/// Stream mode response
|
||||
#[derive(Serialize)]
|
||||
pub struct StreamModeResponse {
|
||||
pub success: bool,
|
||||
pub mode: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transition_id: Option<String>,
|
||||
pub switching: bool,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Get current stream mode
|
||||
pub async fn stream_mode_get(State(state): State<Arc<AppState>>) -> Json<StreamModeResponse> {
|
||||
let mode = state.stream_manager.current_mode().await;
|
||||
let codec = state.stream_manager.current_video_codec().await;
|
||||
let mode_str = stream_mode_label(mode, codec).to_string();
|
||||
|
||||
Json(StreamModeResponse {
|
||||
success: true,
|
||||
mode: mode_str,
|
||||
transition_id: state.stream_manager.current_transition_id().await,
|
||||
switching: state.stream_manager.is_switching(),
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set stream mode (switch between MJPEG and WebRTC)
|
||||
pub async fn stream_mode_set(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SetStreamModeRequest>,
|
||||
) -> Result<Json<StreamModeResponse>> {
|
||||
use crate::video::codec::VideoCodecType;
|
||||
|
||||
let constraints = state.stream_manager.codec_constraints().await;
|
||||
|
||||
let mode_lower = req.mode.to_lowercase();
|
||||
let (new_mode, video_codec) = match mode_lower.as_str() {
|
||||
"mjpeg" => (StreamMode::Mjpeg, None),
|
||||
"webrtc" | "h264" => (StreamMode::WebRTC, Some(VideoCodecType::H264)),
|
||||
"h265" => (StreamMode::WebRTC, Some(VideoCodecType::H265)),
|
||||
"vp8" => (StreamMode::WebRTC, Some(VideoCodecType::VP8)),
|
||||
"vp9" => (StreamMode::WebRTC, Some(VideoCodecType::VP9)),
|
||||
_ => {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Invalid mode '{}'. Valid modes: mjpeg, h264, h265, vp8, vp9",
|
||||
req.mode
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
if new_mode == StreamMode::Mjpeg && !constraints.is_mjpeg_allowed() {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Codec 'mjpeg' is not allowed: {}",
|
||||
constraints.reason
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(codec) = video_codec {
|
||||
if !constraints.is_webrtc_codec_allowed(codec) {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Codec '{}' is not allowed: {}",
|
||||
codec_to_id(codec),
|
||||
constraints.reason
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let requested_mode_str = match (&new_mode, &video_codec) {
|
||||
(StreamMode::Mjpeg, _) => "mjpeg",
|
||||
(StreamMode::WebRTC, Some(VideoCodecType::H264)) => "h264",
|
||||
(StreamMode::WebRTC, Some(VideoCodecType::H265)) => "h265",
|
||||
(StreamMode::WebRTC, Some(VideoCodecType::VP8)) => "vp8",
|
||||
(StreamMode::WebRTC, Some(VideoCodecType::VP9)) => "vp9",
|
||||
(StreamMode::WebRTC, None) => "webrtc",
|
||||
};
|
||||
|
||||
// Detect codec-only switch: already in WebRTC mode, just changing codec.
|
||||
// switch_mode_transaction treats this as "no switch needed" since StreamMode
|
||||
// is still WebRTC, so we handle codec change + event emission here.
|
||||
let current_mode = state.stream_manager.current_mode().await;
|
||||
let prev_codec = state.stream_manager.current_video_codec().await;
|
||||
|
||||
let codec_changed = video_codec.is_some_and(|c| c != prev_codec);
|
||||
let is_codec_only_switch =
|
||||
current_mode == StreamMode::WebRTC && new_mode == StreamMode::WebRTC && codec_changed;
|
||||
|
||||
if let Some(codec) = video_codec {
|
||||
info!("Setting WebRTC video codec to {:?}", codec);
|
||||
if let Err(e) = state.stream_manager.set_video_codec(codec).await {
|
||||
warn!("Failed to set video codec: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// For codec-only switch, emit events directly instead of going through
|
||||
// switch_mode_transaction (which short-circuits when mode is unchanged).
|
||||
if is_codec_only_switch {
|
||||
let transition_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
state
|
||||
.stream_manager
|
||||
.notify_codec_switch(&transition_id, requested_mode_str, &codec_to_id(prev_codec))
|
||||
.await;
|
||||
|
||||
return Ok(Json(StreamModeResponse {
|
||||
success: true,
|
||||
mode: requested_mode_str.to_string(),
|
||||
transition_id: Some(transition_id),
|
||||
switching: false,
|
||||
message: Some(format!("Codec switched to {}", requested_mode_str)),
|
||||
}));
|
||||
}
|
||||
|
||||
let tx = state
|
||||
.stream_manager
|
||||
.switch_mode_transaction(new_mode.clone())
|
||||
.await?;
|
||||
|
||||
let active_mode = state.stream_manager.current_mode().await;
|
||||
let active_codec = state.stream_manager.current_video_codec().await;
|
||||
let active_mode_str = stream_mode_label(active_mode, active_codec).to_string();
|
||||
|
||||
let no_switch_needed = !tx.accepted && !tx.switching && tx.transition_id.is_none();
|
||||
Ok(Json(StreamModeResponse {
|
||||
success: tx.accepted || no_switch_needed,
|
||||
mode: if tx.accepted {
|
||||
requested_mode_str.to_string()
|
||||
} else {
|
||||
active_mode_str
|
||||
},
|
||||
transition_id: tx.transition_id,
|
||||
switching: tx.switching,
|
||||
message: Some(if tx.accepted {
|
||||
format!("Switching to {} mode", requested_mode_str)
|
||||
} else if tx.switching {
|
||||
"Mode switch already in progress".to_string()
|
||||
} else {
|
||||
"No switch needed".to_string()
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Available video codec info
|
||||
#[derive(Serialize)]
|
||||
pub struct VideoCodecInfo {
|
||||
/// Codec identifier (mjpeg, h264, h265, vp8, vp9)
|
||||
pub id: String,
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Protocol (http or webrtc)
|
||||
pub protocol: String,
|
||||
/// Whether hardware accelerated
|
||||
pub hardware: bool,
|
||||
/// Encoder backend name (e.g., "vaapi", "nvenc", "software")
|
||||
pub backend: Option<String>,
|
||||
/// Whether this codec is available
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
/// Encoder backend info
|
||||
#[derive(Serialize)]
|
||||
pub struct EncoderBackendInfo {
|
||||
/// Backend identifier (vaapi, nvenc, qsv, amf, rkmpp, v4l2m2m, software)
|
||||
pub id: String,
|
||||
/// Display name
|
||||
pub name: String,
|
||||
/// Whether this is a hardware backend
|
||||
pub is_hardware: bool,
|
||||
/// Supported video formats (h264, h265, vp8, vp9)
|
||||
pub supported_formats: Vec<String>,
|
||||
}
|
||||
|
||||
/// Available codecs response
|
||||
#[derive(Serialize)]
|
||||
pub struct AvailableCodecsResponse {
|
||||
pub success: bool,
|
||||
/// Available encoder backends
|
||||
pub backends: Vec<EncoderBackendInfo>,
|
||||
/// Available codecs (for backward compatibility)
|
||||
pub codecs: Vec<VideoCodecInfo>,
|
||||
}
|
||||
|
||||
/// Stream constraints response
|
||||
#[derive(Serialize)]
|
||||
pub struct StreamConstraintsResponse {
|
||||
pub success: bool,
|
||||
pub allowed_codecs: Vec<String>,
|
||||
pub locked_codec: Option<String>,
|
||||
pub disallow_mjpeg: bool,
|
||||
pub sources: ConstraintSources,
|
||||
pub reason: String,
|
||||
pub current_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ConstraintSources {
|
||||
pub rustdesk: bool,
|
||||
pub rtsp: bool,
|
||||
}
|
||||
|
||||
/// Get stream codec constraints derived from enabled services.
|
||||
pub async fn stream_constraints_get(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<StreamConstraintsResponse> {
|
||||
let constraints = state.stream_manager.codec_constraints().await;
|
||||
let current_mode = state.stream_manager.current_mode().await;
|
||||
let current_codec = state.stream_manager.current_video_codec().await;
|
||||
let current_mode = stream_mode_label(current_mode, current_codec).to_string();
|
||||
|
||||
Json(StreamConstraintsResponse {
|
||||
success: true,
|
||||
allowed_codecs: constraints
|
||||
.allowed_codecs_for_api()
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
locked_codec: constraints
|
||||
.locked_codec
|
||||
.map(codec_to_id)
|
||||
.map(str::to_string),
|
||||
disallow_mjpeg: !constraints.allow_mjpeg,
|
||||
sources: ConstraintSources {
|
||||
rustdesk: constraints.rustdesk_enabled,
|
||||
rtsp: constraints.rtsp_enabled,
|
||||
},
|
||||
reason: constraints.reason,
|
||||
current_mode,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set bitrate request
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetBitrateRequest {
|
||||
pub bitrate_preset: BitratePreset,
|
||||
}
|
||||
|
||||
/// Set stream bitrate (real-time adjustment)
|
||||
pub async fn stream_set_bitrate(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<SetBitrateRequest>,
|
||||
) -> Result<Json<LoginResponse>> {
|
||||
// Update config
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.stream.bitrate_preset = req.bitrate_preset;
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Apply to WebRTC streamer (real-time adjustment)
|
||||
if let Err(e) = state
|
||||
.stream_manager
|
||||
.set_bitrate_preset(req.bitrate_preset)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to set bitrate dynamically: {}", e);
|
||||
// Don't fail the request - config is saved, will apply on next connection
|
||||
} else {
|
||||
info!("Bitrate updated to {}", req.bitrate_preset);
|
||||
}
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
message: Some(format!("Bitrate set to {}", req.bitrate_preset)),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get available video codecs
|
||||
pub async fn stream_codecs_list() -> Json<AvailableCodecsResponse> {
|
||||
use crate::video::codec::registry::{EncoderRegistry, VideoEncoderType};
|
||||
|
||||
let registry = EncoderRegistry::global();
|
||||
|
||||
// Build backends list
|
||||
let mut backends = Vec::new();
|
||||
for backend in registry.available_backends() {
|
||||
let formats = registry.formats_for_backend(backend);
|
||||
let format_ids: Vec<String> = formats
|
||||
.iter()
|
||||
.copied()
|
||||
.map(crate::video::codec_constraints::encoder_codec_to_id)
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
backends.push(EncoderBackendInfo {
|
||||
id: format!("{:?}", backend).to_lowercase(),
|
||||
name: backend.display_name().to_string(),
|
||||
is_hardware: backend.is_hardware(),
|
||||
supported_formats: format_ids,
|
||||
});
|
||||
}
|
||||
|
||||
// Build codecs list (for backward compatibility)
|
||||
let mut codecs = Vec::new();
|
||||
|
||||
// MJPEG is always available (HTTP streaming)
|
||||
codecs.push(VideoCodecInfo {
|
||||
id: "mjpeg".to_string(),
|
||||
name: "MJPEG / HTTP".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
hardware: false,
|
||||
backend: Some("software".to_string()),
|
||||
available: true,
|
||||
});
|
||||
|
||||
// Check H264 availability (supports software fallback)
|
||||
let h264_encoder = registry.best_available_encoder(VideoEncoderType::H264);
|
||||
codecs.push(VideoCodecInfo {
|
||||
id: "h264".to_string(),
|
||||
name: "H.264 / WebRTC".to_string(),
|
||||
protocol: "webrtc".to_string(),
|
||||
hardware: h264_encoder.map(|e| e.is_hardware).unwrap_or(false),
|
||||
backend: h264_encoder.map(|e| e.backend.to_string()),
|
||||
available: h264_encoder.is_some(),
|
||||
});
|
||||
|
||||
// Check H265 availability (now supports software too)
|
||||
let h265_encoder = registry.best_available_encoder(VideoEncoderType::H265);
|
||||
codecs.push(VideoCodecInfo {
|
||||
id: "h265".to_string(),
|
||||
name: "H.265 / WebRTC".to_string(),
|
||||
protocol: "webrtc".to_string(),
|
||||
hardware: h265_encoder.map(|e| e.is_hardware).unwrap_or(false),
|
||||
backend: h265_encoder.map(|e| e.backend.to_string()),
|
||||
available: h265_encoder.is_some(),
|
||||
});
|
||||
|
||||
// Check VP8 availability (now supports software too)
|
||||
let vp8_encoder = registry.best_available_encoder(VideoEncoderType::VP8);
|
||||
codecs.push(VideoCodecInfo {
|
||||
id: "vp8".to_string(),
|
||||
name: "VP8 / WebRTC".to_string(),
|
||||
protocol: "webrtc".to_string(),
|
||||
hardware: vp8_encoder.map(|e| e.is_hardware).unwrap_or(false),
|
||||
backend: vp8_encoder.map(|e| e.backend.to_string()),
|
||||
available: vp8_encoder.is_some(),
|
||||
});
|
||||
|
||||
// Check VP9 availability (now supports software too)
|
||||
let vp9_encoder = registry.best_available_encoder(VideoEncoderType::VP9);
|
||||
codecs.push(VideoCodecInfo {
|
||||
id: "vp9".to_string(),
|
||||
name: "VP9 / WebRTC".to_string(),
|
||||
protocol: "webrtc".to_string(),
|
||||
hardware: vp9_encoder.map(|e| e.is_hardware).unwrap_or(false),
|
||||
backend: vp9_encoder.map(|e| e.backend.to_string()),
|
||||
available: vp9_encoder.is_some(),
|
||||
});
|
||||
|
||||
Json(AvailableCodecsResponse {
|
||||
success: true,
|
||||
backends,
|
||||
codecs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Run hardware encoder smoke tests across common resolutions/codecs.
|
||||
pub async fn video_encoder_self_check() -> Json<VideoEncoderSelfCheckResponse> {
|
||||
let response = tokio::task::spawn_blocking(run_hardware_self_check)
|
||||
.await
|
||||
.unwrap_or_else(|_| build_hardware_self_check_runtime_error());
|
||||
|
||||
Json(response)
|
||||
}
|
||||
|
||||
/// Query parameters for MJPEG stream
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct MjpegStreamQuery {
|
||||
/// Optional client ID (if not provided, a random UUID will be generated)
|
||||
pub client_id: Option<String>,
|
||||
}
|
||||
|
||||
/// MJPEG stream endpoint
|
||||
pub async fn mjpeg_stream(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<MjpegStreamQuery>,
|
||||
) -> impl IntoResponse {
|
||||
// Check if MJPEG mode is active
|
||||
if !state.stream_manager.is_mjpeg_enabled().await {
|
||||
return axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(axum::body::Body::from(
|
||||
r#"{"error":"MJPEG mode not active. Current mode is WebRTC."}"#,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Check if config is being changed - reject new connections during config change
|
||||
if state.stream_manager.is_config_changing() {
|
||||
return axum::response::Response::builder()
|
||||
.status(axum::http::StatusCode::SERVICE_UNAVAILABLE)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(axum::body::Body::from(
|
||||
r#"{"error":"Video configuration is being changed. Please retry shortly."}"#,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Ensure stream is started (but not during config change)
|
||||
if !state.stream_manager.is_streaming().await && !state.stream_manager.is_config_changing() {
|
||||
if let Err(e) = state.stream_manager.start().await {
|
||||
tracing::error!("Failed to auto-start stream: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let handler = state.stream_manager.mjpeg_handler();
|
||||
|
||||
// Use provided client ID or generate a new one
|
||||
let client_id = query
|
||||
.client_id
|
||||
.filter(|id| !id.is_empty() && id.len() <= 64) // Validate: non-empty, max 64 chars
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
|
||||
// Create RAII guard - this will automatically register and unregister the client
|
||||
let guard = Arc::new(crate::stream::mjpeg::ClientGuard::new(
|
||||
client_id.clone(),
|
||||
handler.clone(),
|
||||
));
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<bytes::Bytes>(1);
|
||||
|
||||
let guard_clone = guard.clone();
|
||||
let handler_clone = handler.clone();
|
||||
tokio::spawn(async move {
|
||||
let _guard = guard_clone; // Keep guard alive
|
||||
let mut notify_rx = handler_clone.subscribe();
|
||||
let mut last_seq = 0u64;
|
||||
let mut timeout_count = 0;
|
||||
|
||||
// Send initial frame if available
|
||||
if let Some(frame) = handler_clone.current_frame() {
|
||||
if frame.is_valid_jpeg() {
|
||||
let data = create_mjpeg_part(frame.data());
|
||||
// send() blocks until receiver is ready (backpressure)
|
||||
if tx.send(data).await.is_ok() {
|
||||
// FPS recording moved to async_stream after yield
|
||||
last_seq = frame.sequence;
|
||||
} else {
|
||||
return; // Receiver closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
// Check if stream went offline (e.g., during config change)
|
||||
if !handler_clone.is_online() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait for new frame notification with timeout
|
||||
let result =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), notify_rx.recv()).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
// Check online status after receiving notification
|
||||
// set_offline() sends a notification, so we need to check here
|
||||
if !handler_clone.is_online() {
|
||||
break;
|
||||
}
|
||||
timeout_count = 0;
|
||||
if let Some(frame) = handler_clone.current_frame() {
|
||||
// Use != instead of > to handle sequence reset when capturer restarts
|
||||
// (e.g., after video config change, new capturer starts from seq=0)
|
||||
if frame.sequence != last_seq && frame.is_valid_jpeg() {
|
||||
let data = create_mjpeg_part(frame.data());
|
||||
if tx.send(data).await.is_ok() {
|
||||
last_seq = frame.sequence;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
|
||||
break;
|
||||
}
|
||||
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {
|
||||
// Receiver was too slow - skip missed frames and jump to latest
|
||||
if !handler_clone.is_online() {
|
||||
break;
|
||||
}
|
||||
timeout_count = 0;
|
||||
|
||||
if let Some(frame) = handler_clone.current_frame() {
|
||||
if frame.is_valid_jpeg() {
|
||||
// Send current frame immediately and reset sequence tracking
|
||||
if tx.send(create_mjpeg_part(frame.data())).await.is_ok() {
|
||||
last_seq = frame.sequence;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - check if still online
|
||||
timeout_count += 1;
|
||||
if timeout_count > 6 || !handler_clone.is_online() {
|
||||
break;
|
||||
}
|
||||
// Send last frame again to keep connection alive
|
||||
let Some(frame) = handler_clone.current_frame() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if frame.is_valid_jpeg()
|
||||
&& tx.send(create_mjpeg_part(frame.data())).await.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create stream that receives from channel and forwards to the HTTP
|
||||
// body. Record FPS *before* yield so the final frame of a session
|
||||
// still gets counted (after-yield code in async_stream! only runs
|
||||
// when the consumer polls again, which never happens for the last
|
||||
// frame of a closing connection).
|
||||
let handler_for_stream = handler.clone();
|
||||
let guard_for_stream = guard.clone();
|
||||
let body_stream = async_stream::stream! {
|
||||
while let Some(data) = rx.recv().await {
|
||||
handler_for_stream.record_frame_sent(guard_for_stream.id());
|
||||
yield Ok::<bytes::Bytes, std::io::Error>(data);
|
||||
}
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
"multipart/x-mixed-replace; boundary=frame",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate")
|
||||
.header(header::PRAGMA, "no-cache")
|
||||
.header(header::EXPIRES, "0")
|
||||
.header(header::CONNECTION, "keep-alive")
|
||||
.body(Body::from_stream(body_stream))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Single JPEG snapshot
|
||||
pub async fn snapshot(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let handler = state.stream_manager.mjpeg_handler();
|
||||
|
||||
match handler.current_frame() {
|
||||
Some(frame) if frame.is_valid_jpeg() => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "image/jpeg")
|
||||
.header(header::CACHE_CONTROL, "no-cache")
|
||||
.body(Body::from(frame.data_bytes()))
|
||||
.unwrap(),
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::SERVICE_UNAVAILABLE)
|
||||
.body(Body::from("No frame available"))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create MJPEG multipart frame bytes
|
||||
fn create_mjpeg_part(jpeg_data: &[u8]) -> bytes::Bytes {
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
let mut buf = BytesMut::with_capacity(128 + jpeg_data.len());
|
||||
|
||||
// Write boundary and headers
|
||||
buf.put_slice(b"--frame\r\n");
|
||||
buf.put_slice(b"Content-Type: image/jpeg\r\n");
|
||||
buf.put_slice(format!("Content-Length: {}\r\n", jpeg_data.len()).as_bytes());
|
||||
buf.put_slice(b"\r\n");
|
||||
|
||||
// Write JPEG data
|
||||
buf.put_slice(jpeg_data);
|
||||
buf.put_slice(b"\r\n");
|
||||
|
||||
buf.freeze()
|
||||
}
|
||||
Reference in New Issue
Block a user