aboutsummaryrefslogtreecommitdiff
path: root/src/subtitle_extractor.rs
diff options
context:
space:
mode:
authorMalte Voos <git@mal.tc>2025-11-14 15:30:49 +0100
committerMalte Voos <git@mal.tc>2025-11-14 15:30:49 +0100
commita8457a25ccb9b1ef47f5ce9d7ac1a84c47600c9e (patch)
tree542b42d3316138043272faba42e0d1005f8403b6 /src/subtitle_extractor.rs
parenta42a73378b7c527a5e4600544b2d7a86d68c5aac (diff)
downloadlleap-a8457a25ccb9b1ef47f5ce9d7ac1a84c47600c9e.tar.gz
lleap-a8457a25ccb9b1ef47f5ce9d7ac1a84c47600c9e.zip
implement file/url open dialog
Diffstat (limited to 'src/subtitle_extractor.rs')
-rw-r--r--src/subtitle_extractor.rs209
1 files changed, 0 insertions, 209 deletions
diff --git a/src/subtitle_extractor.rs b/src/subtitle_extractor.rs
deleted file mode 100644
index b628d73..0000000
--- a/src/subtitle_extractor.rs
+++ /dev/null
@@ -1,209 +0,0 @@
-use std::collections::BTreeMap;
-
-use anyhow::Result;
-
-use ffmpeg::Rational;
-use log::{debug, error, info};
-use relm4::{ComponentSender, SharedState, Worker};
-
-pub type StreamIndex = usize;
-
-#[derive(Debug, Clone)]
-pub struct SubtitleCue {
- pub start: gst::ClockTime,
- pub end: gst::ClockTime,
- pub text: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct SubtitleTrack {
- pub language: Option<isolang::Language>,
- pub title: Option<String>,
- pub cues: Vec<SubtitleCue>,
-}
-
-pub static TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new();
-
-pub struct SubtitleExtractor {}
-
-#[derive(Debug)]
-pub enum SubtitleExtractorMsg {
- ExtractFromUrl(String),
-}
-
-#[derive(Debug)]
-pub enum SubtitleExtractorOutput {
- NewOrUpdatedTrackMetadata(StreamIndex),
- NewCue(StreamIndex, SubtitleCue),
- ExtractionComplete,
-}
-
-impl Worker for SubtitleExtractor {
- type Init = ();
- type Input = SubtitleExtractorMsg;
- type Output = SubtitleExtractorOutput;
-
- fn init(_init: Self::Init, _sender: ComponentSender<Self>) -> Self {
- Self {}
- }
-
- fn update(&mut self, msg: SubtitleExtractorMsg, sender: ComponentSender<Self>) {
- match msg {
- SubtitleExtractorMsg::ExtractFromUrl(url) => {
- self.handle_extract_from_url(url, sender);
- }
- }
- }
-}
-
-impl SubtitleExtractor {
- fn handle_extract_from_url(&mut self, url: String, sender: ComponentSender<Self>) {
- // Clear existing tracks
- TRACKS.write().clear();
-
- // Try to extract subtitles using ffmpeg
- match self.extract_subtitles_ffmpeg(&url, &sender) {
- Ok(_) => {
- info!("Subtitle extraction completed successfully");
- sender
- .output(SubtitleExtractorOutput::ExtractionComplete)
- .unwrap();
- }
- Err(e) => {
- error!("FFmpeg extraction failed: {}", e);
- }
- }
- }
-
- fn extract_subtitles_ffmpeg(&self, url: &str, sender: &ComponentSender<Self>) -> Result<()> {
- let mut input = ffmpeg::format::input(&url)?;
-
- let mut subtitle_decoders = BTreeMap::new();
-
- // create decoder for each subtitle stream
- for (stream_index, stream) in input.streams().enumerate() {
- if stream.parameters().medium() == ffmpeg::media::Type::Subtitle {
- let language_code = stream.metadata().get("language").map(|s| s.to_string());
- let title = stream.metadata().get("title").map(|s| s.to_string());
-
- let track = SubtitleTrack {
- language: language_code.and_then(|code| isolang::Language::from_639_2b(&code)),
- title,
- cues: Vec::new(),
- };
-
- TRACKS.write().insert(stream_index, track);
-
- sender
- .output(SubtitleExtractorOutput::NewOrUpdatedTrackMetadata(
- stream_index,
- ))
- .unwrap();
-
- let context =
- ffmpeg::codec::context::Context::from_parameters(stream.parameters())?;
- if let Ok(decoder) = context.decoder().subtitle() {
- subtitle_decoders.insert(stream_index, decoder);
- debug!("Created decoder for subtitle stream {}", stream_index);
- } else {
- error!(
- "Failed to create decoder for subtitle stream {}",
- stream_index
- );
- }
- }
- }
-
- // process packets
- for (stream, packet) in input.packets() {
- let stream_index = stream.index();
-
- if let Some(decoder) = subtitle_decoders.get_mut(&stream_index) {
- let mut subtitle = ffmpeg::Subtitle::new();
- if decoder.decode(&packet, &mut subtitle).is_ok() {
- if let Some(cue) = Self::subtitle_to_cue(&subtitle, &packet, stream.time_base())
- {
- if let Some(track) = TRACKS.write().get_mut(&stream_index) {
- track.cues.push(cue.clone());
- }
-
- sender
- .output(SubtitleExtractorOutput::NewCue(stream_index, cue))
- .unwrap();
- }
- }
- }
- }
-
- Ok(())
- }
-
- fn subtitle_to_cue(
- subtitle: &ffmpeg::Subtitle,
- packet: &ffmpeg::Packet,
- time_base: Rational,
- ) -> Option<SubtitleCue> {
- let time_to_clock_time = |time: i64| {
- let nseconds: i64 = (time * time_base.numerator() as i64 * 1_000_000_000)
- / time_base.denominator() as i64;
- gst::ClockTime::from_nseconds(nseconds as u64)
- };
-
- let text = subtitle
- .rects()
- .into_iter()
- .map(|rect| match rect {
- ffmpeg::subtitle::Rect::Text(text) => text.get().to_string(),
- ffmpeg::subtitle::Rect::Ass(ass) => {
- Self::extract_dialogue_text(ass.get()).unwrap_or(String::new())
- }
- _ => String::new(),
- })
- .collect::<Vec<String>>()
- .join("\n— ");
-
- let start = time_to_clock_time(packet.pts()?);
- let end = time_to_clock_time(packet.pts()? + packet.duration());
-
- Some(SubtitleCue { start, end, text })
- }
-
- fn extract_dialogue_text(dialogue_line: &str) -> Option<String> {
- // ASS dialogue format: ReadOrder,Layer,Style,Name,MarginL,MarginR,MarginV,Effect,Text
- // we need the 9th field (Text), so split on comma but only take first 9 splits
- // see also https://github.com/FFmpeg/FFmpeg/blob/a700f0f72d1f073e5adcfbb16f4633850b0ef51c/libavcodec/ass_split.c#L433
- let text = dialogue_line.splitn(9, ',').last()?;
-
- // remove ASS override codes (formatting tags) like {\b1}, {\i1}, {\c&Hffffff&}, etc.
- let mut result = String::new();
- let mut in_tag = false;
- let mut char_iter = text.chars().peekable();
-
- while let Some(c) = char_iter.next() {
- if c == '{' && char_iter.peek() == Some(&'\\') {
- in_tag = true;
- } else if c == '}' {
- in_tag = false;
- } else if !in_tag {
- // process line breaks and hard spaces
- if c == '\\' {
- match char_iter.peek() {
- Some(&'N') => {
- char_iter.next();
- result.push('\n');
- }
- Some(&'n') | Some(&'h') => {
- char_iter.next();
- result.push(' ');
- }
- _ => result.push(c),
- }
- } else {
- result.push(c);
- }
- }
- }
-
- Some(result)
- }
-}