mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 09:01:54 +08:00
init
This commit is contained in:
257
src/web/audio_ws.rs
Normal file
257
src/web/audio_ws.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Audio WebSocket handler for MJPEG mode
|
||||
//!
|
||||
//! Provides a dedicated WebSocket endpoint (`/api/ws/audio`) for streaming
|
||||
//! Opus-encoded audio data in binary format.
|
||||
//!
|
||||
//! ## Binary Protocol
|
||||
//!
|
||||
//! Each audio packet is sent as a binary WebSocket message with the following format:
|
||||
//!
|
||||
//! ```text
|
||||
//! Byte 0: Type (0x02 = audio)
|
||||
//! Bytes 1-4: Timestamp (u32 LE, milliseconds since stream start)
|
||||
//! Bytes 5-6: Duration (u16 LE, milliseconds)
|
||||
//! Bytes 7-10: Sequence (u32 LE)
|
||||
//! Bytes 11-14: Data length (u32 LE)
|
||||
//! Bytes 15+: Opus encoded data
|
||||
//! ```
|
||||
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
State,
|
||||
},
|
||||
response::Response,
|
||||
};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::audio::OpusFrame;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Audio packet type identifier
|
||||
const AUDIO_PACKET_TYPE: u8 = 0x02;
|
||||
|
||||
/// Audio WebSocket upgrade handler
|
||||
///
|
||||
/// Upgrades HTTP connection to WebSocket for audio streaming.
|
||||
pub async fn audio_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Response {
|
||||
ws.on_upgrade(move |socket| handle_audio_socket(socket, state))
|
||||
}
|
||||
|
||||
/// Handle audio WebSocket connection
|
||||
async fn handle_audio_socket(socket: WebSocket, state: Arc<AppState>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Try to get Opus frame subscription
|
||||
let opus_rx = match state.audio.subscribe_opus_async().await {
|
||||
Some(rx) => rx,
|
||||
None => {
|
||||
warn!("Audio not streaming, rejecting WebSocket connection");
|
||||
// Send error message before closing
|
||||
let _ = sender
|
||||
.send(Message::Text(
|
||||
r#"{"error": "Audio not streaming"}"#.to_string(),
|
||||
))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut opus_rx = opus_rx;
|
||||
let stream_start = Instant::now();
|
||||
|
||||
info!("Audio WebSocket client connected");
|
||||
|
||||
// Track connection for cleanup
|
||||
let mut closed = false;
|
||||
|
||||
// Use interval instead of sleep for more efficient keepalive
|
||||
let mut ping_interval = tokio::time::interval(std::time::Duration::from_secs(30));
|
||||
ping_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Receive Opus frames and send to client
|
||||
opus_result = opus_rx.recv() => {
|
||||
match opus_result {
|
||||
Ok(frame) => {
|
||||
let binary = encode_audio_packet(&frame, stream_start);
|
||||
if sender.send(Message::Binary(binary)).await.is_err() {
|
||||
debug!("Failed to send audio frame, client disconnected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
warn!("Audio WebSocket client lagged by {} frames", n);
|
||||
// Continue - just skip the missed frames
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
info!("Audio stream closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle client messages (ping/close)
|
||||
msg = receiver.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Close(_))) => {
|
||||
debug!("Audio WebSocket client requested close");
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
if sender.send(Message::Pong(data)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Pong(_))) => {
|
||||
// Pong received, connection is alive
|
||||
}
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
// Handle potential control messages
|
||||
debug!("Received text message on audio WS: {}", text);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
warn!("Audio WebSocket receive error: {}", e);
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
// Connection closed
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic ping to keep connection alive (using interval)
|
||||
_ = ping_interval.tick() => {
|
||||
if sender.send(Message::Ping(vec![])).await.is_err() {
|
||||
warn!("Failed to send ping, disconnecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !closed {
|
||||
// Try to send close message
|
||||
let _ = sender.send(Message::Close(None)).await;
|
||||
}
|
||||
|
||||
info!("Audio WebSocket client disconnected");
|
||||
}
|
||||
|
||||
/// Encode Opus frame to binary packet format
|
||||
///
|
||||
/// ## Format
|
||||
///
|
||||
/// | Offset | Size | Description |
|
||||
/// |--------|------|-------------|
|
||||
/// | 0 | 1 | Packet type (0x02 for audio) |
|
||||
/// | 1 | 4 | Timestamp (u32 LE, ms since start) |
|
||||
/// | 5 | 2 | Duration (u16 LE, ms) |
|
||||
/// | 7 | 4 | Sequence number (u32 LE) |
|
||||
/// | 11 | 4 | Data length (u32 LE) |
|
||||
/// | 15 | N | Opus encoded data |
|
||||
fn encode_audio_packet(frame: &OpusFrame, stream_start: Instant) -> Vec<u8> {
|
||||
let timestamp_ms = stream_start.elapsed().as_millis() as u32;
|
||||
let data_len = frame.data.len() as u32;
|
||||
|
||||
let mut buf = Vec::with_capacity(15 + frame.data.len());
|
||||
|
||||
// Header
|
||||
buf.push(AUDIO_PACKET_TYPE);
|
||||
buf.extend_from_slice(×tamp_ms.to_le_bytes());
|
||||
buf.extend_from_slice(&(frame.duration_ms as u16).to_le_bytes());
|
||||
buf.extend_from_slice(&(frame.sequence as u32).to_le_bytes());
|
||||
buf.extend_from_slice(&data_len.to_le_bytes());
|
||||
|
||||
// Opus data
|
||||
buf.extend_from_slice(&frame.data);
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
/// Decode audio packet from binary format (for testing/debugging)
|
||||
#[allow(dead_code)]
|
||||
pub fn decode_audio_packet(data: &[u8]) -> Option<AudioPacketHeader> {
|
||||
if data.len() < 15 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if data[0] != AUDIO_PACKET_TYPE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let timestamp = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
|
||||
let duration_ms = u16::from_le_bytes([data[5], data[6]]);
|
||||
let sequence = u32::from_le_bytes([data[7], data[8], data[9], data[10]]);
|
||||
let data_length = u32::from_le_bytes([data[11], data[12], data[13], data[14]]);
|
||||
|
||||
Some(AudioPacketHeader {
|
||||
packet_type: data[0],
|
||||
timestamp,
|
||||
duration_ms,
|
||||
sequence,
|
||||
data_length,
|
||||
})
|
||||
}
|
||||
|
||||
/// Audio packet header (for decoding/testing)
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AudioPacketHeader {
|
||||
pub packet_type: u8,
|
||||
pub timestamp: u32,
|
||||
pub duration_ms: u16,
|
||||
pub sequence: u32,
|
||||
pub data_length: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
|
||||
#[test]
|
||||
fn test_encode_decode_packet() {
|
||||
let frame = OpusFrame {
|
||||
data: Bytes::from(vec![1, 2, 3, 4, 5]),
|
||||
duration_ms: 20,
|
||||
sequence: 42,
|
||||
timestamp: Instant::now(),
|
||||
rtp_timestamp: 0,
|
||||
};
|
||||
|
||||
let stream_start = Instant::now();
|
||||
let encoded = encode_audio_packet(&frame, stream_start);
|
||||
|
||||
assert!(encoded.len() >= 15);
|
||||
assert_eq!(encoded[0], AUDIO_PACKET_TYPE);
|
||||
|
||||
let header = decode_audio_packet(&encoded).unwrap();
|
||||
assert_eq!(header.packet_type, AUDIO_PACKET_TYPE);
|
||||
assert_eq!(header.duration_ms, 20);
|
||||
assert_eq!(header.sequence, 42);
|
||||
assert_eq!(header.data_length, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_invalid_packet() {
|
||||
// Too short
|
||||
assert!(decode_audio_packet(&[]).is_none());
|
||||
assert!(decode_audio_packet(&[0x02; 10]).is_none());
|
||||
|
||||
// Wrong type
|
||||
let mut bad = vec![0x01; 20];
|
||||
assert!(decode_audio_packet(&bad).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user