Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ HTTP API `smelter` crate → `smelter-api` (parse) → `smelter-core` (pipeline:

## API Changes

After modifying types in `smelter-api`, use `/api-change` to run the full generation and validation workflow.
After modifying types in `smelter-api` or types in `smelter-core::stats`, use `/api-change` to run the full generation and validation workflow.
2 changes: 1 addition & 1 deletion .claude/skills/api-change/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Run the API change workflow. All steps must pass before the change is considered
This generates `ts/smelter/src/api.generated.ts`.

4. **Build the TypeScript SDK to verify compatibility**
Run in `./ts`: `pnpm build:all`
Run in `./ts`: `pnpm i && pnpm build:all`

5. **Show a summary of all generated/changed files** so the user can review what was affected.

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions smelter-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ glyphon = { workspace = true }
tokio = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
utoipa = { workspace = true }
axum = { version = "0.7.7", features = ["macros"] }
tower-http = { workspace = true }
Expand Down
29 changes: 15 additions & 14 deletions smelter-core/src/stats/input_reports.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum InputStatsReport {
Rtp(RtpInputStatsReport),
Expand All @@ -12,25 +13,25 @@ pub enum InputStatsReport {
Mp4(Mp4InputStatsReport),
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtpInputStatsReport {
pub video_rtp: RtpJitterBufferStatsReport,
pub audio_rtp: RtpJitterBufferStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct WhipInputStatsReport {
pub video_rtp: RtpJitterBufferStatsReport,
pub audio_rtp: RtpJitterBufferStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct WhepInputStatsReport {
pub video_rtp: RtpJitterBufferStatsReport,
pub audio_rtp: RtpJitterBufferStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtpJitterBufferStatsReport {
pub packets_lost: u64,
pub packets_received: u64,
Expand All @@ -40,7 +41,7 @@ pub struct RtpJitterBufferStatsReport {
pub last_10_seconds: RtpJitterBufferSlidingWindowStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtpJitterBufferSlidingWindowStatsReport {
pub packets_lost: u64,
pub packets_received: u64,
Expand All @@ -57,39 +58,39 @@ pub struct RtpJitterBufferSlidingWindowStatsReport {
pub input_buffer_min_seconds: f64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtmpInputStatsReport {
pub video: RtmpInputTrackStatsReport,
pub audio: RtmpInputTrackStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtmpInputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct Mp4InputStatsReport {
pub video: Mp4InputTrackStatsReport,
pub audio: Mp4InputTrackStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct Mp4InputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct HlsInputStatsReport {
pub video: HlsInputTrackStatsReport,
pub audio: HlsInputTrackStatsReport,
pub corrupted_packets_received: u64,
pub corrupted_packets_received_last_10_seconds: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct HlsInputTrackStatsReport {
pub packets_received: u64,
pub discontinuities_detected: u32,
Expand All @@ -99,7 +100,7 @@ pub struct HlsInputTrackStatsReport {
pub last_10_seconds: HlsInputTrackSlidingWindowStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct HlsInputTrackSlidingWindowStatsReport {
pub packets_received: u64,
pub discontinuities_detected: u32,
Expand Down
5 changes: 3 additions & 2 deletions smelter-core/src/stats/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use std::{
};

use crossbeam_channel::{Receiver, Sender, TrySendError, bounded};
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::warn;
use utoipa::ToSchema;

Expand All @@ -24,7 +25,7 @@ pub(crate) use input::*;
pub(crate) use output::*;
pub(crate) use state::StatsEvent;

#[derive(Debug, Serialize, Clone, ToSchema)]
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, ToSchema)]
pub struct StatsReport {
pub inputs: BTreeMap<String, InputStatsReport>,
pub outputs: BTreeMap<String, OutputStatsReport>,
Expand Down
29 changes: 15 additions & 14 deletions smelter-core/src/stats/output_reports.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum OutputStatsReport {
Whep(WhepOutputStatsReport),
Expand All @@ -12,75 +13,75 @@ pub enum OutputStatsReport {
Rtp(RtpOutputStatsReport),
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct WhepOutputStatsReport {
pub video: WhepOutputTrackStatsReport,
pub audio: WhepOutputTrackStatsReport,
pub connected_peers: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct WhepOutputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct WhipOutputStatsReport {
pub video: WhipOutputTrackStatsReport,
pub audio: WhipOutputTrackStatsReport,
pub is_connected: bool,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct WhipOutputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct HlsOutputStatsReport {
pub video: HlsOutputTrackStatsReport,
pub audio: HlsOutputTrackStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct HlsOutputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct Mp4OutputStatsReport {
pub video: Mp4OutputTrackStatsReport,
pub audio: Mp4OutputTrackStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct Mp4OutputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtmpOutputStatsReport {
pub video: RtmpOutputTrackStatsReport,
pub audio: RtmpOutputTrackStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtmpOutputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtpOutputStatsReport {
pub video: RtpOutputTrackStatsReport,
pub audio: RtpOutputTrackStatsReport,
}

#[derive(Debug, Clone, Copy, Serialize, ToSchema)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RtpOutputTrackStatsReport {
pub bitrate_1_second: u64,
pub bitrate_1_minute: u64,
Expand Down
2 changes: 2 additions & 0 deletions tools/src/bin/generate_from_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ enum ApiTypes {
RegisterWebRenderer(smelter_api::WebRendererSpec),
RegisterShader(smelter_api::ShaderSpec),
UpdateOutput(Box<routes::update_output::UpdateOutputRequest>),

StatsReport(smelter_core::stats::StatsReport),
}

pub fn generate_json_schema(check_flag: bool) {
Expand Down
10 changes: 10 additions & 0 deletions ts/smelter-core/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Api } from '@swmansion/smelter';
import type { StatsReport } from '@swmansion/smelter';
import type { SmelterManager } from './smelterManager';
import type { RegisterOutputRequest } from './api/output';
import { inputRefIntoRawId, type InputRef, type RegisterInputRequest } from './api/input';
import { imageRefIntoRawId, type ImageRef } from './api/image';
import { fromApiStatsReport } from './api/stats';

export { Api };

Expand Down Expand Up @@ -151,4 +153,12 @@ export class ApiClient {
body: {},
});
}

public async stats(): Promise<StatsReport> {
const response = (await this.serverManager.sendRequest({
method: 'GET',
route: `/stats`,
})) as Api.StatsReport;
return fromApiStatsReport(response);
}
}
62 changes: 62 additions & 0 deletions ts/smelter-core/src/api/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Api, StatsReport, InputStatsReport, OutputStatsReport } from '@swmansion/smelter';
import {
fromApiRtpInputStats,
fromApiWhipInputStats,
fromApiWhepInputStats,
fromApiHlsInputStats,
fromApiRtmpInputStats,
fromApiMp4InputStats,
} from './stats/inputs.js';
import {
fromApiWhepOutputStats,
fromApiWhipOutputStats,
fromApiHlsOutputStats,
fromApiMp4OutputStats,
fromApiRtmpOutputStats,
fromApiRtpOutputStats,
} from './stats/outputs.js';

export function fromApiStatsReport(report: Api.StatsReport): StatsReport {
return {
inputs: Object.fromEntries(
Object.entries(report.inputs).map(([id, input]) => [id, fromApiInputStatsReport(input)])
),
outputs: Object.fromEntries(
Object.entries(report.outputs).map(([id, output]) => [id, fromApiOutputStatsReport(output)])
),
};
}

function fromApiInputStatsReport(report: Api.InputStatsReport): InputStatsReport {
if ('rtp' in report) {
return { rtp: fromApiRtpInputStats(report.rtp) };
} else if ('whip' in report) {
return { whip: fromApiWhipInputStats(report.whip) };
} else if ('whep' in report) {
return { whep: fromApiWhepInputStats(report.whep) };
} else if ('hls' in report) {
return { hls: fromApiHlsInputStats(report.hls) };
} else if ('rtmp' in report) {
return { rtmp: fromApiRtmpInputStats(report.rtmp) };
} else if ('mp4' in report) {
return { mp4: fromApiMp4InputStats(report.mp4) };
}
throw new Error(`Unknown input stats report type: ${JSON.stringify(report)}`);
}

function fromApiOutputStatsReport(report: Api.OutputStatsReport): OutputStatsReport {
if ('whep' in report) {
return { whep: fromApiWhepOutputStats(report.whep) };
} else if ('whip' in report) {
return { whip: fromApiWhipOutputStats(report.whip) };
} else if ('hls' in report) {
return { hls: fromApiHlsOutputStats(report.hls) };
} else if ('mp4' in report) {
return { mp4: fromApiMp4OutputStats(report.mp4) };
} else if ('rtmp' in report) {
return { rtmp: fromApiRtmpOutputStats(report.rtmp) };
} else if ('rtp' in report) {
return { rtp: fromApiRtpOutputStats(report.rtp) };
}
throw new Error(`Unknown output stats report type: ${JSON.stringify(report)}`);
}
Loading
Loading