chore: cleanup
This commit is contained in:
parent
9045bb93e4
commit
e11d7dc787
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1044,7 +1044,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ffmpeg-rs-raw"
|
name = "ffmpeg-rs-raw"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://git.v0l.io/Kieran/ffmpeg-rs-raw.git?rev=df69b2f05da4279e36ad55086d77b45b2caf5174#df69b2f05da4279e36ad55086d77b45b2caf5174"
|
source = "git+https://git.v0l.io/Kieran/ffmpeg-rs-raw.git?rev=a63b88ef3c8f58c7c0ac57d361d06ff0bb3ed385#a63b88ef3c8f58c7c0ac57d361d06ff0bb3ed385"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ffmpeg-sys-the-third",
|
"ffmpeg-sys-the-third",
|
||||||
|
@ -7,7 +7,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
ffmpeg-rs-raw = { git = "https://git.v0l.io/Kieran/ffmpeg-rs-raw.git", rev = "df69b2f05da4279e36ad55086d77b45b2caf5174" }
|
ffmpeg-rs-raw = { git = "https://git.v0l.io/Kieran/ffmpeg-rs-raw.git", rev = "a63b88ef3c8f58c7c0ac57d361d06ff0bb3ed385" }
|
||||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
anyhow = { version = "^1.0.91", features = ["backtrace"] }
|
anyhow = { version = "^1.0.91", features = ["backtrace"] }
|
||||||
async-trait = "0.1.77"
|
async-trait = "0.1.77"
|
||||||
|
3
TODO.md
3
TODO.md
@ -1,5 +1,4 @@
|
|||||||
- RTMP?
|
|
||||||
- Setup multi-variant output
|
- Setup multi-variant output
|
||||||
- API parity https://git.v0l.io/Kieran/zap.stream/issues/7
|
- API parity https://git.v0l.io/Kieran/zap.stream/issues/7
|
||||||
- HLS-LL
|
- HLS-LL
|
||||||
- Delete old segments (HLS+N94)
|
- Delete old segments (N94)
|
@ -144,7 +144,7 @@ impl PipelineRunner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// run transcoder pipeline
|
// run transcoder pipeline
|
||||||
let (mut pkt, stream) = self.demuxer.get_packet()?;
|
let (mut pkt, _stream) = self.demuxer.get_packet()?;
|
||||||
if pkt.is_null() {
|
if pkt.is_null() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@ -159,7 +159,7 @@ impl PipelineRunner {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut egress_results = vec![];
|
let mut egress_results = vec![];
|
||||||
for frame in frames {
|
for (frame, stream) in frames {
|
||||||
// Copy frame from GPU if using hwaccel decoding
|
// Copy frame from GPU if using hwaccel decoding
|
||||||
let mut frame = get_frame_from_hw(frame)?;
|
let mut frame = get_frame_from_hw(frame)?;
|
||||||
(*frame).time_base = (*stream).time_base;
|
(*frame).time_base = (*stream).time_base;
|
||||||
|
@ -3,6 +3,7 @@ use anyhow::Result;
|
|||||||
use sqlx::{Executor, MySqlPool, Row};
|
use sqlx::{Executor, MySqlPool, Row};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ZapStreamDb {
|
pub struct ZapStreamDb {
|
||||||
db: MySqlPool,
|
db: MySqlPool,
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["srt", "rtmp"]
|
default = ["srt", "rtmp", "test-pattern"]
|
||||||
srt = ["zap-stream-core/srt"]
|
srt = ["zap-stream-core/srt"]
|
||||||
rtmp = ["zap-stream-core/rtmp"]
|
rtmp = ["zap-stream-core/rtmp"]
|
||||||
test-pattern = ["zap-stream-core/test-pattern", "zap-stream-db/test-pattern"]
|
test-pattern = ["zap-stream-core/test-pattern", "zap-stream-db/test-pattern"]
|
||||||
|
@ -5,6 +5,10 @@ endpoints:
|
|||||||
- "rtmp://127.0.0.1:3336"
|
- "rtmp://127.0.0.1:3336"
|
||||||
- "srt://127.0.0.1:3335"
|
- "srt://127.0.0.1:3335"
|
||||||
- "tcp://127.0.0.1:3334"
|
- "tcp://127.0.0.1:3334"
|
||||||
|
- "test-pattern://"
|
||||||
|
|
||||||
|
# Public hostname which points to the IP address used to listen for all [endpoints]
|
||||||
|
endpoints_public_hostname: "localhost"
|
||||||
|
|
||||||
# Output directory for recording / hls
|
# Output directory for recording / hls
|
||||||
output_dir: "./out"
|
output_dir: "./out"
|
||||||
@ -40,8 +44,8 @@ overseer:
|
|||||||
zap-stream:
|
zap-stream:
|
||||||
cost: 16
|
cost: 16
|
||||||
nsec: "nsec1wya428srvpu96n4h78gualaj7wqw4ecgatgja8d5ytdqrxw56r2se440y4"
|
nsec: "nsec1wya428srvpu96n4h78gualaj7wqw4ecgatgja8d5ytdqrxw56r2se440y4"
|
||||||
blossom:
|
#blossom:
|
||||||
- "http://localhost:8881"
|
# - "http://localhost:8881"
|
||||||
relays:
|
relays:
|
||||||
- "ws://localhost:7766"
|
- "ws://localhost:7766"
|
||||||
database: "mysql://root:root@localhost:3368/zap_stream?max_connections=2"
|
database: "mysql://root:root@localhost:3368/zap_stream?max_connections=2"
|
||||||
|
2
crates/zap-stream/dev-setup/db.sql
Normal file
2
crates/zap-stream/dev-setup/db.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
create database route96;
|
||||||
|
create database zap_stream;
|
5
crates/zap-stream/dev-setup/route96.yaml
Normal file
5
crates/zap-stream/dev-setup/route96.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
listen: "0.0.0.0:8000"
|
||||||
|
database: "mysql://root:root@db:3306/route96"
|
||||||
|
storage_dir: "./data"
|
||||||
|
max_upload_bytes: 5e+9
|
||||||
|
public_url: "http://localhost:8881"
|
@ -47,7 +47,7 @@ relay {
|
|||||||
port = 7777
|
port = 7777
|
||||||
|
|
||||||
# Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)
|
# Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)
|
||||||
nofiles = 1000000
|
nofiles = 0
|
||||||
|
|
||||||
# HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)
|
# HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)
|
||||||
realIpHeader = ""
|
realIpHeader = ""
|
||||||
@ -64,6 +64,12 @@ relay {
|
|||||||
|
|
||||||
# NIP-11: Alternative administrative contact (email, website, etc)
|
# NIP-11: Alternative administrative contact (email, website, etc)
|
||||||
contact = ""
|
contact = ""
|
||||||
|
|
||||||
|
# NIP-11: URL pointing to an image to be used as an icon for the relay
|
||||||
|
icon = ""
|
||||||
|
|
||||||
|
# List of supported lists as JSON array, or empty string to use default. Example: "[1,2]"
|
||||||
|
nips = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)
|
# Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)
|
||||||
@ -86,7 +92,7 @@ relay {
|
|||||||
|
|
||||||
writePolicy {
|
writePolicy {
|
||||||
# If non-empty, path to an executable script that implements the writePolicy plugin logic
|
# If non-empty, path to an executable script that implements the writePolicy plugin logic
|
||||||
plugin = "/app/write-policy.py"
|
plugin = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
compression {
|
compression {
|
||||||
@ -135,4 +141,4 @@ relay {
|
|||||||
# Maximum records that sync will process before returning an error
|
# Maximum records that sync will process before returning an error
|
||||||
maxSyncEvents = 1000000
|
maxSyncEvents = 1000000
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,14 +18,14 @@ services:
|
|||||||
blossom:
|
blossom:
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
image: voidic/route96
|
image: voidic/route96:latest
|
||||||
environment:
|
environment:
|
||||||
- "RUST_LOG=info"
|
- "RUST_LOG=info"
|
||||||
ports:
|
ports:
|
||||||
- "8881:8000"
|
- "8881:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- "blossom:/app/data"
|
- "blossom:/app/data"
|
||||||
- "./dev-setup/route96.toml:/app/config.toml"
|
- "./dev-setup/route96.yaml:/app/config.yaml"
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
blossom:
|
blossom:
|
||||||
|
192
crates/zap-stream/src/api.rs
Normal file
192
crates/zap-stream/src/api.rs
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
use crate::http::check_nip98_auth;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::ListenerEndpoint;
|
||||||
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use fedimint_tonic_lnd::tonic::codegen::Body;
|
||||||
|
use http_body_util::combinators::BoxBody;
|
||||||
|
use http_body_util::{BodyExt, Full};
|
||||||
|
use hyper::body::Incoming;
|
||||||
|
use hyper::{Method, Request, Response};
|
||||||
|
use nostr_sdk::{serde_json, Event, PublicKey};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use url::Url;
|
||||||
|
use zap_stream_db::ZapStreamDb;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Api {
|
||||||
|
db: ZapStreamDb,
|
||||||
|
settings: Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Api {
|
||||||
|
pub fn new(db: ZapStreamDb, settings: Settings) -> Self {
|
||||||
|
Self { db, settings }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
self,
|
||||||
|
req: Request<Incoming>,
|
||||||
|
) -> Result<Response<BoxBody<Bytes, anyhow::Error>>, anyhow::Error> {
|
||||||
|
let base = Response::builder()
|
||||||
|
.header("server", "zap-stream")
|
||||||
|
.header("access-control-allow-origin", "*")
|
||||||
|
.header("access-control-allow-headers", "*")
|
||||||
|
.header("access-control-allow-methods", "HEAD, GET");
|
||||||
|
|
||||||
|
Ok(match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, "/api/v1/account") => {
|
||||||
|
let auth = check_nip98_auth(&req)?;
|
||||||
|
let rsp = self.get_account(&auth.pubkey).await?;
|
||||||
|
return Ok(base.body(Self::body_json(&rsp)?)?);
|
||||||
|
}
|
||||||
|
(&Method::PATCH, "/api/v1/account") => {
|
||||||
|
let auth = check_nip98_auth(&req)?;
|
||||||
|
let body = req.collect().await?.to_bytes();
|
||||||
|
let r_body: PatchAccount = serde_json::from_slice(&body)?;
|
||||||
|
let rsp = self.update_account(&auth.pubkey, r_body).await?;
|
||||||
|
return Ok(base.body(Self::body_json(&rsp)?)?);
|
||||||
|
}
|
||||||
|
(&Method::GET, "/api/v1/topup") => {
|
||||||
|
let auth = check_nip98_auth(&req)?;
|
||||||
|
let url: Url = req.uri().to_string().parse()?;
|
||||||
|
let amount: usize = url
|
||||||
|
.query_pairs()
|
||||||
|
.find_map(|(k, v)| if k == "amount" { Some(v) } else { None })
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.ok_or(anyhow!("Missing amount"))?;
|
||||||
|
let rsp = self.topup(&auth.pubkey, amount).await?;
|
||||||
|
return Ok(base.body(Self::body_json(&rsp)?)?);
|
||||||
|
}
|
||||||
|
(&Method::PATCH, "/api/v1/event") => {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
(&Method::POST, "/api/v1/withdraw") => {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
(&Method::POST, "/api/v1/account/forward") => {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
(&Method::DELETE, "/api/v1/account/forward/<id>") => {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
(&Method::GET, "/api/v1/account/history") => {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
(&Method::GET, "/api/v1/account/keys") => {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if req.method() == Method::OPTIONS {
|
||||||
|
base.body(Default::default())?
|
||||||
|
} else {
|
||||||
|
base.status(404).body(Default::default())?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body_json<T: Serialize>(obj: &T) -> Result<BoxBody<Bytes, anyhow::Error>> {
|
||||||
|
Ok(Full::from(serde_json::to_string(obj)?)
|
||||||
|
.map_err(|e| match e {})
|
||||||
|
.boxed())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_account(&self, pubkey: &PublicKey) -> Result<AccountInfo> {
|
||||||
|
let uid = self.db.upsert_user(&pubkey.to_bytes()).await?;
|
||||||
|
let user = self.db.get_user(uid).await?;
|
||||||
|
|
||||||
|
Ok(AccountInfo {
|
||||||
|
endpoints: self
|
||||||
|
.settings
|
||||||
|
.endpoints
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| match ListenerEndpoint::from_str(&e).ok()? {
|
||||||
|
ListenerEndpoint::SRT { endpoint } => {
|
||||||
|
let addr: SocketAddr = endpoint.parse().ok()?;
|
||||||
|
Some(Endpoint {
|
||||||
|
name: "SRT".to_string(),
|
||||||
|
url: format!("srt://{}:{}", self.settings.endpoints_public_hostname, addr.port()),
|
||||||
|
key: user.stream_key.clone(),
|
||||||
|
capabilities: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ListenerEndpoint::RTMP { endpoint } => {
|
||||||
|
let addr: SocketAddr = endpoint.parse().ok()?;
|
||||||
|
Some(Endpoint {
|
||||||
|
name: "RTMP".to_string(),
|
||||||
|
url: format!("rtmp://{}:{}", self.settings.endpoints_public_hostname, addr.port()),
|
||||||
|
key: user.stream_key.clone(),
|
||||||
|
capabilities: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ListenerEndpoint::TCP { endpoint } => {
|
||||||
|
let addr: SocketAddr = endpoint.parse().ok()?;
|
||||||
|
Some(Endpoint {
|
||||||
|
name: "TCP".to_string(),
|
||||||
|
url: format!("tcp://{}:{}", self.settings.endpoints_public_hostname, addr.port()),
|
||||||
|
key: user.stream_key.clone(),
|
||||||
|
capabilities: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ListenerEndpoint::File { .. } => None,
|
||||||
|
ListenerEndpoint::TestPattern => None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
event: None,
|
||||||
|
balance: user.balance as u64,
|
||||||
|
tos: AccountTos {
|
||||||
|
accepted: user.tos_accepted.is_some(),
|
||||||
|
link: "https://zap.stream/tos".to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_account(&self, pubkey: &PublicKey, account: PatchAccount) -> Result<()> {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn topup(&self, pubkey: &PublicKey, amount: usize) -> Result<TopupResponse> {
|
||||||
|
bail!("Not implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct AccountInfo {
|
||||||
|
pub endpoints: Vec<Endpoint>,
|
||||||
|
pub event: Option<Event>,
|
||||||
|
pub balance: u64,
|
||||||
|
pub tos: AccountTos,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct Endpoint {
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub key: String,
|
||||||
|
pub capabilities: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct EndpointCost {
|
||||||
|
pub unit: String,
|
||||||
|
pub rate: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct AccountTos {
|
||||||
|
pub accepted: bool,
|
||||||
|
pub link: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct PatchAccount {
|
||||||
|
pub accept_tos: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct TopupResponse {
|
||||||
|
pub pr: String,
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
|
use crate::api::Api;
|
||||||
|
use crate::overseer::ZapStreamOverseer;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use base64::Engine;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::TryStreamExt;
|
use futures_util::TryStreamExt;
|
||||||
use http_body_util::combinators::BoxBody;
|
use http_body_util::combinators::BoxBody;
|
||||||
@ -5,7 +9,8 @@ use http_body_util::{BodyExt, Full, StreamBody};
|
|||||||
use hyper::body::{Frame, Incoming};
|
use hyper::body::{Frame, Incoming};
|
||||||
use hyper::service::Service;
|
use hyper::service::Service;
|
||||||
use hyper::{Method, Request, Response};
|
use hyper::{Method, Request, Response};
|
||||||
use log::error;
|
use log::{error, info};
|
||||||
|
use nostr_sdk::{serde_json, Event};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@ -13,21 +18,20 @@ use std::sync::Arc;
|
|||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use zap_stream_core::overseer::Overseer;
|
use zap_stream_core::overseer::Overseer;
|
||||||
use crate::overseer::ZapStreamOverseer;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HttpServer {
|
pub struct HttpServer {
|
||||||
index: String,
|
index: String,
|
||||||
files_dir: PathBuf,
|
files_dir: PathBuf,
|
||||||
overseer: Arc<ZapStreamOverseer>,
|
api: Api,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpServer {
|
impl HttpServer {
|
||||||
pub fn new(index: String, files_dir: PathBuf, overseer: Arc<ZapStreamOverseer>) -> Self {
|
pub fn new(index: String, files_dir: PathBuf, api: Api) -> Self {
|
||||||
Self {
|
Self {
|
||||||
index,
|
index,
|
||||||
files_dir,
|
files_dir,
|
||||||
overseer,
|
api,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,9 +85,9 @@ impl Service<Request<Incoming>> for HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// otherwise handle in overseer
|
// otherwise handle in overseer
|
||||||
let overseer = self.overseer.clone();
|
let mut api = self.api.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match overseer.api(req).await {
|
match api.handler(req).await {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
@ -93,3 +97,22 @@ impl Service<Request<Incoming>> for HttpServer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_nip98_auth(req: &Request<Incoming>) -> Result<Event> {
|
||||||
|
let auth = if let Some(a) = req.headers().get("authorization") {
|
||||||
|
a.to_str()?
|
||||||
|
} else {
|
||||||
|
bail!("Authorization header missing");
|
||||||
|
};
|
||||||
|
|
||||||
|
if !auth.starts_with("Nostr ") {
|
||||||
|
bail!("Invalid authorization scheme");
|
||||||
|
}
|
||||||
|
|
||||||
|
let json =
|
||||||
|
String::from_utf8(base64::engine::general_purpose::STANDARD.decode(auth[6..].as_bytes())?)?;
|
||||||
|
info!("{}", json);
|
||||||
|
|
||||||
|
// TODO: check tags
|
||||||
|
Ok(serde_json::from_str::<Event>(&json)?)
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ use hyper_util::rt::TokioIo;
|
|||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
@ -21,13 +22,15 @@ use zap_stream_core::ingress::srt;
|
|||||||
#[cfg(feature = "test-pattern")]
|
#[cfg(feature = "test-pattern")]
|
||||||
use zap_stream_core::ingress::test;
|
use zap_stream_core::ingress::test;
|
||||||
|
|
||||||
use zap_stream_core::ingress::{file, tcp};
|
use crate::api::Api;
|
||||||
use zap_stream_core::overseer::Overseer;
|
|
||||||
use crate::http::HttpServer;
|
use crate::http::HttpServer;
|
||||||
use crate::monitor::BackgroundMonitor;
|
use crate::monitor::BackgroundMonitor;
|
||||||
use crate::overseer::ZapStreamOverseer;
|
use crate::overseer::ZapStreamOverseer;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
use zap_stream_core::ingress::{file, tcp};
|
||||||
|
use zap_stream_core::overseer::Overseer;
|
||||||
|
|
||||||
|
mod api;
|
||||||
mod blossom;
|
mod blossom;
|
||||||
mod http;
|
mod http;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
@ -56,6 +59,7 @@ async fn main() -> Result<()> {
|
|||||||
let settings: Settings = builder.try_deserialize()?;
|
let settings: Settings = builder.try_deserialize()?;
|
||||||
let overseer = settings.get_overseer().await?;
|
let overseer = settings.get_overseer().await?;
|
||||||
|
|
||||||
|
// Create ingress listeners
|
||||||
let mut tasks = vec![];
|
let mut tasks = vec![];
|
||||||
for e in &settings.endpoints {
|
for e in &settings.endpoints {
|
||||||
match try_create_listener(e, &settings.output_dir, &overseer) {
|
match try_create_listener(e, &settings.output_dir, &overseer) {
|
||||||
@ -67,10 +71,12 @@ async fn main() -> Result<()> {
|
|||||||
let http_addr: SocketAddr = settings.listen_http.parse()?;
|
let http_addr: SocketAddr = settings.listen_http.parse()?;
|
||||||
let index_html = include_str!("../index.html").replace("%%PUBLIC_URL%%", &settings.public_url);
|
let index_html = include_str!("../index.html").replace("%%PUBLIC_URL%%", &settings.public_url);
|
||||||
|
|
||||||
|
let api = Api::new(overseer.database(), settings.clone());
|
||||||
|
// HTTP server
|
||||||
let server = HttpServer::new(
|
let server = HttpServer::new(
|
||||||
index_html,
|
index_html,
|
||||||
PathBuf::from(settings.output_dir),
|
PathBuf::from(settings.output_dir),
|
||||||
overseer.clone(),
|
api,
|
||||||
);
|
);
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::spawn(async move {
|
||||||
let listener = TcpListener::bind(&http_addr).await?;
|
let listener = TcpListener::bind(&http_addr).await?;
|
||||||
@ -87,7 +93,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// spawn background job
|
// Background worker
|
||||||
let mut bg = BackgroundMonitor::new(overseer.clone());
|
let mut bg = BackgroundMonitor::new(overseer.clone());
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
@ -98,6 +104,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Join tasks and get errors
|
||||||
for handle in tasks {
|
for handle in tasks {
|
||||||
if let Err(e) = handle.await? {
|
if let Err(e) = handle.await? {
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
@ -107,37 +114,69 @@ async fn main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum ListenerEndpoint {
|
||||||
|
SRT { endpoint: String },
|
||||||
|
RTMP { endpoint: String },
|
||||||
|
TCP { endpoint: String },
|
||||||
|
File { path: PathBuf },
|
||||||
|
TestPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ListenerEndpoint {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
let url: Url = s.parse()?;
|
||||||
|
match url.scheme() {
|
||||||
|
"srt" => Ok(Self::SRT {
|
||||||
|
endpoint: format!("{}:{}", url.host().unwrap(), url.port().unwrap()),
|
||||||
|
}),
|
||||||
|
"rtmp" => Ok(Self::RTMP {
|
||||||
|
endpoint: format!("{}:{}", url.host().unwrap(), url.port().unwrap()),
|
||||||
|
}),
|
||||||
|
"tcp" => Ok(Self::TCP {
|
||||||
|
endpoint: format!("{}:{}", url.host().unwrap(), url.port().unwrap()),
|
||||||
|
}),
|
||||||
|
"file" => Ok(Self::File {
|
||||||
|
path: PathBuf::from(url.path()),
|
||||||
|
}),
|
||||||
|
"test-pattern" => Ok(Self::TestPattern),
|
||||||
|
_ => bail!("Unsupported endpoint scheme: {}", url.scheme()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn try_create_listener(
|
fn try_create_listener(
|
||||||
u: &str,
|
u: &str,
|
||||||
out_dir: &str,
|
out_dir: &str,
|
||||||
overseer: &Arc<ZapStreamOverseer>,
|
overseer: &Arc<ZapStreamOverseer>,
|
||||||
) -> Result<JoinHandle<Result<()>>> {
|
) -> Result<JoinHandle<Result<()>>> {
|
||||||
let url: Url = u.parse()?;
|
let ep = ListenerEndpoint::from_str(u)?;
|
||||||
match url.scheme() {
|
match ep {
|
||||||
#[cfg(feature = "srt")]
|
#[cfg(feature = "srt")]
|
||||||
"srt" => Ok(tokio::spawn(srt::listen(
|
ListenerEndpoint::SRT { endpoint } => Ok(tokio::spawn(srt::listen(
|
||||||
out_dir.to_string(),
|
out_dir.to_string(),
|
||||||
format!("{}:{}", url.host().unwrap(), url.port().unwrap()),
|
endpoint,
|
||||||
overseer.clone(),
|
overseer.clone(),
|
||||||
))),
|
))),
|
||||||
#[cfg(feature = "rtmp")]
|
#[cfg(feature = "rtmp")]
|
||||||
"rtmp" => Ok(tokio::spawn(rtmp::listen(
|
ListenerEndpoint::RTMP { endpoint } => Ok(tokio::spawn(rtmp::listen(
|
||||||
out_dir.to_string(),
|
out_dir.to_string(),
|
||||||
format!("{}:{}", url.host().unwrap(), url.port().unwrap()),
|
endpoint,
|
||||||
overseer.clone(),
|
overseer.clone(),
|
||||||
))),
|
))),
|
||||||
"tcp" => Ok(tokio::spawn(tcp::listen(
|
ListenerEndpoint::TCP { endpoint } => Ok(tokio::spawn(tcp::listen(
|
||||||
out_dir.to_string(),
|
out_dir.to_string(),
|
||||||
format!("{}:{}", url.host().unwrap(), url.port().unwrap()),
|
endpoint,
|
||||||
overseer.clone(),
|
overseer.clone(),
|
||||||
))),
|
))),
|
||||||
"file" => Ok(tokio::spawn(file::listen(
|
ListenerEndpoint::File { path } => Ok(tokio::spawn(file::listen(
|
||||||
out_dir.to_string(),
|
out_dir.to_string(),
|
||||||
PathBuf::from(url.path()),
|
path,
|
||||||
overseer.clone(),
|
overseer.clone(),
|
||||||
))),
|
))),
|
||||||
#[cfg(feature = "test-pattern")]
|
#[cfg(feature = "test-pattern")]
|
||||||
"test-pattern" => Ok(tokio::spawn(test::listen(
|
ListenerEndpoint::TestPattern => Ok(tokio::spawn(test::listen(
|
||||||
out_dir.to_string(),
|
out_dir.to_string(),
|
||||||
overseer.clone(),
|
overseer.clone(),
|
||||||
))),
|
))),
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
use crate::blossom::{BlobDescriptor, Blossom};
|
use crate::blossom::{BlobDescriptor, Blossom};
|
||||||
use zap_stream_core::egress::hls::HlsEgress;
|
use crate::settings::LndSettings;
|
||||||
use zap_stream_core::egress::EgressConfig;
|
|
||||||
use zap_stream_core::ingress::ConnectionInfo;
|
|
||||||
use zap_stream_core::overseer::{IngressInfo, IngressStreamType, Overseer};
|
|
||||||
use zap_stream_core::pipeline::{EgressType, PipelineConfig};
|
|
||||||
use zap_stream_core::variant::{StreamMapping, VariantStream};
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use base64::alphabet::STANDARD;
|
use base64::alphabet::STANDARD;
|
||||||
@ -14,6 +9,7 @@ use chrono::Utc;
|
|||||||
use fedimint_tonic_lnd::verrpc::VersionRequest;
|
use fedimint_tonic_lnd::verrpc::VersionRequest;
|
||||||
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVCodecID::AV_CODEC_ID_MJPEG;
|
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVCodecID::AV_CODEC_ID_MJPEG;
|
||||||
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVFrame;
|
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVFrame;
|
||||||
|
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPixelFormat::AV_PIX_FMT_YUV420P;
|
||||||
use ffmpeg_rs_raw::Encoder;
|
use ffmpeg_rs_raw::Encoder;
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use http_body_util::combinators::BoxBody;
|
use http_body_util::combinators::BoxBody;
|
||||||
@ -31,16 +27,20 @@ use std::fs::create_dir_all;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ffmpeg_rs_raw::ffmpeg_sys_the_third::AVPixelFormat::AV_PIX_FMT_YUV420P;
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use zap_stream_core::egress::hls::HlsEgress;
|
||||||
|
use zap_stream_core::egress::EgressConfig;
|
||||||
|
use zap_stream_core::ingress::ConnectionInfo;
|
||||||
|
use zap_stream_core::overseer::{IngressInfo, IngressStreamType, Overseer};
|
||||||
|
use zap_stream_core::pipeline::{EgressType, PipelineConfig};
|
||||||
use zap_stream_core::variant::audio::AudioVariant;
|
use zap_stream_core::variant::audio::AudioVariant;
|
||||||
use zap_stream_core::variant::mapping::VariantMapping;
|
use zap_stream_core::variant::mapping::VariantMapping;
|
||||||
use zap_stream_core::variant::video::VideoVariant;
|
use zap_stream_core::variant::video::VideoVariant;
|
||||||
|
use zap_stream_core::variant::{StreamMapping, VariantStream};
|
||||||
use zap_stream_db::sqlx::Encode;
|
use zap_stream_db::sqlx::Encode;
|
||||||
use zap_stream_db::{UserStream, UserStreamState, ZapStreamDb};
|
use zap_stream_db::{UserStream, UserStreamState, ZapStreamDb};
|
||||||
use crate::settings::LndSettings;
|
|
||||||
|
|
||||||
const STREAM_EVENT_KIND: u16 = 30_313;
|
const STREAM_EVENT_KIND: u16 = 30_313;
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ impl ZapStreamOverseer {
|
|||||||
PathBuf::from(&lnd.cert),
|
PathBuf::from(&lnd.cert),
|
||||||
PathBuf::from(&lnd.macaroon),
|
PathBuf::from(&lnd.macaroon),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let version = lnd
|
let version = lnd
|
||||||
.versioner()
|
.versioner()
|
||||||
@ -133,50 +133,8 @@ impl ZapStreamOverseer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn api(&self, req: Request<Incoming>) -> Result<Response<BoxBody<Bytes, anyhow::Error>>> {
|
pub(crate) fn database(&self) -> ZapStreamDb {
|
||||||
let base = Response::builder()
|
self.db.clone()
|
||||||
.header("server", "zap-stream-core")
|
|
||||||
.header("access-control-allow-origin", "*")
|
|
||||||
.header("access-control-allow-headers", "*")
|
|
||||||
.header("access-control-allow-methods", "HEAD, GET");
|
|
||||||
|
|
||||||
Ok(match (req.method(), req.uri().path()) {
|
|
||||||
(&Method::GET, "/api/v1/account") => {
|
|
||||||
self.check_nip98_auth(req)?;
|
|
||||||
base.body(Default::default())?
|
|
||||||
}
|
|
||||||
(&Method::PATCH, "/api/v1/account") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::GET, "/api/v1/topup") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::PATCH, "/api/v1/event") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::POST, "/api/v1/withdraw") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::POST, "/api/v1/account/forward") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::DELETE, "/api/v1/account/forward/<id>") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::GET, "/api/v1/account/history") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
(&Method::GET, "/api/v1/account/keys") => {
|
|
||||||
bail!("Not implemented")
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if req.method() == Method::OPTIONS {
|
|
||||||
base.body(Default::default())?
|
|
||||||
} else {
|
|
||||||
base.status(404).body(Default::default())?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_to_event_builder(&self, stream: &UserStream) -> Result<EventBuilder> {
|
fn stream_to_event_builder(&self, stream: &UserStream) -> Result<EventBuilder> {
|
||||||
@ -280,25 +238,6 @@ impl ZapStreamOverseer {
|
|||||||
let u: Url = self.public_url.parse()?;
|
let u: Url = self.public_url.parse()?;
|
||||||
Ok(u.join(path)?.to_string())
|
Ok(u.join(path)?.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_nip98_auth(&self, req: Request<Incoming>) -> Result<()> {
|
|
||||||
let auth = if let Some(a) = req.headers().get("authorization") {
|
|
||||||
a.to_str()?
|
|
||||||
} else {
|
|
||||||
bail!("Authorization header missing");
|
|
||||||
};
|
|
||||||
|
|
||||||
if !auth.starts_with("Nostr ") {
|
|
||||||
bail!("Invalid authorization scheme");
|
|
||||||
}
|
|
||||||
|
|
||||||
let json = String::from_utf8(
|
|
||||||
base64::engine::general_purpose::STANDARD.decode(auth[6..].as_bytes())?,
|
|
||||||
)?;
|
|
||||||
info!("{}", json);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -459,7 +398,6 @@ impl Overseer for ZapStreamOverseer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn get_default_variants(info: &IngressInfo) -> Result<Vec<VariantStream>> {
|
fn get_default_variants(info: &IngressInfo) -> Result<Vec<VariantStream>> {
|
||||||
let mut vars: Vec<VariantStream> = vec![];
|
let mut vars: Vec<VariantStream> = vec![];
|
||||||
if let Some(video_src) = info
|
if let Some(video_src) = info
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use crate::overseer::ZapStreamOverseer;
|
use crate::overseer::ZapStreamOverseer;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use zap_stream_core::overseer::Overseer;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
@ -12,6 +11,9 @@ pub struct Settings {
|
|||||||
/// - rtmp://localhost:1935
|
/// - rtmp://localhost:1935
|
||||||
pub endpoints: Vec<String>,
|
pub endpoints: Vec<String>,
|
||||||
|
|
||||||
|
/// Public facing hostname that maps to [endpoints]
|
||||||
|
pub endpoints_public_hostname: String,
|
||||||
|
|
||||||
/// Where to store output (static files)
|
/// Where to store output (static files)
|
||||||
pub output_dir: String,
|
pub output_dir: String,
|
||||||
|
|
||||||
@ -21,7 +23,7 @@ pub struct Settings {
|
|||||||
/// Binding address for http server serving files from [output_dir]
|
/// Binding address for http server serving files from [output_dir]
|
||||||
pub listen_http: String,
|
pub listen_http: String,
|
||||||
|
|
||||||
/// Overseer service see [crate::overseer::Overseer] for more info
|
/// Overseer service see [Overseer] for more info
|
||||||
pub overseer: OverseerConfig,
|
pub overseer: OverseerConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user