1use std::io::Write;
4use std::path::Path;
5use std::sync::OnceLock;
6use std::time::Instant;
7
8use ffmpeg::format::{self, Pixel};
9use ffmpeg::media::Type;
10use ffmpeg::software::scaling::{context::Context as ScalerContext, flag::Flags as ScaleFlags};
11use ffmpeg::util::frame::video::Video;
12use ffmpeg::util::mathematics::{rescale, Rescale};
13use ffmpeg_next as ffmpeg;
14use tempfile::NamedTempFile;
15use url::Url;
16
17use crate::subtitle::SubtitleCue;
18
19static FFMPEG_INIT: OnceLock<Result<(), ffmpeg::Error>> = OnceLock::new();
20
21fn ensure_ffmpeg() -> Result<(), ffmpeg::Error> {
22 *FFMPEG_INIT.get_or_init(|| {
23 let r = ffmpeg::init();
24 if r.is_ok() {
25 ffmpeg::util::log::set_level(ffmpeg::util::log::Level::Quiet);
27 }
28 r
29 })
30}
31
32fn frame_pts_ms_from_tb(time_base: ffmpeg::Rational, frame: &Video) -> Option<u64> {
33 let pts = frame.timestamp().or_else(|| frame.pts())?;
34 if pts < 0 {
35 return None;
36 }
37 let ms = pts.rescale(time_base, (1, 1000));
38 if ms < 0 {
39 None
40 } else {
41 Some(ms as u64)
42 }
43}
44
45fn scale_frame_to_rgba(
46 scaler: &mut ScalerContext,
47 decoded: &Video,
48) -> Result<(Vec<u8>, u32, u32), String> {
49 let mut rgb = Video::empty();
50 scaler.run(decoded, &mut rgb).map_err(|e| e.to_string())?;
51 let w = rgb.width();
52 let h = rgb.height();
53 let stride = rgb.stride(0);
54 let mut packed = Vec::with_capacity((w * h * 4) as usize);
55 for row in 0..h as usize {
56 let start = row * stride;
57 let end = start + (w as usize * 4);
58 packed.extend_from_slice(&rgb.data(0)[start..end]);
59 }
60 Ok((packed, w, h))
61}
62
63pub struct VideoPlayer {
65 input: format::context::Input,
66 video_stream_index: usize,
67 decoder: ffmpeg::decoder::Video,
68 scaler: ScalerContext,
69 time_base: ffmpeg::Rational,
70 pub duration_ms: u64,
71 pub width: u32,
72 pub height: u32,
73 last_pts_ms: Option<u64>,
75 last_rgba: Option<(Vec<u8>, u32, u32, u64)>,
77 decoder_eof_sent: bool,
79}
80
81impl VideoPlayer {
82 fn open_input(input: format::context::Input) -> Result<Self, String> {
83 ensure_ffmpeg().map_err(|e| e.to_string())?;
84
85 let stream = input
86 .streams()
87 .best(Type::Video)
88 .ok_or_else(|| "no video stream".to_string())?;
89 let video_stream_index = stream.index();
90 let time_base = stream.time_base();
91
92 let context = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
93 .map_err(|e| e.to_string())?;
94 let decoder = context.decoder().video().map_err(|e| e.to_string())?;
95
96 let duration_ms = Self::probe_duration_ms(&input, &stream);
97
98 let width = decoder.width();
99 let height = decoder.height();
100
101 let scaler = ScalerContext::get(
102 decoder.format(),
103 width,
104 height,
105 Pixel::RGBA,
106 width,
107 height,
108 ScaleFlags::BILINEAR,
109 )
110 .map_err(|e| e.to_string())?;
111
112 Ok(Self {
113 input,
114 video_stream_index,
115 decoder,
116 scaler,
117 time_base,
118 duration_ms,
119 width,
120 height,
121 last_pts_ms: None,
122 last_rgba: None,
123 decoder_eof_sent: false,
124 })
125 }
126
127 pub fn open_path(path: &Path) -> Result<Self, String> {
128 let input = format::input(path).map_err(|e| e.to_string())?;
129 Self::open_input(input)
130 }
131
132 pub fn open_url(url: &str) -> Result<Self, String> {
133 let input = format::input(url).map_err(|e| e.to_string())?;
134 Self::open_input(input)
135 }
136
137 fn probe_duration_ms(
138 input: &format::context::Input,
139 stream: &ffmpeg::format::stream::Stream,
140 ) -> u64 {
141 let d = input.duration();
142 if d > 0 {
143 let ms = d.rescale(rescale::TIME_BASE, (1, 1000));
144 if ms > 0 {
145 return ms as u64;
146 }
147 }
148 let sd = stream.duration();
149 if sd > 0 {
150 let ms = sd.rescale(stream.time_base(), (1, 1000));
151 return ms.max(0) as u64;
152 }
153 0
154 }
155
156 fn seek_to_ms(&mut self, target_ms: u64) -> Result<(), String> {
157 let ts = (target_ms as i64).rescale((1, 1000), rescale::TIME_BASE);
158 self.input.seek(ts, ts..ts).map_err(|e| e.to_string())?;
159 self.decoder.flush();
160 self.last_pts_ms = None;
161 self.last_rgba = None;
162 self.decoder_eof_sent = false;
163 Ok(())
164 }
165
166 pub fn decode_frame_at(&mut self, target_ms: u64) -> Result<(Vec<u8>, u32, u32), String> {
168 let target_ms = target_ms.min(self.duration_ms.saturating_add(500));
169
170 let need_seek = match self.last_pts_ms {
171 None => true,
172 Some(last) => target_ms < last || target_ms.saturating_sub(last) > 2_500,
173 };
174
175 if need_seek {
176 self.seek_to_ms(target_ms)?;
177 }
178
179 let mut best_before: Option<(Vec<u8>, u32, u32, u64)> = None;
180 let mut best_after: Option<(Vec<u8>, u32, u32, u64)> = None;
181
182 {
183 let VideoPlayer {
184 ref mut input,
185 video_stream_index,
186 ref mut decoder,
187 ref mut scaler,
188 time_base,
189 ..
190 } = self;
191
192 'outer: for (stream, packet) in input.packets() {
193 if stream.index() != *video_stream_index {
194 continue;
195 }
196 decoder.send_packet(&packet).map_err(|e| e.to_string())?;
197 let mut decoded = Video::empty();
198 while decoder.receive_frame(&mut decoded).is_ok() {
199 let pts_ms = frame_pts_ms_from_tb(*time_base, &decoded).unwrap_or(0);
200 let (buf, w, h) = scale_frame_to_rgba(scaler, &decoded)?;
201 if pts_ms >= target_ms {
202 best_after = Some((buf, w, h, pts_ms));
203 break 'outer;
204 }
205 best_before = Some((buf, w, h, pts_ms));
206 }
207 }
208 }
209
210 if best_after.is_none() {
212 let VideoPlayer {
213 ref mut decoder,
214 ref mut scaler,
215 time_base,
216 ..
217 } = self;
218 let mut decoded = Video::empty();
219 while decoder.receive_frame(&mut decoded).is_ok() {
220 let pts_ms = frame_pts_ms_from_tb(*time_base, &decoded).unwrap_or(0);
221 let (buf, w, h) = scale_frame_to_rgba(scaler, &decoded)?;
222 if pts_ms >= target_ms {
223 best_after = Some((buf, w, h, pts_ms));
224 break;
225 }
226 best_before = Some((buf, w, h, pts_ms));
227 }
228 }
229
230 if let Some((buf, w, h, pts)) = best_after {
231 self.last_pts_ms = Some(pts);
232 self.last_rgba = Some((buf.clone(), w, h, pts));
233 return Ok((buf, w, h));
234 }
235
236 if let Some((buf, w, h, pts)) = best_before {
237 self.last_pts_ms = Some(pts);
238 self.last_rgba = Some((buf.clone(), w, h, pts));
239 return Ok((buf, w, h));
240 }
241
242 if !self.decoder_eof_sent {
244 let _ = self.decoder.send_eof();
245 self.decoder_eof_sent = true;
246 let VideoPlayer {
247 ref mut decoder,
248 ref mut scaler,
249 time_base,
250 ..
251 } = self;
252 let mut decoded = Video::empty();
253 while decoder.receive_frame(&mut decoded).is_ok() {
254 let pts_ms = frame_pts_ms_from_tb(*time_base, &decoded).unwrap_or(0);
255 let (buf, w, h) = scale_frame_to_rgba(scaler, &decoded)?;
256 if pts_ms >= target_ms {
257 best_after = Some((buf, w, h, pts_ms));
258 break;
259 }
260 best_before = Some((buf, w, h, pts_ms));
261 }
262 }
263
264 if let Some((buf, w, h, pts)) = best_after {
265 self.last_pts_ms = Some(pts);
266 self.last_rgba = Some((buf.clone(), w, h, pts));
267 return Ok((buf, w, h));
268 }
269
270 if let Some((buf, w, h, pts)) = best_before {
271 self.last_pts_ms = Some(pts);
272 self.last_rgba = Some((buf.clone(), w, h, pts));
273 return Ok((buf, w, h));
274 }
275
276 if let Some((buf, w, h, _pts)) = self.last_rgba.clone() {
277 return Ok((buf, w, h));
278 }
279
280 Err("no video frame decoded".into())
281 }
282}
283
284unsafe impl Send for VideoPlayer {}
286
287pub struct VideoPlaybackState {
289 pub player: Option<VideoPlayer>,
290 pub playing: bool,
291 play_start: Option<Instant>,
292 pub base_position_ms: u64,
293 pub volume: f32,
294 pub looping: bool,
295 pub pip: bool,
296 pub subtitles: Vec<SubtitleCue>,
297 pub last_url_content_type: String,
298 pub hls_variants: Vec<String>,
299 pub hls_base_url: String,
300 temp_file: Option<NamedTempFile>,
301}
302
303impl Default for VideoPlaybackState {
304 fn default() -> Self {
305 Self {
306 player: None,
307 playing: false,
308 play_start: None,
309 base_position_ms: 0,
310 volume: 1.0,
311 looping: false,
312 pip: false,
313 subtitles: Vec::new(),
314 last_url_content_type: String::new(),
315 hls_variants: Vec::new(),
316 hls_base_url: String::new(),
317 temp_file: None,
318 }
319 }
320}
321
322impl VideoPlaybackState {
323 pub fn duration_ms(&self) -> u64 {
324 self.player.as_ref().map(|p| p.duration_ms).unwrap_or(0)
325 }
326
327 pub fn current_position_ms(&self) -> u64 {
328 let dur = self.duration_ms();
329 let pos = if self.playing {
330 let start = self.play_start.expect("play_start when playing");
331 self.base_position_ms + start.elapsed().as_millis() as u64
332 } else {
333 self.base_position_ms
334 };
335 if self.looping && dur > 0 {
336 pos % dur
337 } else if dur > 0 {
338 pos.min(dur)
339 } else {
340 pos
341 }
342 }
343
344 pub fn play(&mut self) {
345 self.play_start = Some(Instant::now());
346 self.playing = true;
347 }
348
349 pub fn pause(&mut self) {
350 if self.playing {
351 self.base_position_ms = self.current_position_ms();
352 self.playing = false;
353 self.play_start = None;
354 }
355 }
356
357 pub fn stop(&mut self) {
358 self.playing = false;
359 self.play_start = None;
360 self.base_position_ms = 0;
361 self.player = None;
362 self.temp_file = None;
363 self.hls_variants.clear();
364 self.hls_base_url.clear();
365 }
366
367 pub fn reset_playback_clock(&mut self) {
369 self.base_position_ms = 0;
370 self.playing = false;
371 self.play_start = None;
372 }
373
374 pub fn seek(&mut self, position_ms: u64) {
375 let dur = self.duration_ms();
376 let pos = if dur > 0 {
377 position_ms.min(dur)
378 } else {
379 position_ms
380 };
381 self.base_position_ms = pos;
382 if self.playing {
383 self.play_start = Some(Instant::now());
384 }
385 }
386
387 pub fn open_path(&mut self, path: &Path) -> Result<(), String> {
388 self.stop();
389 let p = VideoPlayer::open_path(path)?;
390 self.player = Some(p);
391 Ok(())
392 }
393
394 pub fn open_bytes(&mut self, data: &[u8], format_hint: u32) -> Result<(), String> {
395 self.stop();
396 let ext = crate::video_format::suffix_for_format(format_hint);
397 let mut tmp = tempfile::Builder::new()
398 .suffix(ext)
399 .tempfile()
400 .map_err(|e| e.to_string())?;
401 tmp.write_all(data).map_err(|e| e.to_string())?;
402 tmp.flush().map_err(|e| e.to_string())?;
403 let path = tmp.path().to_owned();
404 self.temp_file = Some(tmp);
405 self.open_path(&path)
406 }
407}
408
409pub fn parse_hls_master_variants(body: &str) -> Vec<String> {
411 let lines: Vec<&str> = body.lines().collect();
412 let mut out = Vec::new();
413 for i in 0..lines.len() {
414 if lines[i].starts_with("#EXT-X-STREAM-INF") && i + 1 < lines.len() {
415 let u = lines[i + 1].trim();
416 if !u.starts_with('#') && !u.is_empty() {
417 out.push(u.to_string());
418 }
419 }
420 }
421 out
422}
423
424pub fn resolve_against_base(base: &str, relative: &str) -> Option<String> {
425 let b = Url::parse(base).ok()?;
426 b.join(relative).ok().map(|u| u.to_string())
427}