Skip to content
Merged
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 Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
default_to_workspace = false

[env]
RUST_LOG = "warn,remux=info,remux_server=info,remux_server::request=debug,axum=info,tower_http=info,hyper=warn,sqlx=warn"
RUST_LOG = "warn,remux=info,remux_server=info,remux_server::request=info,axum=info,tower_http=info,hyper=warn,sqlx=warn"
RUST_BACKTRACE = 0
CONFIG = "./config"
AXUM_ANYHOW_EXPOSE_ERRORS=1
Expand Down
34 changes: 30 additions & 4 deletions crates/remux-dashboard/src/pages/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use crate::{
};
use dioxus::prelude::*;
use remux_sdks::remux::{
CountryInfo, EncodingOptions, GetCountries, GetEncodingConfiguration,
GetIntroConfiguration, GetSystemConfiguration, HardwareAccelerationType,
IntroOptions, IntroOrder, IntroTriggers, ServerConfiguration, StartTask,
UpdateEncodingConfiguration, UpdateIntroConfiguration, UpdateSystemConfiguration,
CountryInfo, EmbeddedSubtitleHandling, EncodingOptions, GetCountries,
GetEncodingConfiguration, GetIntroConfiguration, GetSystemConfiguration,
HardwareAccelerationType, IntroOptions, IntroOrder, IntroTriggers,
ServerConfiguration, StartTask, UpdateEncodingConfiguration,
UpdateIntroConfiguration, UpdateSystemConfiguration,
};

#[component]
Expand Down Expand Up @@ -315,6 +316,7 @@ pub fn PlaybackSettingsCard(app_state: AppState) -> Element {
let mut h264_crf = use_signal(|| 23_u32);
let mut h265_crf = use_signal(|| 28_u32);
let mut enable_video_transcoding = use_signal(|| true);
let mut subtitle_mode = use_signal(|| "Burn".to_string());
let mut loading = use_signal(|| true);
let mut saving = use_signal(|| false);
let mut error = use_signal(|| Option::<String>::None);
Expand Down Expand Up @@ -394,6 +396,11 @@ pub fn PlaybackSettingsCard(app_state: AppState) -> Element {
opts.enable_video_transcoding
.unwrap_or(true),
);
subtitle_mode.set(
opts.subtitle_mode
.unwrap_or(EmbeddedSubtitleHandling::Burn)
.to_string(),
);
}
Err(e) => error.set(Some(format!("Failed to load settings: {e}"))),
}
Expand Down Expand Up @@ -442,6 +449,10 @@ pub fn PlaybackSettingsCard(app_state: AppState) -> Element {
h264_crf: Some(*h264_crf.peek()),
h265_crf: Some(*h265_crf.peek()),
enable_video_transcoding: Some(*enable_video_transcoding.peek()),
subtitle_mode: subtitle_mode
.peek()
.parse::<EmbeddedSubtitleHandling>()
.ok(),
};
saving.set(true);
error.set(None);
Expand Down Expand Up @@ -479,6 +490,21 @@ pub fn PlaybackSettingsCard(app_state: AppState) -> Element {
}
}

div { class: "field",
label { class: "field-label", "Unsupported Subtitle Handling" }
div { class: "field-hint",
"What to do with embedded subtitle streams the client device doesn't support. Burn encodes them into the video. Extract delivers them separately via the subtitle stream endpoint (may be slow for remote sources). Strip removes them from the media source so the client never sees them — no subtitle-triggered transcoding."
}
select {
class: "select-input",
value: subtitle_mode.read().clone(),
onchange: move |e| subtitle_mode.set(e.value()),
option { value: "Burn", "Burn into video (default)" }
option { value: "Extract", "Extract and deliver separately" }
option { value: "Strip", "Strip (remove, no transcoding)" }
}
}

