add playout service: HLS streaming, FFmpeg, SegmentStore port (#9)

This commit is contained in:
2026-07-12 13:58:09 +02:00
parent de7f3092d2
commit 1b3ecc10e1
13 changed files with 1022 additions and 1 deletions

132
crates/playout/src/main.rs Normal file
View File

@@ -0,0 +1,132 @@
use std::sync::Arc;
use tokio::net::TcpListener;
use tracing::info;
use playout::config::PlayoutConfig;
use playout::engine::{PlayoutEngine, SourceUriResolver};
use playout::http::{self, AppState};
use playout::segment_store::filesystem::FilesystemSegmentStore;
use domain::value_objects::SourceUri;
struct StubResolver;
impl SourceUriResolver for StubResolver {
fn resolve(&self, _provider_id: &str, _external_id: &str) -> Option<SourceUri> {
None
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let config = PlayoutConfig::from_env();
info!(addr = %config.listen_addr, "starting k-tv-playout");
let store = Arc::new(FilesystemSegmentStore::new(config.storage_path.clone()));
let resolver: Arc<dyn SourceUriResolver> = Arc::new(StubResolver);
// TODO: wire real ScheduleQuery from adapter-sqlite once DB URL is configured
let schedule_query: Arc<dyn domain::ports::ScheduleQuery> =
Arc::new(NoopScheduleQuery);
let engine = Arc::new(PlayoutEngine::new(
config.clone(),
schedule_query,
resolver,
store.clone(),
));
let tick_engine = engine.clone();
let tick_interval = config.tick_interval_ms;
tokio::spawn(async move {
let mut interval =
tokio::time::interval(tokio::time::Duration::from_millis(tick_interval));
loop {
interval.tick().await;
tick_engine.tick().await;
}
});
let state = AppState {
engine: engine.clone(),
store,
config: config.clone(),
};
let app = http::router(state);
let listener = TcpListener::bind(&config.listen_addr).await?;
info!("listening on {}", config.listen_addr);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal(engine))
.await?;
Ok(())
}
async fn shutdown_signal(engine: Arc<PlayoutEngine>) {
tokio::signal::ctrl_c()
.await
.expect("failed to install ctrl+c handler");
info!("shutting down");
engine.shutdown().await;
}
struct NoopScheduleQuery;
#[async_trait::async_trait]
impl domain::ports::ScheduleQuery for NoopScheduleQuery {
async fn find_active(
&self,
_channel_id: domain::value_objects::ChannelId,
_at: chrono::DateTime<chrono::Utc>,
) -> domain::DomainResult<Option<domain::GeneratedSchedule>> {
Ok(None)
}
async fn find_latest(
&self,
_channel_id: domain::value_objects::ChannelId,
) -> domain::DomainResult<Option<domain::GeneratedSchedule>> {
Ok(None)
}
async fn find_playback_history(
&self,
_channel_id: domain::value_objects::ChannelId,
) -> domain::DomainResult<Vec<domain::PlaybackRecord>> {
Ok(vec![])
}
async fn find_last_slot_per_block(
&self,
_channel_id: domain::value_objects::ChannelId,
) -> domain::DomainResult<std::collections::HashMap<domain::BlockId, domain::MediaItemId>> {
Ok(std::collections::HashMap::new())
}
async fn list_schedule_history(
&self,
_channel_id: domain::value_objects::ChannelId,
) -> domain::DomainResult<Vec<domain::GeneratedSchedule>> {
Ok(vec![])
}
async fn get_schedule_by_id(
&self,
_channel_id: domain::value_objects::ChannelId,
_schedule_id: domain::value_objects::ScheduleId,
) -> domain::DomainResult<Option<domain::GeneratedSchedule>> {
Ok(None)
}
}