Compare commits
16 Commits
f3443867c5
...
15eb9a2700
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15eb9a2700 | ||
|
|
72187a45a4 | ||
|
|
f6fc120bbd | ||
|
|
f6b1802021 | ||
|
|
e0f61f0983 | ||
|
|
7eee097e55 | ||
|
|
3f08b0d5d9 | ||
|
|
342abd5aea | ||
|
|
a7d41b3c17 | ||
|
|
722af4fe87 | ||
|
|
dbcd7ba5e9 | ||
|
|
643accfc92 | ||
|
|
263daa0763 | ||
|
|
6ea57d3a3e | ||
|
|
5587e6a67a | ||
|
|
0b75987fd0 |
4358
Cargo.lock
generated
Normal file
4358
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ tao = "0.30"
|
|||||||
wry = "0.54"
|
wry = "0.54"
|
||||||
|
|
||||||
# Video/Audio
|
# Video/Audio
|
||||||
sdl2 = "0.38"
|
sdl2 = { version = "0.38", features = ["ttf"] }
|
||||||
ffmpeg-sidecar = "2.4"
|
ffmpeg-sidecar = "2.4"
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
|
|||||||
@@ -19,17 +19,58 @@ pub struct Config {
|
|||||||
#[arg(short = 'y', long = "yolo", help = "YOLO JSON file path")]
|
#[arg(short = 'y', long = "yolo", help = "YOLO JSON file path")]
|
||||||
pub yolo: Option<PathBuf>,
|
pub yolo: Option<PathBuf>,
|
||||||
|
|
||||||
#[arg(short = 'w', long = "width", default_value = "1280", help = "Window width")]
|
#[arg(short = 'c', long = "chunks", help = "Chunk/Cut JSON file path")]
|
||||||
|
pub chunks: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short = 'w',
|
||||||
|
long = "width",
|
||||||
|
default_value = "1280",
|
||||||
|
help = "Window width"
|
||||||
|
)]
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
|
|
||||||
#[arg(short = 'h', long = "height", default_value = "720", help = "Window height")]
|
#[arg(
|
||||||
|
short = 'h',
|
||||||
|
long = "height",
|
||||||
|
default_value = "1053",
|
||||||
|
help = "Window height"
|
||||||
|
)]
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
|
|
||||||
#[arg(long = "fullscreen", help = "Start in fullscreen mode")]
|
#[arg(long = "fullscreen", help = "Start in fullscreen mode")]
|
||||||
pub fullscreen: bool,
|
pub fullscreen: bool,
|
||||||
|
|
||||||
#[arg(long = "locale", default_value = "en", help = "UI language (en, zh-TW, etc.)")]
|
#[arg(
|
||||||
|
long = "locale",
|
||||||
|
default_value = "en",
|
||||||
|
help = "UI language (en, zh-TW, etc.)"
|
||||||
|
)]
|
||||||
pub locale: String,
|
pub locale: String,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short = 's',
|
||||||
|
long = "start",
|
||||||
|
default_value = "0",
|
||||||
|
help = "Start time in seconds"
|
||||||
|
)]
|
||||||
|
pub start_seconds: f64,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short = 'e',
|
||||||
|
long = "end",
|
||||||
|
default_value = "0",
|
||||||
|
help = "End time in seconds (0 = no limit)"
|
||||||
|
)]
|
||||||
|
pub end_seconds: f64,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
short = 'l',
|
||||||
|
long = "loop",
|
||||||
|
default_value = "false",
|
||||||
|
help = "Loop playback between start and end"
|
||||||
|
)]
|
||||||
|
pub loop_playback: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! Unified media player with ASR/YOLO/Chunks overlay support
|
//! Unified media player with ASR/YOLO/Chunks overlay support
|
||||||
|
|
||||||
pub mod player;
|
|
||||||
pub mod overlay;
|
|
||||||
pub mod web;
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod overlay;
|
||||||
|
pub mod player;
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
pub use config::Config;
|
pub use config::Config;
|
||||||
|
|||||||
1200
src/main.rs
1200
src/main.rs
File diff suppressed because it is too large
Load Diff
@@ -28,17 +28,18 @@ impl AsrLoader {
|
|||||||
let content = std::fs::read_to_string(path)
|
let content = std::fs::read_to_string(path)
|
||||||
.with_context(|| format!("Failed to read ASR file: {:?}", path))?;
|
.with_context(|| format!("Failed to read ASR file: {:?}", path))?;
|
||||||
|
|
||||||
let data: AsrData = serde_json::from_str(&content)
|
let data: AsrData =
|
||||||
.with_context(|| "Failed to parse ASR JSON")?;
|
serde_json::from_str(&content).with_context(|| "Failed to parse ASR JSON")?;
|
||||||
|
|
||||||
Ok(Self { data })
|
Ok(Self { data })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_segment_at(&self, time_ms: f64) -> Option<&AsrSegment> {
|
pub fn get_segment_at(&self, time_ms: f64) -> Option<&AsrSegment> {
|
||||||
let time_sec = time_ms / 1000.0;
|
let time_sec = time_ms / 1000.0;
|
||||||
self.data.segments.iter().find(|seg| {
|
self.data
|
||||||
time_sec >= seg.start && time_sec < seg.end
|
.segments
|
||||||
})
|
.iter()
|
||||||
|
.find(|seg| time_sec >= seg.start && time_sec < seg.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text_at(&self, time_ms: f64) -> Option<String> {
|
pub fn get_text_at(&self, time_ms: f64) -> Option<String> {
|
||||||
|
|||||||
52
src/overlay/chunk.rs
Normal file
52
src/overlay/chunk.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use log::info;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct CutData {
|
||||||
|
pub frame_count: u64,
|
||||||
|
pub fps: f64,
|
||||||
|
pub scenes: Vec<Scene>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Scene {
|
||||||
|
pub scene_number: u32,
|
||||||
|
pub start_frame: u64,
|
||||||
|
pub end_frame: u64,
|
||||||
|
pub start_time: f64,
|
||||||
|
pub end_time: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChunkLoader {
|
||||||
|
pub data: CutData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkLoader {
|
||||||
|
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("Failed to read chunk file: {:?}", path))?;
|
||||||
|
let data: CutData = serde_json::from_str(&content)
|
||||||
|
.with_context(|| format!("Failed to parse chunk JSON: {:?}", path))?;
|
||||||
|
info!("Loaded {} scenes from {:?}", data.scenes.len(), path);
|
||||||
|
Ok(Self { data })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_count(&self) -> usize {
|
||||||
|
self.data.scenes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scene_boundaries(&self) -> Vec<u64> {
|
||||||
|
self.data.scenes.iter().map(|s| s.start_frame).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_scene(&self, frame: u64) -> Option<&Scene> {
|
||||||
|
self.data
|
||||||
|
.scenes
|
||||||
|
.iter()
|
||||||
|
.find(|s| frame >= s.start_frame && frame <= s.end_frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
//! Overlay module
|
//! Overlay module
|
||||||
//!
|
//!
|
||||||
//! ASR subtitle and YOLO bbox overlay management
|
//! ASR subtitle, YOLO bbox, and chunk marker overlay management
|
||||||
|
|
||||||
pub mod asr;
|
pub mod asr;
|
||||||
|
pub mod chunk;
|
||||||
pub mod yolo;
|
pub mod yolo;
|
||||||
|
|
||||||
pub use asr::AsrLoader;
|
pub use asr::AsrLoader;
|
||||||
|
pub use chunk::ChunkLoader;
|
||||||
pub use yolo::YoloLoader;
|
pub use yolo::YoloLoader;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use lru::LruCache;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::BufReader;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -40,65 +41,62 @@ pub struct YoloMetadata {
|
|||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
pub total_detections: u64,
|
pub total_detections: u64,
|
||||||
pub avg_detections_per_frame: f64,
|
pub avg_detections_per_frame: f64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_save_interval: Option<u32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub processing_time: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub avg_time_per_frame: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub last_saved_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub completed_at: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_save_count: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct YoloData {
|
pub struct YoloData {
|
||||||
pub metadata: YoloMetadata,
|
pub metadata: YoloMetadata,
|
||||||
#[serde(flatten)]
|
|
||||||
pub frames: HashMap<String, FrameData>,
|
pub frames: HashMap<String, FrameData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct YoloLoader {
|
pub struct YoloLoader {
|
||||||
data: YoloData,
|
data: YoloData,
|
||||||
cache: LruCache<u64, Vec<Detection>>,
|
cache: LruCache<u64, Vec<Detection>>,
|
||||||
frame_index: HashMap<u64, usize>,
|
|
||||||
file_path: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl YoloLoader {
|
impl YoloLoader {
|
||||||
const CACHE_SIZE: usize = 60;
|
const CACHE_SIZE: usize = 60;
|
||||||
|
|
||||||
pub fn load(path: &Path) -> Result<Self> {
|
pub fn load(path: &Path) -> Result<Self> {
|
||||||
let file_path = path.to_string_lossy().to_string();
|
let file =
|
||||||
|
File::open(path).with_context(|| format!("Failed to open YOLO file: {:?}", path))?;
|
||||||
let file = File::open(path)
|
|
||||||
.with_context(|| format!("Failed to open YOLO file: {:?}", path))?;
|
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
let data: YoloData = serde_json::from_reader(reader)
|
let data: YoloData =
|
||||||
.with_context(|| "Failed to parse YOLO JSON")?;
|
serde_json::from_reader(reader).with_context(|| "Failed to parse YOLO JSON")?;
|
||||||
|
|
||||||
let mut frame_index = HashMap::new();
|
let cache = LruCache::new(NonZeroUsize::new(Self::CACHE_SIZE).unwrap());
|
||||||
for (i, (key, frame)) in data.frames.iter().enumerate() {
|
|
||||||
if let Ok(frame_num) = key.parse::<u64>() {
|
|
||||||
frame_index.insert(frame_num, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { data, cache })
|
||||||
data,
|
|
||||||
cache: LruCache::new(Self::CACHE_SIZE),
|
|
||||||
frame_index,
|
|
||||||
file_path,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_detections(&mut self, frame: u64) -> Vec<&Detection> {
|
pub fn get_detections(&mut self, frame: u64) -> Vec<Detection> {
|
||||||
if let Some(dets) = self.cache.get(&frame) {
|
if let Some(dets) = self.cache.get(&frame) {
|
||||||
return dets.iter().collect();
|
return dets.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(frame_data) = self.data.frames.get(&frame.to_string()) {
|
if let Some(frame_data) = self.data.frames.get(&frame.to_string()) {
|
||||||
let dets: Vec<Detection> = frame_data.detections.clone();
|
let dets = frame_data.detections.clone();
|
||||||
self.cache.put(frame, dets.clone());
|
self.cache.put(frame, dets.clone());
|
||||||
dets.iter().collect()
|
dets
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_detections_at_time(&mut self, time_ms: u64) -> Vec<&Detection> {
|
pub fn get_detections_at_time(&mut self, time_ms: u64) -> Vec<Detection> {
|
||||||
let fps = self.data.metadata.fps;
|
let fps = self.data.metadata.fps;
|
||||||
let frame = ((time_ms as f64 / 1000.0) * fps) as u64;
|
let frame = ((time_ms as f64 / 1000.0) * fps) as u64;
|
||||||
self.get_detections(frame)
|
self.get_detections(frame)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
//! 音頻播放模組
|
//! Audio player module
|
||||||
|
|
||||||
use anyhow::Result;
|
use log::info;
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::io::Write;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct AudioPlayer {
|
pub struct AudioPlayer {
|
||||||
process: Option<std::process::Child>,
|
process: Option<Child>,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
muted: bool,
|
muted: bool,
|
||||||
}
|
}
|
||||||
@@ -21,8 +19,8 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&mut self, path: &str, start_ms: u64) -> Result<()> {
|
pub fn play(&mut self, path: &std::path::Path, start_ms: u64) -> Option<()> {
|
||||||
self.stop()?;
|
self.stop();
|
||||||
|
|
||||||
let start_sec = start_ms as f64 / 1000.0;
|
let start_sec = start_ms as f64 / 1000.0;
|
||||||
let volume_filter = if self.muted {
|
let volume_filter = if self.muted {
|
||||||
@@ -31,36 +29,51 @@ impl AudioPlayer {
|
|||||||
format!("volume={}", self.volume)
|
format!("volume={}", self.volume)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cmd = Command::new("ffplay");
|
let cmd = Command::new("ffplay")
|
||||||
cmd.args([
|
.args([
|
||||||
"-nodisp",
|
"-nodisp",
|
||||||
"-autoexit",
|
"-autoexit",
|
||||||
"-ss", &format!("{}", start_sec),
|
"-ss",
|
||||||
"-af", &volume_filter,
|
&format!("{:.3}", start_sec),
|
||||||
"-i", path,
|
"-af",
|
||||||
])
|
&volume_filter,
|
||||||
.stdin(Stdio::null())
|
"-i",
|
||||||
.stdout(Stdio::null())
|
])
|
||||||
.stderr(Stdio::null())
|
.arg(path)
|
||||||
.spawn()?;
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
self.process = Some(cmd);
|
self.process = Some(cmd);
|
||||||
Ok(())
|
info!("Audio playback started from {:.1}s", start_sec);
|
||||||
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self) -> Result<()> {
|
pub fn pause(&mut self) {
|
||||||
if let Some(mut child) = self.process.take() {
|
if let Some(ref mut child) = self.process {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
}
|
}
|
||||||
Ok(())
|
self.process = None;
|
||||||
|
info!("Audio paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume(&mut self, path: &std::path::Path, current_time_ms: u64) -> Option<()> {
|
||||||
|
self.play(path, current_time_ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
if let Some(ref mut child) = self.process.take() {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
self.process = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_volume(&mut self, volume: f32) {
|
pub fn set_volume(&mut self, volume: f32) {
|
||||||
self.volume = volume.clamp(0.0, 1.0);
|
self.volume = volume.clamp(0.0, 1.0);
|
||||||
if !self.muted {
|
|
||||||
self.restart_with_volume()?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_mute(&mut self) -> bool {
|
pub fn toggle_mute(&mut self) -> bool {
|
||||||
@@ -71,14 +84,16 @@ impl AudioPlayer {
|
|||||||
pub fn is_muted(&self) -> bool {
|
pub fn is_muted(&self) -> bool {
|
||||||
self.muted
|
self.muted
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn restart_with_volume(&mut self) -> Result<()> {
|
impl Default for AudioPlayer {
|
||||||
Ok(())
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for AudioPlayer {
|
impl Drop for AudioPlayer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = self.stop();
|
self.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
//! FFmpeg 封裝
|
//! FFmpeg wrapper
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::{BufReader, Read};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{Command, Stdio, Child, ChildStdout};
|
use std::process::{Child, ChildStdout, Command, Stdio};
|
||||||
use std::io::{Read, BufReader};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VideoInfo {
|
pub struct VideoInfo {
|
||||||
@@ -21,6 +21,14 @@ pub struct FFmpegDecoder {
|
|||||||
process: Option<Child>,
|
process: Option<Child>,
|
||||||
stdout: Option<BufReader<ChildStdout>>,
|
stdout: Option<BufReader<ChildStdout>>,
|
||||||
info: VideoInfo,
|
info: VideoInfo,
|
||||||
|
frame_buffer: VecDeque<Vec<u8>>,
|
||||||
|
frame_times: VecDeque<u64>,
|
||||||
|
buffer_loaded: bool,
|
||||||
|
current_read_ms: u64,
|
||||||
|
display_time_ms: u64,
|
||||||
|
reverse_start_ms: u64,
|
||||||
|
reverse_end_ms: u64,
|
||||||
|
in_reverse_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FFmpegDecoder {
|
impl FFmpegDecoder {
|
||||||
@@ -33,14 +41,24 @@ impl FFmpegDecoder {
|
|||||||
process: None,
|
process: None,
|
||||||
stdout: None,
|
stdout: None,
|
||||||
info,
|
info,
|
||||||
|
frame_buffer: VecDeque::new(),
|
||||||
|
frame_times: VecDeque::new(),
|
||||||
|
buffer_loaded: false,
|
||||||
|
current_read_ms: 0,
|
||||||
|
display_time_ms: 0,
|
||||||
|
reverse_start_ms: 0,
|
||||||
|
reverse_end_ms: 0,
|
||||||
|
in_reverse_mode: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn probe(path: &Path) -> Result<VideoInfo> {
|
fn probe(path: &Path) -> Result<VideoInfo> {
|
||||||
let output = Command::new("ffprobe")
|
let output = Command::new("ffprobe")
|
||||||
.args([
|
.args([
|
||||||
"-v", "quiet",
|
"-v",
|
||||||
"-print_format", "json",
|
"quiet",
|
||||||
|
"-print_format",
|
||||||
|
"json",
|
||||||
"-show_format",
|
"-show_format",
|
||||||
"-show_streams",
|
"-show_streams",
|
||||||
path.to_str().unwrap_or(""),
|
path.to_str().unwrap_or(""),
|
||||||
@@ -48,14 +66,12 @@ impl FFmpegDecoder {
|
|||||||
.output()
|
.output()
|
||||||
.context("Failed to run ffprobe")?;
|
.context("Failed to run ffprobe")?;
|
||||||
|
|
||||||
let json: serde_json::Value = serde_json::from_slice(&output.stdout)
|
let json: serde_json::Value =
|
||||||
.context("Failed to parse ffprobe output")?;
|
serde_json::from_slice(&output.stdout).context("Failed to parse ffprobe output")?;
|
||||||
|
|
||||||
let video_stream = json["streams"]
|
let video_stream = json["streams"]
|
||||||
.as_array()
|
.as_array()
|
||||||
.and_then(|streams| {
|
.and_then(|streams| streams.iter().find(|s| s["codec_type"] == "video"))
|
||||||
streams.iter().find(|s| s["codec_type"] == "video")
|
|
||||||
})
|
|
||||||
.context("No video stream found")?;
|
.context("No video stream found")?;
|
||||||
|
|
||||||
let width = video_stream["width"].as_u64().unwrap_or(0) as u32;
|
let width = video_stream["width"].as_u64().unwrap_or(0) as u32;
|
||||||
@@ -65,7 +81,10 @@ impl FFmpegDecoder {
|
|||||||
let (num, den) = {
|
let (num, den) = {
|
||||||
let parts: Vec<&str> = fps_str.split('/').collect();
|
let parts: Vec<&str> = fps_str.split('/').collect();
|
||||||
if parts.len() == 2 {
|
if parts.len() == 2 {
|
||||||
(parts[0].parse::<f64>().unwrap_or(30.0), parts[1].parse::<f64>().unwrap_or(1.0))
|
(
|
||||||
|
parts[0].parse::<f64>().unwrap_or(30.0),
|
||||||
|
parts[1].parse::<f64>().unwrap_or(1.0),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(fps_str.parse::<f64>().unwrap_or(30.0), 1.0)
|
(fps_str.parse::<f64>().unwrap_or(30.0), 1.0)
|
||||||
}
|
}
|
||||||
@@ -77,7 +96,10 @@ impl FFmpegDecoder {
|
|||||||
let duration_ms = (duration_sec * 1000.0) as u64;
|
let duration_ms = (duration_sec * 1000.0) as u64;
|
||||||
|
|
||||||
let frame_count = (duration_sec * fps) as u64;
|
let frame_count = (duration_sec * fps) as u64;
|
||||||
let codec = video_stream["codec_name"].as_str().unwrap_or("unknown").to_string();
|
let codec = video_stream["codec_name"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
Ok(VideoInfo {
|
Ok(VideoInfo {
|
||||||
width,
|
width,
|
||||||
@@ -94,16 +116,20 @@ impl FFmpegDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_decoding(&mut self, start_ms: u64) -> Result<()> {
|
pub fn start_decoding(&mut self, start_ms: u64) -> Result<()> {
|
||||||
self.stop()?;
|
self.stop();
|
||||||
|
|
||||||
let start_sec = start_ms as f64 / 1000.0;
|
let start_sec = start_ms as f64 / 1000.0;
|
||||||
|
|
||||||
let mut child = Command::new("ffmpeg")
|
let mut child = Command::new("ffmpeg")
|
||||||
.args([
|
.args([
|
||||||
"-ss", &format!("{}", start_sec),
|
"-ss",
|
||||||
"-i", &self.path,
|
&format!("{}", start_sec),
|
||||||
"-f", "rawvideo",
|
"-i",
|
||||||
"-pix_fmt", "rgb24",
|
&self.path,
|
||||||
|
"-f",
|
||||||
|
"rawvideo",
|
||||||
|
"-pix_fmt",
|
||||||
|
"rgb24",
|
||||||
"-",
|
"-",
|
||||||
])
|
])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
@@ -111,17 +137,43 @@ impl FFmpegDecoder {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.context("Failed to start ffmpeg")?;
|
.context("Failed to start ffmpeg")?;
|
||||||
|
|
||||||
let stdout = child.stdout.take()
|
let stdout = child.stdout.take().context("Failed to capture stdout")?;
|
||||||
.context("Failed to capture stdout")?;
|
|
||||||
|
|
||||||
self.process = Some(child);
|
self.process = Some(child);
|
||||||
self.stdout = Some(BufReader::new(stdout));
|
self.stdout = Some(BufReader::new(stdout));
|
||||||
|
self.current_read_ms = start_ms;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> {
|
pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> {
|
||||||
self.start_decoding(timestamp_ms)
|
self.in_reverse_mode = false;
|
||||||
|
self.frame_buffer.clear();
|
||||||
|
self.current_read_ms = timestamp_ms;
|
||||||
|
self.start_decoding(timestamp_ms)?;
|
||||||
|
let frame_size = (self.info.width * self.info.height * 3) as usize;
|
||||||
|
for _ in 0..5 {
|
||||||
|
let mut dummy = vec![0u8; frame_size];
|
||||||
|
if let Some(ref mut reader) = self.stdout {
|
||||||
|
if reader.read_exact(&mut dummy).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enable_reverse(&mut self, start_ms: u64, end_ms: u64) {
|
||||||
|
self.stop();
|
||||||
|
self.frame_buffer.clear();
|
||||||
|
self.frame_times.clear();
|
||||||
|
self.buffer_loaded = false;
|
||||||
|
self.reverse_start_ms = start_ms;
|
||||||
|
self.reverse_end_ms = end_ms;
|
||||||
|
self.current_read_ms = end_ms;
|
||||||
|
self.display_time_ms = start_ms;
|
||||||
|
self.in_reverse_mode = true;
|
||||||
|
self.start_decoding(end_ms).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self) {
|
pub fn stop(&mut self) {
|
||||||
@@ -134,11 +186,33 @@ impl FFmpegDecoder {
|
|||||||
|
|
||||||
pub fn read_frame(&mut self) -> Result<Option<Vec<u8>>> {
|
pub fn read_frame(&mut self) -> Result<Option<Vec<u8>>> {
|
||||||
let frame_size = (self.info.width * self.info.height * 3) as usize;
|
let frame_size = (self.info.width * self.info.height * 3) as usize;
|
||||||
|
|
||||||
|
if let Some(frame) = self.frame_buffer.pop_front() {
|
||||||
|
if self.in_reverse_mode && self.frame_buffer.len() < 300 {
|
||||||
|
let _ = self.refill_reverse_buffer(600);
|
||||||
|
}
|
||||||
|
return Ok(Some(frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.in_reverse_mode {
|
||||||
|
if self.current_read_ms <= self.reverse_end_ms {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let _ = self.refill_reverse_buffer(30);
|
||||||
|
if let Some(frame) = self.frame_buffer.pop_front() {
|
||||||
|
return Ok(Some(frame));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let mut buffer = vec![0u8; frame_size];
|
let mut buffer = vec![0u8; frame_size];
|
||||||
|
|
||||||
if let Some(ref mut reader) = self.stdout {
|
if let Some(ref mut reader) = self.stdout {
|
||||||
match reader.read_exact(&mut buffer) {
|
match reader.read_exact(&mut buffer) {
|
||||||
Ok(_) => Ok(Some(buffer)),
|
Ok(_) => {
|
||||||
|
self.current_read_ms += 16;
|
||||||
|
Ok(Some(buffer))
|
||||||
|
}
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
@@ -146,6 +220,133 @@ impl FFmpegDecoder {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fill_reverse_buffer(&mut self, num_frames: usize) -> Result<()> {
|
||||||
|
let frame_size = (self.info.width * self.info.height * 3) as usize;
|
||||||
|
self.frame_buffer.clear();
|
||||||
|
self.frame_times.clear();
|
||||||
|
|
||||||
|
let frame_duration_ms = (1000.0 / self.info.fps) as u64;
|
||||||
|
let total_range = self.reverse_start_ms.saturating_sub(self.reverse_end_ms);
|
||||||
|
let max_frames = (total_range / frame_duration_ms).max(1) as usize;
|
||||||
|
let frames_to_read = max_frames.min(num_frames);
|
||||||
|
|
||||||
|
let mut temp_frames: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut temp_times: Vec<u64> = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..frames_to_read {
|
||||||
|
let mut buffer = vec![0u8; frame_size];
|
||||||
|
if let Some(ref mut reader) = self.stdout {
|
||||||
|
match reader.read_exact(&mut buffer) {
|
||||||
|
Ok(_) => {
|
||||||
|
temp_frames.push(buffer);
|
||||||
|
temp_times.push(self.current_read_ms);
|
||||||
|
self.current_read_ms += frame_duration_ms;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in (0..temp_frames.len()).rev() {
|
||||||
|
self.frame_buffer.push_back(temp_frames.remove(i));
|
||||||
|
self.frame_times.push_back(temp_times.remove(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refill_reverse_buffer(&mut self, _num_frames: usize) -> Result<()> {
|
||||||
|
if !self.in_reverse_mode {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.frame_buffer.clear();
|
||||||
|
self.frame_times.clear();
|
||||||
|
self.current_read_ms = self.reverse_end_ms;
|
||||||
|
self.display_time_ms = self.reverse_start_ms;
|
||||||
|
|
||||||
|
let frame_duration_ms = (1000.0 / self.info.fps) as u64;
|
||||||
|
let frames_to_read =
|
||||||
|
((self.reverse_start_ms - self.reverse_end_ms) / frame_duration_ms).max(1) as usize;
|
||||||
|
|
||||||
|
self.stop();
|
||||||
|
self.start_decoding(self.reverse_end_ms)?;
|
||||||
|
|
||||||
|
let frame_size = (self.info.width * self.info.height * 3) as usize;
|
||||||
|
|
||||||
|
let mut temp_frames: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut temp_times: Vec<u64> = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..frames_to_read {
|
||||||
|
let mut buffer = vec![0u8; frame_size];
|
||||||
|
if let Some(ref mut reader) = self.stdout {
|
||||||
|
match reader.read_exact(&mut buffer) {
|
||||||
|
Ok(_) => {
|
||||||
|
temp_frames.push(buffer);
|
||||||
|
temp_times.push(self.current_read_ms);
|
||||||
|
self.current_read_ms += frame_duration_ms;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in (0..temp_frames.len()).rev() {
|
||||||
|
self.frame_buffer.push_back(temp_frames.remove(i));
|
||||||
|
self.frame_times.push_back(temp_times.remove(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_frames_in_buffer(&self) -> bool {
|
||||||
|
!self.frame_buffer.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_size(&self) -> usize {
|
||||||
|
self.frame_buffer.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_buffer_front(&mut self) -> Option<Vec<u8>> {
|
||||||
|
self.frame_buffer.pop_front()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_frame_time(&mut self) -> Option<u64> {
|
||||||
|
if self.in_reverse_mode {
|
||||||
|
let time = self.display_time_ms;
|
||||||
|
self.display_time_ms = self
|
||||||
|
.display_time_ms
|
||||||
|
.saturating_sub((1000.0 / self.info.fps) as u64);
|
||||||
|
Some(time)
|
||||||
|
} else {
|
||||||
|
self.frame_times.pop_front()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_loading(&self) -> bool {
|
||||||
|
self.in_reverse_mode && self.frame_buffer.len() < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_time_ms(&self) -> u64 {
|
||||||
|
self.current_read_ms
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_time_ms(&mut self, ms: u64) {
|
||||||
|
self.current_read_ms = ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_reverse_end_ms(&self) -> u64 {
|
||||||
|
self.reverse_end_ms
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_at_boundary(&self) -> bool {
|
||||||
|
self.in_reverse_mode && self.current_read_ms <= self.reverse_end_ms
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for FFmpegDecoder {
|
impl Drop for FFmpegDecoder {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
//!
|
//!
|
||||||
//! Video playback, frame decoding, and rendering
|
//! Video playback, frame decoding, and rendering
|
||||||
|
|
||||||
pub mod video;
|
pub mod audio;
|
||||||
pub mod ffmpeg;
|
pub mod ffmpeg;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod video;
|
||||||
|
|
||||||
pub use video::Video;
|
pub use audio::AudioPlayer;
|
||||||
pub use state::{PlayerState, PlaybackState};
|
pub use state::{PlaybackState, PlayerState};
|
||||||
|
pub use video::VideoPlayer;
|
||||||
|
|||||||
@@ -3,58 +3,42 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use sdl2::pixels::PixelFormatEnum;
|
use sdl2::pixels::PixelFormatEnum;
|
||||||
use sdl2::rect::Rect;
|
use sdl2::rect::Rect;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
sdl: sdl2::Sdl,
|
sdl: sdl2::Sdl,
|
||||||
video_subsystem: sdl2::VideoSubsystem,
|
|
||||||
window: sdl2::video::Window,
|
|
||||||
canvas: sdl2::render::Canvas<sdl2::video::Window>,
|
canvas: sdl2::render::Canvas<sdl2::video::Window>,
|
||||||
texture_creator: sdl2::render::TextureCreator<sdl2::video::WindowContext>,
|
|
||||||
texture: Option<sdl2::render::Texture>,
|
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
pub fn new(title: &str, width: u32, height: u32) -> Result<Self> {
|
pub fn new(title: &str, width: u32, height: u32) -> Result<Self> {
|
||||||
let sdl = sdl2::init()?;
|
let sdl = sdl2::init().map_err(|e| anyhow::anyhow!("SDL init failed: {}", e))?;
|
||||||
let video_subsystem = sdl.video()?;
|
let video_subsystem = sdl
|
||||||
|
.video()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Video subsystem failed: {}", e))?;
|
||||||
|
|
||||||
let window = video_subsystem
|
let window = video_subsystem
|
||||||
.window(title, width, height)
|
.window(title, width, height)
|
||||||
.position_centered()
|
.position_centered()
|
||||||
.build()?;
|
.build()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Window creation failed: {}", e))?;
|
||||||
|
|
||||||
let canvas = window.into_canvas().build()?;
|
let canvas = window
|
||||||
let texture_creator = canvas.texture_creator();
|
.into_canvas()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Canvas creation failed: {}", e))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
sdl,
|
sdl,
|
||||||
video_subsystem,
|
|
||||||
window,
|
|
||||||
canvas,
|
canvas,
|
||||||
texture_creator,
|
|
||||||
texture: None,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_texture(&mut self, width: u32, height: u32) -> Result<()> {
|
pub fn update_frame(&mut self, texture: &sdl2::render::Texture) -> Result<()> {
|
||||||
self.texture = Some(
|
let _ = self.canvas.copy(texture, None, None);
|
||||||
self.texture_creator
|
|
||||||
.create_texture_streaming(PixelFormatEnum::RGB24, width, height)?
|
|
||||||
);
|
|
||||||
self.width = width;
|
|
||||||
self.height = height;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_texture(&mut self, data: &[u8]) -> Result<()> {
|
|
||||||
if let Some(ref mut texture) = self.texture {
|
|
||||||
texture.update(None, data, self.width as usize * 3)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,53 +47,43 @@ impl Renderer {
|
|||||||
self.canvas.clear();
|
self.canvas.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_bbox(&mut self, x: i32, y: i32, w: u32, h: u32, label: &str) {
|
pub fn draw_bbox(&mut self, x: i32, y: i32, w: u32, h: u32, _label: &str) {
|
||||||
// Draw rectangle border
|
self.canvas
|
||||||
self.canvas.set_draw_color(sdl2::pixels::Color::RGB(0, 255, 0));
|
.set_draw_color(sdl2::pixels::Color::RGB(0, 255, 0));
|
||||||
let _ = self.canvas.draw_rect(Rect::new(x, y, w, h));
|
let _ = self.canvas.draw_rect(Rect::new(x, y, w, h));
|
||||||
|
|
||||||
// Draw label background
|
|
||||||
let label_rect = Rect::new(x, y - 20, 100, 20);
|
let label_rect = Rect::new(x, y - 20, 100, 20);
|
||||||
self.canvas.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 180));
|
self.canvas
|
||||||
|
.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 180));
|
||||||
let _ = self.canvas.fill_rect(label_rect);
|
let _ = self.canvas.fill_rect(label_rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn present(&mut self) {
|
pub fn present(&mut self) {
|
||||||
// Draw texture if available
|
|
||||||
if let Some(ref texture) = self.texture {
|
|
||||||
self.canvas.copy(texture, None, None).ok();
|
|
||||||
}
|
|
||||||
self.canvas.present();
|
self.canvas.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_fullscreen(&mut self, fullscreen: bool) -> Result<()> {
|
|
||||||
if fullscreen {
|
|
||||||
self.window.set_fullscreen(sdl2::video::FullscreenType::Desktop)?;
|
|
||||||
} else {
|
|
||||||
self.window.set_fullscreen(sdl2::video::FullscreenType::Off)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(&mut self, width: u32, height: u32) -> Result<()> {
|
|
||||||
self.window.set_size(width, height)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_events(&mut self) -> Vec<sdl2::event::Event> {
|
pub fn poll_events(&mut self) -> Vec<sdl2::event::Event> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let pump = self.sdl.event_pump();
|
if let Ok(mut pump) = self.sdl.event_pump() {
|
||||||
if let Ok(pump) = pump {
|
|
||||||
for event in pump.poll_iter() {
|
for event in pump.poll_iter() {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn canvas(&mut self) -> &mut sdl2::render::Canvas<sdl2::video::Window> {
|
||||||
|
&mut self.canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Renderer {
|
impl Drop for Renderer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {}
|
||||||
// Cleanup handled automatically
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! 視頻播放核心實現
|
//! 視頻播放核心實現
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use crate::player::ffmpeg::{FFmpegDecoder, VideoInfo};
|
use crate::player::ffmpeg::{FFmpegDecoder, VideoInfo};
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ impl VideoPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek_frame(&mut self, frame: u64) -> Result<()> {
|
pub fn seek_frame(&mut self, frame: u64) -> Result<()> {
|
||||||
if let Some(ref decoder) = self.decoder {
|
if let Some(ref mut decoder) = self.decoder {
|
||||||
if let Some(info) = &self.info {
|
if let Some(ref info) = self.info {
|
||||||
let timestamp_ms = (frame * 1000) / info.fps as u64;
|
let timestamp_ms = (frame * 1000) / info.fps as u64;
|
||||||
decoder.seek(timestamp_ms)?;
|
decoder.seek(timestamp_ms)?;
|
||||||
self.current_frame = frame;
|
self.current_frame = frame;
|
||||||
@@ -84,9 +84,9 @@ impl VideoPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek_time(&mut self, ms: u64) -> Result<()> {
|
pub fn seek_time(&mut self, ms: u64) -> Result<()> {
|
||||||
if let Some(ref decoder) = self.decoder {
|
if let Some(ref mut decoder) = self.decoder {
|
||||||
decoder.seek(ms)?;
|
decoder.seek(ms)?;
|
||||||
if let Some(info) = &self.info {
|
if let Some(ref info) = self.info {
|
||||||
self.current_frame = (ms * info.fps as u64) / 1000;
|
self.current_frame = (ms * info.fps as u64) / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/search/mod.rs
Normal file
96
src/search/mod.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SearchResult {
|
||||||
|
pub frame: u64,
|
||||||
|
pub time_ms: u64,
|
||||||
|
pub text: String,
|
||||||
|
pub score: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VectorSearcher {
|
||||||
|
qdrant_url: String,
|
||||||
|
collection: String,
|
||||||
|
asr_loader: Option<super::overlay::AsrLoader>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VectorSearcher {
|
||||||
|
pub fn new(qdrant_url: &str, collection: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
qdrant_url: qdrant_url.to_string(),
|
||||||
|
collection: collection.to_string(),
|
||||||
|
asr_loader: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_asr(&mut self, path: &Path) -> Result<()> {
|
||||||
|
let loader =
|
||||||
|
super::overlay::AsrLoader::load(path).context("Failed to load ASR for search")?;
|
||||||
|
info!(
|
||||||
|
"Loaded ASR with {} segments for search",
|
||||||
|
loader.segment_count()
|
||||||
|
);
|
||||||
|
self.asr_loader = Some(loader);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search(&self, query: &str) -> Vec<SearchResult> {
|
||||||
|
info!("Searching for: {}", query);
|
||||||
|
|
||||||
|
if let Some(ref asr) = self.asr_loader {
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
let mut results: Vec<SearchResult> = Vec::new();
|
||||||
|
|
||||||
|
for segment in asr.get_all_segments() {
|
||||||
|
let text_lower = segment.text.to_lowercase();
|
||||||
|
if text_lower.contains(&query_lower) {
|
||||||
|
let score = self.calculate_score(&query_lower, &text_lower);
|
||||||
|
results.push(SearchResult {
|
||||||
|
frame: (segment.start * 60.0) as u64,
|
||||||
|
time_ms: (segment.start * 1000.0) as u64,
|
||||||
|
text: segment.text.clone(),
|
||||||
|
score,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
|
||||||
|
results.truncate(10);
|
||||||
|
|
||||||
|
info!("Found {} results", results.len());
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("No ASR loaded for search");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_score(&self, query: &str, text: &str) -> f32 {
|
||||||
|
let query_words: Vec<&str> = query.split_whitespace().collect();
|
||||||
|
let text_words: Vec<&str> = text.split_whitespace().collect();
|
||||||
|
|
||||||
|
let mut matches = 0;
|
||||||
|
for qw in &query_words {
|
||||||
|
for tw in &text_words {
|
||||||
|
if tw.contains(qw) {
|
||||||
|
matches += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(matches as f32) / (query_words.len() as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_available(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_searcher() -> VectorSearcher {
|
||||||
|
VectorSearcher::new("http://localhost:6333", "AccusysDB")
|
||||||
|
}
|
||||||
1
target/.rustc_info.json
Normal file
1
target/.rustc_info.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"rustc_fingerprint":7910028290815472967,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: aarch64-apple-darwin\nrelease: 1.92.0\nLLVM version: 21.1.3\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/accusys/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
|
||||||
3
target/CACHEDIR.TAG
Normal file
3
target/CACHEDIR.TAG
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by cargo.
|
||||||
|
# For information about cache directory tags see https://bford.info/cachedir/
|
||||||
0
target/debug/.cargo-lock
Normal file
0
target/debug/.cargo-lock
Normal file
BIN
target/debug/.fingerprint/adler2-41b0dd48552c20c5/dep-lib-adler2
Normal file
BIN
target/debug/.fingerprint/adler2-41b0dd48552c20c5/dep-lib-adler2
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cafaf23dcf812b49
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":5347358027863023418,"path":15149082351976033191,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/adler2-41b0dd48552c20c5/dep-lib-adler2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ac3a9d80f9cd03e9
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":5347358027863023418,"path":2498799609881310857,"deps":[[1363051979936526615,"memchr",false,16761034740145381771]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-88b3cd4431528e8d/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
621c37ce8678a92d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"alloc\"]","declared_features":"[\"alloc\", \"default\", \"fresh-rust\", \"nightly\", \"serde\", \"std\"]","target":5388200169723499962,"profile":8526714817676984181,"path":9266711163172033080,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/allocator-api2-5e7341e257c04569/dep-lib-allocator_api2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
35fdd6938cddbe96
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"auto\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":11278316191512382530,"profile":11459093354283867776,"path":1368805165058083929,"deps":[[5652275617566266604,"anstyle_query",false,8874033107635935327],[7098682853475662231,"anstyle",false,12300851714675958605],[7711617929439759244,"colorchoice",false,3274431315892103036],[7727459912076845739,"is_terminal_polyfill",false,12638843975273980479],[11410867133969439143,"anstyle_parse",false,3908141501184340663],[17716308468579268865,"utf8parse",false,6151700546281254876]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstream-5c7ee89b12f2f0af/dep-lib-anstream","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
df1b360d59411b84
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"auto\", \"default\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":11278316191512382530,"profile":8255941854203129366,"path":14237504360179493621,"deps":[[2608044744973004659,"anstyle_parse",false,1360650918450305252],[5652275617566266604,"anstyle_query",false,8874033107635935327],[7098682853475662231,"anstyle",false,12300851714675958605],[7711617929439759244,"colorchoice",false,3274431315892103036],[7727459912076845739,"is_terminal_polyfill",false,12638843975273980479],[17716308468579268865,"utf8parse",false,6151700546281254876]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstream-cbae704d8623b559/dep-lib-anstream","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
4de303387667b5aa
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":6165884447290141869,"profile":8255941854203129366,"path":1622006416877328561,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-0c8ab8bd38595486/dep-lib-anstyle","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
e4188a21cd00e212
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":10225663410500332907,"profile":8255941854203129366,"path":13053215332907560763,"deps":[[17716308468579268865,"utf8parse",false,6151700546281254876]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-parse-392f1d4c1d178323/dep-lib-anstyle_parse","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
b7f20d36fd813c36
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":10225663410500332907,"profile":11459093354283867776,"path":14980379806015639209,"deps":[[17716308468579268865,"utf8parse",false,6151700546281254876]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-parse-584c63468a348cbd/dep-lib-anstyle_parse","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
5f18b24118e6267b
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":10705714425685373190,"profile":14848920055892446256,"path":4316627989718112974,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anstyle-query-3a5ace2dbec2ae9f/dep-lib-anstyle_query","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
edc67d7f466199ec
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":5408242616063297496,"profile":3033921117576893,"path":15975461479635710502,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-24ec003b790501cc/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
bc6796b89332f94d
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[12478428894219133322,"build_script_build",false,17048764819802277613]],"local":[{"RerunIfChanged":{"output":"debug/build/anyhow-40fe6c014f4e8bb6/output","paths":["src/nightly.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0}
|
||||||
BIN
target/debug/.fingerprint/anyhow-d838341a95e3a987/dep-lib-anyhow
Normal file
BIN
target/debug/.fingerprint/anyhow-d838341a95e3a987/dep-lib-anyhow
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
acdf419166c7c9f4
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":1563897884725121975,"profile":5347358027863023418,"path":8136069237744135612,"deps":[[12478428894219133322,"build_script_build",false,5618577620159850428]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-d838341a95e3a987/dep-lib-anyhow","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
28cf21415662fb90
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":5347358027863023418,"path":11009143541493348455,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/atomic-waker-b0bffc3cf38abbeb/dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1477b7ed36cbf522
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":3033921117576893,"path":10045383212328745351,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/autocfg-e2e21eab783460c2/dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
BIN
target/debug/.fingerprint/base64-80c574e62179e036/dep-lib-base64
Normal file
BIN
target/debug/.fingerprint/base64-80c574e62179e036/dep-lib-base64
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0c8f0e4e099d8852
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":5347358027863023418,"path":2443796168128073955,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/base64-80c574e62179e036/dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
2ef398f319cb65f8
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"std\"]","declared_features":"[\"arbitrary\", \"bytemuck\", \"example_generated\", \"serde\", \"serde_core\", \"std\"]","target":7691312148208718491,"profile":5347358027863023418,"path":1039962568932081109,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-ccb9bc03a9edff55/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0375a828b8d21fda
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\"]","declared_features":"[\"compiler_builtins\", \"core\", \"default\", \"example_generated\", \"rustc-dep-of-std\"]","target":12919857562465245259,"profile":5347358027863023418,"path":3910279081600245434,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-fc563d35d55f7f61/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
BIN
target/debug/.fingerprint/block-7f847bd28d463d32/dep-lib-block
Normal file
BIN
target/debug/.fingerprint/block-7f847bd28d463d32/dep-lib-block
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
b63146aeea754deb
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[]","declared_features":"[]","target":6813287393046767197,"profile":5347358027863023418,"path":7951925049428246312,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/block-7f847bd28d463d32/dep-lib-block","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
BIN
target/debug/.fingerprint/block2-2dc27831c2863634/dep-lib-block2
Normal file
BIN
target/debug/.fingerprint/block2-2dc27831c2863634/dep-lib-block2
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
649d78e788b7a9f2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"compiler-rt\", \"default\", \"gnustep-1-7\", \"gnustep-1-8\", \"gnustep-1-9\", \"gnustep-2-0\", \"gnustep-2-1\", \"std\", \"unstable-coerce-pointee\", \"unstable-objfw\", \"unstable-private\", \"unstable-winobjc\"]","target":8611651741325771798,"profile":8196097686603091492,"path":14352777551528949466,"deps":[[5711497484949304150,"objc2",false,9362850287131531880]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/block2-2dc27831c2863634/dep-lib-block2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
b8b18efa93809b0f
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\"]","declared_features":"[\"allocator-api2\", \"allocator_api\", \"bench_allocator_api\", \"boxed\", \"collections\", \"default\", \"serde\", \"std\"]","target":10625613344215589528,"profile":5347358027863023418,"path":18342583852565774136,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bumpalo-9214dc9bc289b3ef/dep-lib-bumpalo","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
BIN
target/debug/.fingerprint/bytes-5544e1f3425bf39c/dep-lib-bytes
Normal file
BIN
target/debug/.fingerprint/bytes-5544e1f3425bf39c/dep-lib-bytes
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This file has an mtime of when this was started.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fc13a733e8b70924
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rustc":18415816196306954164,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"extra-platforms\", \"serde\", \"std\"]","target":11402411492164584411,"profile":7855341030452660939,"path":9738655571473828057,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bytes-5544e1f3425bf39c/dep-lib-bytes","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user