This commit is contained in:
2026-08-29 15:15:28 +01:00
parent 98dbb3d36d
commit c4a7e622b7
10 changed files with 99 additions and 56 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
use std::error::Error; use std::error::Error;
use bytes::Bytes; use bytes::Bytes;
use rubato::{audioadapter_buffers::direct::InterleavedSlice, Fft, Resampler}; use rubato::{Fft, Resampler, audioadapter_buffers::direct::InterleavedSlice};
use symphonia::core::{ use symphonia::core::{
audio::SampleBuffer, audio::SampleBuffer,
codecs::{CodecParameters, Decoder, DecoderOptions, CODEC_TYPE_AAC}, codecs::{CODEC_TYPE_AAC, CodecParameters, Decoder, DecoderOptions},
formats::Packet, formats::Packet,
}; };
+28 -13
View File
@@ -50,7 +50,9 @@ impl Av1CodecParser {
let has_size = (header >> 1) & 1 != 0; let has_size = (header >> 1) & 1 != 0;
i += 1; i += 1;
if has_extension { if has_extension {
if i >= data.len() { return false; } if i >= data.len() {
return false;
}
i += 1; i += 1;
} }
if obu_type == 1 { if obu_type == 1 {
@@ -61,13 +63,19 @@ impl Av1CodecParser {
let mut size: usize = 0; let mut size: usize = 0;
let mut shift = 0; let mut shift = 0;
loop { loop {
if i >= data.len() { return false; } if i >= data.len() {
return false;
}
let b = data[i] as usize; let b = data[i] as usize;
i += 1; i += 1;
size |= (b & 0x7F) << shift; size |= (b & 0x7F) << shift;
shift += 7; shift += 7;
if b & 0x80 == 0 { break; } if b & 0x80 == 0 {
if shift > 32 { return false; } break;
}
if shift > 32 {
return false;
}
} }
i += size; i += size;
} else { } else {
@@ -78,7 +86,11 @@ impl Av1CodecParser {
false false
} }
fn obus_for_frame(&mut self, payload: &[u8], rtmp_is_keyframe: bool) -> Option<(Vec<u8>, bool)> { fn obus_for_frame(
&mut self,
payload: &[u8],
rtmp_is_keyframe: bool,
) -> Option<(Vec<u8>, bool)> {
if payload.is_empty() { if payload.is_empty() {
return None; return None;
} }
@@ -95,13 +107,12 @@ impl Av1CodecParser {
self.first_coded_frame = false; self.first_coded_frame = false;
if is_keyframe if is_keyframe && let Some(config) = &self.config_obus {
&& let Some(config) = &self.config_obus { let mut out = Vec::with_capacity(config.len() + payload.len());
let mut out = Vec::with_capacity(config.len() + payload.len()); out.extend_from_slice(config);
out.extend_from_slice(config); out.extend_from_slice(payload);
out.extend_from_slice(payload); return Some((out, true));
return Some((out, true)); }
}
Some((payload.to_vec(), is_keyframe)) Some((payload.to_vec(), is_keyframe))
} }
@@ -129,7 +140,11 @@ impl CodecParser for Av1CodecParser {
// FourCC — enhanced RTMP only defines CTS for hvc1 CodedFrames. // FourCC — enhanced RTMP only defines CTS for hvc1 CodedFrames.
let payload = data.get(5..)?; let payload = data.get(5..)?;
let (obus, is_keyframe) = self.obus_for_frame(payload, rtmp_is_keyframe)?; let (obus, is_keyframe) = self.obus_for_frame(payload, rtmp_is_keyframe)?;
Some(VideoFrame { data: Bytes::from(obus), is_keyframe, timestamp_ms }) Some(VideoFrame {
data: Bytes::from(obus),
is_keyframe,
timestamp_ms,
})
} }
_ => None, _ => None,
} }
+21 -10
View File
@@ -53,12 +53,20 @@ impl H264CodecParser {
} }
let pts_ms = Self::pts_ms(timestamp_ms, &bytes[5..8]); let pts_ms = Self::pts_ms(timestamp_ms, &bytes[5..8]);
let data = self.avcc_to_annexb(bytes.get(8..)?, is_keyframe)?; let data = self.avcc_to_annexb(bytes.get(8..)?, is_keyframe)?;
Some(VideoFrame { data: Bytes::from(data), is_keyframe, timestamp_ms: pts_ms }) Some(VideoFrame {
data: Bytes::from(data),
is_keyframe,
timestamp_ms: pts_ms,
})
} }
3 => { 3 => {
// CodedFramesX: no CTS field, bytes 5+ = AVCC NALUs // CodedFramesX: no CTS field, bytes 5+ = AVCC NALUs
let data = self.avcc_to_annexb(bytes.get(5..)?, is_keyframe)?; let data = self.avcc_to_annexb(bytes.get(5..)?, is_keyframe)?;
Some(VideoFrame { data: Bytes::from(data), is_keyframe, timestamp_ms }) Some(VideoFrame {
data: Bytes::from(data),
is_keyframe,
timestamp_ms,
})
} }
_ => None, _ => None,
} }
@@ -77,7 +85,11 @@ impl H264CodecParser {
// PTS = DTS + CTS — see note on CodedFrames above. // PTS = DTS + CTS — see note on CodedFrames above.
let pts_ms = Self::pts_ms(timestamp_ms, &bytes[2..5]); let pts_ms = Self::pts_ms(timestamp_ms, &bytes[2..5]);
let data = self.avcc_to_annexb(&bytes[5..], is_keyframe)?; let data = self.avcc_to_annexb(&bytes[5..], is_keyframe)?;
Some(VideoFrame { data: Bytes::from(data), is_keyframe, timestamp_ms: pts_ms }) Some(VideoFrame {
data: Bytes::from(data),
is_keyframe,
timestamp_ms: pts_ms,
})
} }
_ => None, _ => None,
} }
@@ -148,13 +160,12 @@ impl H264CodecParser {
// Prepend SPS+PPS before every keyframe so str0m's packetizer // Prepend SPS+PPS before every keyframe so str0m's packetizer
// can bundle them into a STAP-A alongside the IDR NALU. // can bundle them into a STAP-A alongside the IDR NALU.
if is_keyframe if is_keyframe && let (Some(sps), Some(pps)) = (&self.sps, &self.pps) {
&& let (Some(sps), Some(pps)) = (&self.sps, &self.pps) { out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(&[0, 0, 0, 1]); out.extend_from_slice(sps);
out.extend_from_slice(sps); out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(&[0, 0, 0, 1]); out.extend_from_slice(pps);
out.extend_from_slice(pps); }
}
// Convert each length-prefixed NALU to an Annex B start-code NALU. // Convert each length-prefixed NALU to an Annex B start-code NALU.
let mut i = 0; let mut i = 0;
+19 -11
View File
@@ -74,15 +74,15 @@ impl H265CodecParser {
fn hvcc_to_annexb(&self, payload: &[u8], is_keyframe: bool) -> Option<Vec<u8>> { fn hvcc_to_annexb(&self, payload: &[u8], is_keyframe: bool) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(payload.len()); let mut out = Vec::with_capacity(payload.len());
if is_keyframe if is_keyframe && let (Some(vps), Some(sps), Some(pps)) = (&self.vps, &self.sps, &self.pps)
&& let (Some(vps), Some(sps), Some(pps)) = (&self.vps, &self.sps, &self.pps) { {
out.extend_from_slice(&[0, 0, 0, 1]); out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(vps); out.extend_from_slice(vps);
out.extend_from_slice(&[0, 0, 0, 1]); out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(sps); out.extend_from_slice(sps);
out.extend_from_slice(&[0, 0, 0, 1]); out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(pps); out.extend_from_slice(pps);
} }
let mut i = 0; let mut i = 0;
while i + 4 <= payload.len() { while i + 4 <= payload.len() {
@@ -128,12 +128,20 @@ impl CodecParser for H265CodecParser {
let cts = i32::from_be_bytes([0, data[5], data[6], data[7]]) << 8 >> 8; let cts = i32::from_be_bytes([0, data[5], data[6], data[7]]) << 8 >> 8;
let pts_ms = (timestamp_ms as i64 + cts as i64).max(0) as u32; let pts_ms = (timestamp_ms as i64 + cts as i64).max(0) as u32;
let annexb = self.hvcc_to_annexb(data.get(8..)?, is_keyframe)?; let annexb = self.hvcc_to_annexb(data.get(8..)?, is_keyframe)?;
Some(VideoFrame { data: Bytes::from(annexb), is_keyframe, timestamp_ms: pts_ms }) Some(VideoFrame {
data: Bytes::from(annexb),
is_keyframe,
timestamp_ms: pts_ms,
})
} }
3 => { 3 => {
// CodedFramesX: no CTS, bytes 5+ = HVCC // CodedFramesX: no CTS, bytes 5+ = HVCC
let annexb = self.hvcc_to_annexb(data.get(5..)?, is_keyframe)?; let annexb = self.hvcc_to_annexb(data.get(5..)?, is_keyframe)?;
Some(VideoFrame { data: Bytes::from(annexb), is_keyframe, timestamp_ms }) Some(VideoFrame {
data: Bytes::from(annexb),
is_keyframe,
timestamp_ms,
})
} }
_ => None, _ => None,
} }
+3 -1
View File
@@ -13,7 +13,9 @@ use axum_extra::extract::{CookieJar, cookie::Cookie};
/// dashes and apostrophes — no spaces. Mirrors the frontend /// dashes and apostrophes — no spaces. Mirrors the frontend
/// `/^[A-Za-z0-9'-]{0,67}$/` used by the keys-page popups. /// `/^[A-Za-z0-9'-]{0,67}$/` used by the keys-page popups.
fn valid_charset(s: &str) -> bool { fn valid_charset(s: &str) -> bool {
s.len() <= 67 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '\'') s.len() <= 67
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '\'')
} }
use axum::{ use axum::{
+1 -1
View File
@@ -42,7 +42,7 @@ impl HttpError {
Self::BadRequest(_) => StatusCode::BAD_REQUEST, Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::Unprocessable(_) => StatusCode::UNPROCESSABLE_ENTITY, Self::Unprocessable(_) => StatusCode::UNPROCESSABLE_ENTITY,
Self::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE, Self::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
Self::WhepCodecError(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE, Self::WhepCodecError(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
} }
} }
} }
+1 -4
View File
@@ -16,10 +16,7 @@ use tracing_subscriber::EnvFilter;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
audio::OpusAudioFrame, audio::OpusAudioFrame, codec::VideoFrame, http::HttpServer, webrtc_proxy::WebrtcProxy,
codec::VideoFrame,
http::HttpServer,
webrtc_proxy::WebrtcProxy,
}; };
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
+7 -1
View File
@@ -71,7 +71,13 @@ pub async fn handle_whip_injest_delete(
state.appstate.lock().await.stream_sessions.remove(&key.id); state.appstate.lock().await.stream_sessions.remove(&key.id);
// Drop the trickle channel too, so PATCHes to a deleted session 404 and // Drop the trickle channel too, so PATCHes to a deleted session 404 and
// the lingering detach task's own cleanup can't reach a newer session. // the lingering detach task's own cleanup can't reach a newer session.
state.appstate.lock().await.webrtc_proxy.trickle_tx.remove(&key.id); state
.appstate
.lock()
.await
.webrtc_proxy
.trickle_tx
.remove(&key.id);
Ok(StatusCode::OK) Ok(StatusCode::OK)
} }
+16 -12
View File
@@ -34,10 +34,10 @@ impl WebrtcProxy {
let sock = UdpSocket::bind(format!("0.0.0.0:{}", proxy_port)).await?; let sock = UdpSocket::bind(format!("0.0.0.0:{}", proxy_port)).await?;
let port = sock.local_addr()?.port(); let port = sock.local_addr()?.port();
let public_ip = match env::var("PUBLIC_DOMAIN") let public_ip = match env::var("PUBLIC_DOMAIN")
.ok() .ok()
.filter(|s| !s.trim().is_empty()) .filter(|s| !s.trim().is_empty())
{ {
Some(domain) => { Some(domain) => {
let ip = resolve_domain(&domain).await?; let ip = resolve_domain(&domain).await?;
info!(%domain, %ip, "resolved PUBLIC_DOMAIN for WebRTC candidates"); info!(%domain, %ip, "resolved PUBLIC_DOMAIN for WebRTC candidates");
@@ -51,12 +51,14 @@ impl WebrtcProxy {
// IP_UNICAST_IF), which makes checks to 127.0.0.1 vanish. // IP_UNICAST_IF), which makes checks to 127.0.0.1 vanish.
match default_iface_ipv4() { match default_iface_ipv4() {
Some(ip) => { Some(ip) => {
info!(%ip, "using default interface IP for WebRTC candidates (debug build)"); info!(%ip, "using default interface IP for WebRTC candidates (debug build)");
ip ip
} }
None => { None => {
warn!("no non-loopback IPv4 interface found, falling back to 127.0.0.1"); warn!(
IpAddr::from([127, 0, 0, 1]) "no non-loopback IPv4 interface found, falling back to 127.0.0.1"
);
IpAddr::from([127, 0, 0, 1])
} }
} }
} else { } else {
@@ -200,9 +202,9 @@ fn stun_attributes(data: &[u8]) -> impl Iterator<Item = (u16, &[u8])> {
/// connect() only does a route lookup (no packets sent), so the kernel binds /// connect() only does a route lookup (no packets sent), so the kernel binds
/// the source address the OS would use for outbound traffic. /// the source address the OS would use for outbound traffic.
fn default_iface_ipv4() -> Option<IpAddr> { fn default_iface_ipv4() -> Option<IpAddr> {
let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?; let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?;
sock.connect("8.8.8.8:9").ok()?; sock.connect("8.8.8.8:9").ok()?;
sock.local_addr().ok().map(|a| a.ip()) sock.local_addr().ok().map(|a| a.ip())
} }
async fn resolve_domain(domain: &str) -> Result<std::net::IpAddr, Box<dyn Error>> { async fn resolve_domain(domain: &str) -> Result<std::net::IpAddr, Box<dyn Error>> {
@@ -254,7 +256,9 @@ fn parse_xor_mapped_address(data: &[u8]) -> Option<std::net::IpAddr> {
// byte 0: reserved, byte 1: family (0x01=IPv4, 0x02=IPv6) // byte 0: reserved, byte 1: family (0x01=IPv4, 0x02=IPv6)
if value[1] == 0x01 { if value[1] == 0x01 {
let x_addr = u32::from_be_bytes(value[4..8].try_into().ok()?); let x_addr = u32::from_be_bytes(value[4..8].try_into().ok()?);
Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(x_addr ^ magic))) Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(
x_addr ^ magic,
)))
} else { } else {
None None
} }
+1 -1
View File
@@ -3,7 +3,7 @@
//! whether the string-fixups in webrtc_ingest.rs actually match. //! whether the string-fixups in webrtc_ingest.rs actually match.
use std::{net::SocketAddr, time::Instant}; use std::{net::SocketAddr, time::Instant};
use str0m::{change::SdpOffer, net::Protocol, Candidate, Rtc}; use str0m::{Candidate, Rtc, change::SdpOffer, net::Protocol};
fn obs_like_offer() -> String { fn obs_like_offer() -> String {
let mut s = String::new(); let mut s = String::new();