Compare commits

...
4 Commits
10 changed files with 101 additions and 30 deletions
Generated
+4 -3
View File
@@ -2856,9 +2856,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.150" version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -2892,7 +2892,7 @@ dependencies = [
[[package]] [[package]]
name = "server" name = "server"
version = "0.5.4" version = "0.6.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"async-broadcast", "async-broadcast",
@@ -2908,6 +2908,7 @@ dependencies = [
"rubato", "rubato",
"sea-orm", "sea-orm",
"serde", "serde",
"serde_json",
"str0m", "str0m",
"symphonia", "symphonia",
"sysinfo", "sysinfo",
+4 -4
View File
@@ -51,20 +51,20 @@ impl Model {
.await .await
} }
pub async fn get_all_active_sessions(db: &DatabaseConnection) -> Result<Vec<Model>, DbErr> { pub async fn get_all_active_sessions(db: &DatabaseConnection) -> Result<Vec<Model>, DbErr> {
Ok(Entity::find() Entity::find()
.filter(Column::EndedAt.is_null()) .filter(Column::EndedAt.is_null())
.all(db) .all(db)
.await?) .await
} }
pub async fn get_active_by_stream_key_id( pub async fn get_active_by_stream_key_id(
db: &DatabaseConnection, db: &DatabaseConnection,
stream_key_id: i32, stream_key_id: i32,
) -> Result<Option<Model>, DbErr> { ) -> Result<Option<Model>, DbErr> {
Ok(Entity::find() Entity::find()
.filter(Column::StreamKeyId.eq(stream_key_id)) .filter(Column::StreamKeyId.eq(stream_key_id))
.filter(Column::EndedAt.is_null()) .filter(Column::EndedAt.is_null())
.one(db) .one(db)
.await?) .await
} }
pub async fn clean_unended_streams(db: &DatabaseConnection) -> Result<u64, DbErr> { pub async fn clean_unended_streams(db: &DatabaseConnection) -> Result<u64, DbErr> {
+4 -4
View File
@@ -58,18 +58,18 @@ impl Entity {
if let Some(x) = sessions { if let Some(x) = sessions {
Entity::find_by_id(x.id_user).one(db).await Entity::find_by_id(x.id_user).one(db).await
} else { } else {
return Ok(None); Ok(None)
} }
} }
pub async fn find_by_username( pub async fn find_by_username(
db: &DatabaseConnection, db: &DatabaseConnection,
username: String, username: String,
) -> Result<Option<Model>, DbErr> { ) -> Result<Option<Model>, DbErr> {
let user = Entity::find()
Entity::find()
.filter(Column::Username.eq(username)) .filter(Column::Username.eq(username))
.one(db) .one(db)
.await; .await
user
} }
} }
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "server" name = "server"
version = "0.5.4" version = "0.6.0"
edition = "2024" edition = "2024"
[target.x86_64-unknown-linux-gnu] [target.x86_64-unknown-linux-gnu]
@@ -36,3 +36,4 @@ time = "0.3"
thiserror = "2.0.18" thiserror = "2.0.18"
sysinfo = "0.36" sysinfo = "0.36"
axum-extra = { version = "0.12.6", features = ["cookie"] } axum-extra = { version = "0.12.6", features = ["cookie"] }
serde_json = "1.0.151"
+1 -1
View File
@@ -44,7 +44,7 @@ impl AudioProcesser {
pub fn encode(&mut self, frame: AudioFrame) -> Vec<OpusAudioFrame> { pub fn encode(&mut self, frame: AudioFrame) -> Vec<OpusAudioFrame> {
let mut samples: Vec<f32> = frame let mut samples: Vec<f32> = frame
.data .data
.chunks_exact(4) .as_chunks::<4>().0.iter()
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])) .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect(); .collect();
+9 -7
View File
@@ -125,9 +125,11 @@ impl HttpServer {
#[derive(Serialize)] #[derive(Serialize)]
struct StreamListing { struct StreamListing {
label: String, label: String,
custom_url_label: String,
id: i32, id: i32,
user: String, user: String,
started_at: DateTime<Utc>, //UNIX TIMESTAMP started_at: DateTime<Utc>,
is_password_protected: bool,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -160,16 +162,14 @@ async fn edit_stream_key(
} }
} }
// Empty values are allowed (they clear the field). // Empty values are allowed (they clear the field).
if let Some(custom_id) = payload.custom_id.as_deref() { if let Some(custom_id) = payload.custom_id.as_deref()
if !custom_id.is_empty() && !valid_charset(custom_id) { && !custom_id.is_empty() && !valid_charset(custom_id) {
return Err(HttpError::BadRequest("invalid custom id".into())); return Err(HttpError::BadRequest("invalid custom id".into()));
} }
} if let Some(pwd) = payload.password.as_deref()
if let Some(pwd) = payload.password.as_deref() { && !pwd.is_empty() && !valid_charset(pwd) {
if !pwd.is_empty() && !valid_charset(pwd) {
return Err(HttpError::BadRequest("invalid password".into())); return Err(HttpError::BadRequest("invalid password".into()));
} }
}
let stream_key = stream_key::Entity::find_by_id(payload.id) let stream_key = stream_key::Entity::find_by_id(payload.id)
.one(&state.db) .one(&state.db)
@@ -252,8 +252,10 @@ async fn catalog_handler(
.map(|x| StreamListing { .map(|x| StreamListing {
id: x.stream_key_id, id: x.stream_key_id,
label: x.stream_key_label.clone(), label: x.stream_key_label.clone(),
custom_url_label: x.custom_id.clone().unwrap_or_default(),
user: x.stream_key_user.clone(), user: x.stream_key_user.clone(),
started_at: x.started_at, started_at: x.started_at,
is_password_protected: x.password.is_some(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(Json(catalog)) Ok(Json(catalog))
+1
View File
@@ -27,6 +27,7 @@ mod hash;
mod http; mod http;
mod http_error; mod http_error;
mod rtmp; mod rtmp;
mod stream_session_2;
mod webrtc; mod webrtc;
mod webrtc_ingest; mod webrtc_ingest;
mod webrtc_proxy; mod webrtc_proxy;
+18
View File
@@ -0,0 +1,18 @@
use serde::Serialize;
use crate::StreamSession;
#[derive(Serialize)]
pub struct StreamUpdateData {
viewers: u32,
}
impl StreamSession {
pub fn stream_update_data(&self) -> StreamUpdateData {
StreamUpdateData {
viewers: self
.active_clients
.load(std::sync::atomic::Ordering::Relaxed),
}
}
}
+49 -1
View File
@@ -2,13 +2,18 @@ use bytes::Bytes;
use dashmap::DashMap; use dashmap::DashMap;
use entity::stream_key; use entity::stream_key;
use sea_orm::{DatabaseConnection, EntityTrait}; use sea_orm::{DatabaseConnection, EntityTrait};
use std::{net::SocketAddr, sync::Arc, time::Instant}; use std::{
net::SocketAddr,
sync::Arc,
time::{Duration, Instant},
};
use tokio::{net::UdpSocket, sync::mpsc::Receiver}; use tokio::{net::UdpSocket, sync::mpsc::Receiver};
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
use str0m::{ use str0m::{
Candidate, Event, Input, Output, Rtc, Candidate, Event, Input, Output, Rtc,
change::SdpOffer, change::SdpOffer,
channel::ChannelId,
media::{Frequency, MediaKind, MediaTime, Mid, Pt}, media::{Frequency, MediaKind, MediaTime, Mid, Pt},
net::{Protocol, Receive}, net::{Protocol, Receive},
}; };
@@ -236,10 +241,34 @@ impl Webrtc {
let mut video_pt = None; let mut video_pt = None;
let mut audio_mid: Option<Mid> = None; let mut audio_mid: Option<Mid> = None;
let mut audio_pt = None; let mut audio_pt = None;
let mut channel_id = None;
let mut connected = false; let mut connected = false;
let mut video_stream: Option<async_broadcast::Receiver<Arc<VideoFrame>>> = None; let mut video_stream: Option<async_broadcast::Receiver<Arc<VideoFrame>>> = None;
let mut audio_stream: Option<async_broadcast::Receiver<Arc<OpusAudioFrame>>> = None; let mut audio_stream: Option<async_broadcast::Receiver<Arc<OpusAudioFrame>>> = None;
let mut saw_keyframe = false; let mut saw_keyframe = false;
// Update client with stream info through webrtc data channel ()
let mut tick = tokio::time::interval(Duration::from_millis(2000));
if let Some(ses) = sessions_ref.get(&stream_id) {
ses.active_clients
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
struct ActiveClientGuard {
sessions: Arc<DashMap<i32, StreamSession>>,
id: i32,
}
impl Drop for ActiveClientGuard {
fn drop(&mut self) {
if let Some(ses) = self.sessions.get(&self.id) {
ses.active_clients
.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
}
let _active_client_guard = ActiveClientGuard {
sessions: sessions_ref.clone(),
id: stream_id,
};
loop { loop {
let deadline = loop { let deadline = loop {
@@ -252,6 +281,9 @@ impl Webrtc {
} }
} }
Ok(Output::Event(e)) => match e { Ok(Output::Event(e)) => match e {
Event::ChannelOpen(channelId, _name) => {
channel_id = Some(channelId);
}
Event::MediaAdded(ma) => { Event::MediaAdded(ma) => {
info!(stream_id, kind = ?ma.kind, mid = ?ma.mid, "MediaAdded"); info!(stream_id, kind = ?ma.kind, mid = ?ma.mid, "MediaAdded");
if ma.kind == MediaKind::Video { if ma.kind == MediaKind::Video {
@@ -431,6 +463,14 @@ impl Webrtc {
Err(async_broadcast::RecvError::Overflowed(_)) => {} Err(async_broadcast::RecvError::Overflowed(_)) => {}
} }
} }
_interval = tick.tick() => {
if let Some(id) = channel_id
&& let Some(session) = sessions_ref.get(&stream_id)
&& let Ok(json) = serde_json::to_vec(&session.stream_update_data())
&& let Some(mut ch) = rtc.channel(id) {
let _ = ch.write(false, &json);
}
}
} }
} }
} }
@@ -497,4 +537,12 @@ impl Webrtc {
warn!("RTP write error: {:?}", e); warn!("RTP write error: {:?}", e);
} }
} }
fn write_channel_data(rtc: &mut Rtc, channel_id: ChannelId, data: &[u8]) {
if let Some(mut channel) = rtc.channel(channel_id)
&& let Err(e) = channel.write(false, data)
{
warn!("Channel write error: {:?}", e)
}
}
} }
Generated
+9 -9
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1785284101, "lastModified": 1788465171,
"narHash": "sha256-ghcXEpYEM4a7pbEkoqbn8c0ptJJqgGzuFiG3T6W5g4I=", "narHash": "sha256-Y1/TTVXjYXGF068IThQH9fPSZ0SIE74PABlUxnWTUH0=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "756d6d07c3818ea95d1e2cdac63fa7d02fe3e61b", "rev": "eb35abda9f232cc6610b1d1e3200d15c49b7ac54",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -35,11 +35,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1785301185, "lastModified": 1789012029,
"narHash": "sha256-eoS3KQTO0aPWXZvIaRbRAzSSHW3l5wdMFXtT1ISfoKA=", "narHash": "sha256-1CBkBf+Nhggykzlx0jvXrj5rk20btl+tK8Fa8Ml/BL4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9bc02893134c733dd85de46ee4fb2fac696b5529", "rev": "d5dfd8e6716dde34398bc14bc87c10dece9c8c68",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -64,11 +64,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786160316, "lastModified": 1789024335,
"narHash": "sha256-oLoc3ZLg1LX/S5Jb3v6MrF415AzDyC4vgeWy9UcYTQk=", "narHash": "sha256-kCy/MVLRIr95DJ4vspVzWj+kO/x+JuwSHmYZCvShg8w=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "4e1c940c96560ceab7c547f89642231371a66646", "rev": "577bb1e1fc5af0713169176c5c76622c21fa3ec0",
"type": "github" "type": "github"
}, },
"original": { "original": {