div { class: "field",
label { class: "field-label", "Hardware Acceleration" }
div { class: "field-hint",
Expand Down
79 changes: 79 additions & 0 deletions crates/remux-sdks/src/remux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,39 @@ pub struct EncodingOptions {
/// unaffected by this setting.
#[default(Some(true))]
pub enable_video_transcoding: Option<bool>,
/// Controls how embedded subtitle streams unsupported by the client are handled.
/// Burn: encode into video (default). Extract: serve via Stream.js/VTT endpoint.
/// Strip: remove from media source so the client never sees them.
#[default(Some(EmbeddedSubtitleHandling::Burn))]
pub subtitle_mode: Option<EmbeddedSubtitleHandling>,
}

// --- Embedded subtitle handling ---

#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
Serialize,
Deserialize,
strum_macros::Display,
strum_macros::EnumString,
)]
#[serde(rename_all = "PascalCase")]
#[strum(serialize_all = "PascalCase")]
pub enum EmbeddedSubtitleHandling {
/// Burn unsupported embedded subtitles into the video during transcoding.
#[default]
Burn,
/// Extract and deliver unsupported embedded subtitles via the subtitle stream
/// endpoint (Stream.js / Stream.vtt). May be slow for remote sources.
Extract,
/// Remove unsupported embedded subtitle streams from the media source entirely.
/// No transcoding is triggered for subtitles; they simply won't be available.
Strip,
}

// --- Preroll configuration ---
Expand Down Expand Up @@ -1457,6 +1490,52 @@ mod tests {
"nextUpDateCutoff must be RFC3339, YYYY-MM-DD, or YYYY-MM-DD HH:MM:SS"
);
}

#[test]
fn embedded_subtitle_handling_default_is_burn() {
assert_eq!(
EmbeddedSubtitleHandling::default(),
EmbeddedSubtitleHandling::Burn
);
}

#[test]
fn embedded_subtitle_handling_display_round_trips() {
for (variant, expected) in [
(EmbeddedSubtitleHandling::Burn, "Burn"),
(EmbeddedSubtitleHandling::Extract, "Extract"),
(EmbeddedSubtitleHandling::Strip, "Strip"),
] {
assert_eq!(variant.to_string(), expected);
let parsed: EmbeddedSubtitleHandling = expected
.parse()
.unwrap();
assert_eq!(parsed, variant);
}
}

#[test]
fn embedded_subtitle_handling_serde_round_trips() {
for variant in [
EmbeddedSubtitleHandling::Burn,
EmbeddedSubtitleHandling::Extract,
EmbeddedSubtitleHandling::Strip,
] {
let json = serde_json::to_string(&variant).unwrap();
let back: EmbeddedSubtitleHandling = serde_json::from_str(&json).unwrap();
assert_eq!(back, variant);
}
}

#[test]
fn encoding_options_subtitle_mode_defaults_to_burn() {
let opts = EncodingOptions::default();
assert_eq!(
opts.subtitle_mode
.unwrap_or_default(),
EmbeddedSubtitleHandling::Burn
);
}
}

#[derive(Default, Debug, Deserialize)]
Expand Down
7 changes: 4 additions & 3 deletions crates/remux-server/src/api/hls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ pub async fn master_hls_video(
s.codec
.clone()
});
let burn_subtitle =
q.subtitle_method == Some(api::SubtitleDeliveryMethod::Encode);
let session = TranscodeSession::new(
play_session_id.clone(),
id,
Expand All @@ -309,7 +311,7 @@ pub async fn master_hls_video(
.map(|v| v as i32),
q.subtitle_stream_index
.map(|v| v as i32),
q.subtitle_method == Some(api::SubtitleDeliveryMethod::Encode),
burn_subtitle,
segment_length,
// Parse reasons from query param (set by playbackinfo on the transcoding URL)
q.transcode_reasons
Expand Down Expand Up @@ -376,8 +378,7 @@ pub async fn master_hls_video(
subtitle_stream_index: q
.subtitle_stream_index
.map(|v| v as i32),
burn_subtitle: q.subtitle_method
== Some(api::SubtitleDeliveryMethod::Encode),
burn_subtitle,
subtitle_width: None,
subtitle_height: None,
encoding_preset: encoding_opts.encoding_preset,
Expand Down
Loading
Loading