fix: address final review findings (clip reset, codecs, paths, timeline)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -3297,6 +3297,7 @@ dependencies = [
|
|||||||
name = "tauri-app"
|
name = "tauri-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ tokio = { version = "1.53.1", features = ["full"] }
|
|||||||
thiserror = "2.0.20"
|
thiserror = "2.0.20"
|
||||||
uuid = { version = "1.26.1", features = ["v4"] }
|
uuid = { version = "1.26.1", features = ["v4"] }
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.27.0"
|
||||||
|
dirs = "6.0.0"
|
||||||
|
|
||||||
|
|
||||||
# Read the optimization guideline for more details: https://tauri.app/concept/size/#cargo-configuration
|
# Read the optimization guideline for more details: https://tauri.app/concept/size/#cargo-configuration
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub async fn export_clips(
|
|||||||
on_event: Channel<ExportEvent>,
|
on_event: Channel<ExportEvent>,
|
||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let output_directory = clip_exporter::expand_tilde_path(&config.output_directory);
|
||||||
let ext = clip_exporter::get_extension(&config.source_file_path, &config.cut_mode);
|
let ext = clip_exporter::get_extension(&config.source_file_path, &config.cut_mode);
|
||||||
|
|
||||||
match config.export_scope {
|
match config.export_scope {
|
||||||
@@ -40,7 +41,7 @@ pub async fn export_clips(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let output = clip_exporter::generate_output_path(
|
let output = clip_exporter::generate_output_path(
|
||||||
&config.output_directory,
|
&output_directory,
|
||||||
&config.video_title,
|
&config.video_title,
|
||||||
&clip.label,
|
&clip.label,
|
||||||
&ext,
|
&ext,
|
||||||
@@ -69,7 +70,7 @@ pub async fn export_clips(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let output = clip_exporter::generate_output_path(
|
let output = clip_exporter::generate_output_path(
|
||||||
&config.output_directory,
|
&output_directory,
|
||||||
&config.video_title,
|
&config.video_title,
|
||||||
"Merged",
|
"Merged",
|
||||||
&ext,
|
&ext,
|
||||||
|
|||||||
@@ -23,11 +23,20 @@ pub fn build_ffmpeg_args(
|
|||||||
args.extend(["-c".to_string(), "copy".to_string()]);
|
args.extend(["-c".to_string(), "copy".to_string()]);
|
||||||
}
|
}
|
||||||
CutMode::Precise { format: _ } => {
|
CutMode::Precise { format: _ } => {
|
||||||
|
let ext = Path::new(output)
|
||||||
|
.extension()
|
||||||
|
.map(|e| e.to_string_lossy().to_ascii_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (video_codec, audio_codec) = match ext.as_str() {
|
||||||
|
"webm" => ("libvpx-vp9", "libopus"),
|
||||||
|
"mp4" | "mkv" => ("libx264", "aac"),
|
||||||
|
_ => ("libx264", "aac"),
|
||||||
|
};
|
||||||
args.extend([
|
args.extend([
|
||||||
"-c:v".to_string(),
|
"-c:v".to_string(),
|
||||||
"libx264".to_string(),
|
video_codec.to_string(),
|
||||||
"-c:a".to_string(),
|
"-c:a".to_string(),
|
||||||
"aac".to_string(),
|
audio_codec.to_string(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +45,20 @@ pub fn build_ffmpeg_args(
|
|||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expand_tilde_path(path: &str) -> String {
|
||||||
|
if path == "~" {
|
||||||
|
return dirs::home_dir()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| path.to_string());
|
||||||
|
}
|
||||||
|
if let Some(rest) = path.strip_prefix("~/") {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
return home.join(rest).to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_output_path(
|
pub fn generate_output_path(
|
||||||
output_dir: &str,
|
output_dir: &str,
|
||||||
video_title: &str,
|
video_title: &str,
|
||||||
@@ -206,6 +229,18 @@ mod tests {
|
|||||||
let args = build_ffmpeg_args(&clip, "input.mp4", "output.mp4", &mode);
|
let args = build_ffmpeg_args(&clip, "input.mp4", "output.mp4", &mode);
|
||||||
assert!(args.contains(&"-c:v".to_string()));
|
assert!(args.contains(&"-c:v".to_string()));
|
||||||
assert!(args.contains(&"libx264".to_string()));
|
assert!(args.contains(&"libx264".to_string()));
|
||||||
|
assert!(args.contains(&"aac".to_string()));
|
||||||
|
|
||||||
|
let webm_args = build_ffmpeg_args(&clip, "input.mp4", "output.webm", &mode);
|
||||||
|
assert!(webm_args.contains(&"libvpx-vp9".to_string()));
|
||||||
|
assert!(webm_args.contains(&"libopus".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expand_tilde_path() {
|
||||||
|
let expanded = expand_tilde_path("~/Videos");
|
||||||
|
assert!(!expanded.starts_with("~/"));
|
||||||
|
assert!(expanded.ends_with("Videos") || expanded.contains("Videos"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
import ExportDialog from '$lib/components/ExportDialog.svelte';
|
import ExportDialog from '$lib/components/ExportDialog.svelte';
|
||||||
import { loadPreferences } from '$lib/stores/preferences.svelte';
|
import { loadPreferences } from '$lib/stores/preferences.svelte';
|
||||||
import { getSelectedClipId, removeClip } from '$lib/stores/clips.svelte';
|
import { getSelectedClipId, removeClip } from '$lib/stores/clips.svelte';
|
||||||
import { adjustShuttle } from '$lib/transport/playback';
|
import { adjustShuttle, resetShuttleRate } from '$lib/transport/playback';
|
||||||
|
import { session } from '$lib/stores/videoSession.svelte';
|
||||||
import type { TransportKeyAction } from '$lib/transport/playback';
|
import type { TransportKeyAction } from '$lib/transport/playback';
|
||||||
|
|
||||||
let showSetupWizard = $state(true);
|
let showSetupWizard = $state(true);
|
||||||
@@ -24,6 +25,13 @@
|
|||||||
loadPreferences();
|
loadPreferences();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (session.status === 'resolving') {
|
||||||
|
shuttleRate = 1;
|
||||||
|
resetShuttleRate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function dispatchTransport(action: TransportKeyAction) {
|
function dispatchTransport(action: TransportKeyAction) {
|
||||||
transportControls?.handleKeyAction(action);
|
transportControls?.handleKeyAction(action);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (session.duration > 0) {
|
if (session.duration > 0) {
|
||||||
|
timelineState.visibleStart = 0;
|
||||||
timelineState.visibleEnd = session.duration;
|
timelineState.visibleEnd = session.duration;
|
||||||
|
timelineState.zoom = 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
extractWaveform,
|
extractWaveform,
|
||||||
} from '$lib/bindings/mediaAnalysis';
|
} from '$lib/bindings/mediaAnalysis';
|
||||||
import { clearThumbnailCache } from '$lib/timeline/thumbnailRenderer';
|
import { clearThumbnailCache } from '$lib/timeline/thumbnailRenderer';
|
||||||
|
import { clearAll as clearAllClips } from '$lib/stores/clips.svelte';
|
||||||
|
|
||||||
export type SessionStatus = 'idle' | 'resolving' | 'ready' | 'error';
|
export type SessionStatus = 'idle' | 'resolving' | 'ready' | 'error';
|
||||||
export type DownloadStatus = 'idle' | 'downloading' | 'complete' | 'failed';
|
export type DownloadStatus = 'idle' | 'downloading' | 'complete' | 'failed';
|
||||||
@@ -32,6 +33,7 @@ export const session = $state({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function setMetadata(meta: VideoMetadata) {
|
export function setMetadata(meta: VideoMetadata) {
|
||||||
|
clearAllClips();
|
||||||
session.url = meta.url;
|
session.url = meta.url;
|
||||||
session.title = meta.title;
|
session.title = meta.title;
|
||||||
session.duration = meta.duration;
|
session.duration = meta.duration;
|
||||||
@@ -73,6 +75,7 @@ export function setError(msg: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setResolving() {
|
export function setResolving() {
|
||||||
|
clearAllClips();
|
||||||
clearMediaFields();
|
clearMediaFields();
|
||||||
session.status = 'resolving';
|
session.status = 'resolving';
|
||||||
session.error = null;
|
session.error = null;
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ export function computeZoom(
|
|||||||
duration: number
|
duration: number
|
||||||
): { visibleStart: number; visibleEnd: number; zoom: number } {
|
): { visibleStart: number; visibleEnd: number; zoom: number } {
|
||||||
const currentRange = state.visibleEnd - state.visibleStart;
|
const currentRange = state.visibleEnd - state.visibleStart;
|
||||||
|
if (currentRange <= 0) {
|
||||||
|
return {
|
||||||
|
visibleStart: state.visibleStart,
|
||||||
|
visibleEnd: state.visibleEnd,
|
||||||
|
zoom: state.zoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
const newRange = currentRange / factor;
|
const newRange = currentRange / factor;
|
||||||
const clampedRange = Math.max(MIN_VISIBLE_RANGE, Math.min(newRange, duration));
|
const clampedRange = Math.max(MIN_VISIBLE_RANGE, Math.min(newRange, duration));
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ export function jumpKeyframe(direction: 1 | -1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetShuttleRate() {
|
||||||
|
const videoEl = document.querySelector('video');
|
||||||
|
if (videoEl) {
|
||||||
|
videoEl.playbackRate = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number {
|
export function adjustShuttle(dir: 1 | -1, shuttleRate: number): number {
|
||||||
const videoEl = document.querySelector('video');
|
const videoEl = document.querySelector('video');
|
||||||
if (!videoEl) return shuttleRate;
|
if (!videoEl) return shuttleRate;
|
||||||
|
|||||||
Reference in New Issue
Block a user