diff --git a/src/main.rs b/src/main.rs
index 214a60dddd4753535e5a79a1ccc8d7207bfa8a35..8e54dc40cccb1a0eeaa027d6e2917fc827860b3b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,10 @@ mod iotro;
 mod render;
 mod time;
 
-use crate::time::{parse_date, parse_time, Date, Time};
+use crate::{
+	render::Renderer,
+	time::{parse_date, parse_time, Date, Time}
+};
 use camino::Utf8PathBuf as PathBuf;
 use clap::Parser;
 use rational::Rational;
@@ -85,10 +88,12 @@ struct ProjectLecture {
 struct ProjectSource {
 	files: Vec<String>,
 
-	#[serde_as(as = "DisplayFromStr")]
-	first_file_start: Time,
-	#[serde_as(as = "DisplayFromStr")]
-	last_file_end: Time,
+	#[serde_as(as = "Option<DisplayFromStr>")]
+	start: Option<Time>,
+	#[serde_as(as = "Option<DisplayFromStr>")]
+	end: Option<Time>,
+	#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")]
+	fast: Vec<(Time, Time)>,
 
 	metadata: Option<ProjectSourceMetadata>
 }
@@ -103,7 +108,8 @@ struct ProjectSourceMetadata {
 	#[serde_as(as = "DisplayFromStr")]
 	source_fps: Rational,
 	/// The time base of the source video.
-	source_tbn: u32,
+	#[serde_as(as = "DisplayFromStr")]
+	source_tbn: Rational,
 	/// The resolution of the source video.
 	source_res: Resolution,
 	/// The sample rate of the source audio.
@@ -113,6 +119,8 @@ struct ProjectSourceMetadata {
 #[derive(Default, Deserialize, Serialize)]
 struct ProjectProgress {
 	preprocessed: bool,
+	asked_start_end: bool,
+	asked_fast: bool,
 	rendered: bool,
 	transcoded: BTreeSet<Resolution>
 }
@@ -145,7 +153,7 @@ fn main() {
 
 	// let's see if we need to initialise the project
 	let project_path = directory.join("project.toml");
-	let project = if project_path.exists() {
+	let mut project = if project_path.exists() {
 		toml::from_slice(&fs::read(&project_path).unwrap()).unwrap()
 	} else {
 		let dirname = directory.file_name().unwrap();
@@ -167,21 +175,13 @@ fn main() {
 		assert!(!files.is_empty());
 		println!("I found the following source files: {files:?}");
 
-		let first_file_start = ask_time(format_args!(
-			"Please take a look at the file {} and tell me the first second you want included",
-			files.first().unwrap()
-		));
-		let last_file_end = ask_time(format_args!(
-			"Please take a look at the file {} and tell me the last second you want included",
-			files.last().unwrap()
-		));
-
 		let project = Project {
 			lecture: ProjectLecture { course, date },
 			source: ProjectSource {
 				files,
-				first_file_start,
-				last_file_end,
+				start: None,
+				end: None,
+				fast: Vec::new(),
 				metadata: None
 			},
 			progress: Default::default()
@@ -191,5 +191,52 @@ fn main() {
 	};
 
 	println!("{}", toml::to_string(&project).unwrap());
+
+	let renderer = Renderer::new(&directory, &project).unwrap();
+	let recording = renderer.recording_mp4();
+
+	// preprocess the video
+	if !project.progress.preprocessed {
+		renderer.preprocess(&mut project).unwrap();
+		project.progress.preprocessed = true;
+
+		println!("{}", toml::to_string(&project).unwrap());
+		fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
+	}
+
+	// ask the user about start and end times
+	if !project.progress.asked_start_end {
+		project.source.start = Some(ask_time(format_args!(
+			"Please take a look at the file {recording} and tell me the first second you want included"
+		)));
+		project.source.end = Some(ask_time(format_args!(
+			"Please take a look at the file {recording} and tell me the last second you want included"
+		)));
+		project.progress.asked_start_end = true;
+
+		println!("{}", toml::to_string(&project).unwrap());
+		fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
+	}
+
+	// ask the user about fast forward times
+	if !project.progress.asked_fast {
+		loop {
+			let start = ask_time(format_args!(
+				"Please take a look at the file {recording} and tell me the first second you want fast-forwarded. You may reply with `0` if there are no more fast-forward sections"
+			));
+			if start.seconds == 0 && start.micros == 0 {
+				break;
+			}
+			let end = ask_time(format_args!(
+				"Please tell me the last second you want fast-forwarded"
+			));
+			project.source.fast.push((start, end));
+		}
+		project.progress.asked_fast = true;
+
+		println!("{}", toml::to_string(&project).unwrap());
+		fs::write(&project_path, toml::to_string(&project).unwrap().as_bytes()).unwrap();
+	}
+
 	// render(&directory, &project).unwrap();
 }
diff --git a/src/render/ffmpeg.rs b/src/render/ffmpeg.rs
index f578daf1cec49c416bb4be7096b47eb0241d0b57..4ac812b984e70a9c58aafb019902bedb32e4a1ab 100644
--- a/src/render/ffmpeg.rs
+++ b/src/render/ffmpeg.rs
@@ -4,9 +4,9 @@ use crate::{
 	time::{format_time, Time}
 };
 use anyhow::bail;
-use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
+use camino::Utf8PathBuf as PathBuf;
 use rational::Rational;
-use std::{borrow::Cow, process::Command};
+use std::{borrow::Cow, fmt::Write as _, process::Command};
 
 pub(crate) struct FfmpegInput {
 	pub(crate) concat: bool,
@@ -54,6 +54,7 @@ pub(crate) struct Ffmpeg {
 	filters: Vec<Filter>,
 	filters_output: Cow<'static, str>,
 	loudnorm: bool,
+	duration: Option<Time>,
 	output: PathBuf,
 
 	filter_idx: usize
@@ -66,6 +67,7 @@ impl Ffmpeg {
 			filters: Vec::new(),
 			filters_output: "0".into(),
 			loudnorm: false,
+			duration: None,
 			output,
 
 			filter_idx: 0
@@ -97,13 +99,18 @@ impl Ffmpeg {
 		self
 	}
 
+	pub fn set_duration(&mut self, duration: Time) -> &mut Self {
+		self.duration = Some(duration);
+		self
+	}
+
 	pub fn run(mut self) -> anyhow::Result<()> {
 		let mut cmd = cmd();
-		cmd.arg("ffmpeg").arg("-hide_banner");
+		cmd.arg("ffmpeg").arg("-hide_banner").arg("-y");
 
 		// determine whether the video need to be re-encoded
-		let venc = self.filters.iter().any(|f| f.is_video_filter());
-		let aenc = self.filters.iter().any(|f| f.is_audio_filter()) || self.loudnorm;
+		let venc = !self.filters.is_empty();
+		let aenc = !self.filters.is_empty() || self.loudnorm;
 
 		// initialise a vaapi device if one exists
 		let vaapi_device: PathBuf = "/dev/dri/renderD128".into();
@@ -120,7 +127,7 @@ impl Ffmpeg {
 		// always try to synchronise audio
 		cmd.arg("-async").arg("1");
 
-		// TODO apply filters
+		// apply filters
 		match (self.loudnorm, self.filters) {
 			(true, f) if f.is_empty() => {
 				cmd.arg("-af").arg("pan=mono|c0=FR,loudnorm,pan=stereo|c0=c0|c1=c0,aformat=sample_rates=48000");
@@ -133,8 +140,17 @@ impl Ffmpeg {
 				for filter in f {
 					filter.append_to_complex_filter(&mut complex, &mut self.filter_idx);
 				}
+				if vaapi {
+					write!(
+						complex,
+						"{}format=nv12,hwupload[v]",
+						channel('v', &self.filters_output)
+					);
+				} else {
+					write!(complex, "{}null[v]", channel('v', &self.filters_output));
+				}
 				cmd.arg("-filter_complex").arg(complex);
-				cmd.arg("-map").arg(channel('v', &self.filters_output));
+				cmd.arg("-map").arg("[v]");
 				cmd.arg("-map").arg(channel('a', &self.filters_output));
 			}
 		}
@@ -157,7 +173,11 @@ impl Ffmpeg {
 			cmd.arg("-c:a").arg("copy");
 		}
 
+		if let Some(duration) = self.duration {
+			cmd.arg("-t").arg(format_time(duration));
+		}
 		cmd.arg(&self.output);
+
 		let status = cmd.status()?;
 		if status.success() {
 			Ok(())
diff --git a/src/render/mod.rs b/src/render/mod.rs
index cc4decb3f8d2d3e51d586e594593fd255ec2a957..026cd79ac2673e6f7bb5795de5742ed3591f6f59 100644
--- a/src/render/mod.rs
+++ b/src/render/mod.rs
@@ -39,7 +39,7 @@ fn cmd() -> Command {
 		.arg("-exuo")
 		.arg("pipefail")
 		.arg("-c")
-		.arg("exec");
+		.arg("exec \"$0\" \"${@}\"");
 	cmd
 }
 
@@ -61,7 +61,9 @@ fn read_output(cmd: &mut Command) -> anyhow::Result<String> {
 			out.status.code()
 		);
 	}
-	String::from_utf8(out.stdout).context("Command returned non-utf8 output")
+	String::from_utf8(out.stdout)
+		.context("Command returned non-utf8 output")
+		.map(|str| str.trim().into())
 }
 
 fn ffprobe_video(query: &str, input: &Path) -> anyhow::Result<String> {
@@ -90,23 +92,6 @@ fn ffprobe_audio(query: &str, concat_input: &Path) -> anyhow::Result<String> {
 	)
 }
 
-fn ffmpeg() -> Command {
-	let mut cmd = cmd();
-	cmd.arg("ffmpeg")
-		.arg("-hide_banner")
-		.arg("-vaapi_device")
-		.arg("/dev/dri/renderD128");
-	cmd
-}
-
-fn render_svg(fps: Rational, tbn: u32, input: &Path, duration: Time, output: &Path) {
-	let mut cmd = ffmpeg();
-	cmd.arg("-framerate").arg(fps.to_string());
-	cmd.arg("-loop").arg("1");
-	cmd.arg("-i").arg(input);
-	cmd.arg("-c:v").arg("libx264");
-}
-
 pub(crate) struct Renderer<'a> {
 	/// The directory with all the sources.
 	directory: &'a Path,
@@ -128,21 +113,26 @@ fn svg2mp4(svg: PathBuf, mp4: PathBuf, duration: Time) -> anyhow::Result<()> {
 		output: "out".into()
 	});
 	ffmpeg.set_filter_output("out");
+	ffmpeg.set_duration(duration);
 	ffmpeg.run()
 }
 
 fn svg2png(svg: &Path, png: &Path, size: usize) -> anyhow::Result<()> {
 	let mut cmd = cmd();
 	let size = size.to_string();
-	cmd.arg("inkscape").arg("-w").arg(&size).arg("-h").arg(&size);
+	cmd.arg("inkscape")
+		.arg("-w")
+		.arg(&size)
+		.arg("-h")
+		.arg(&size);
 	cmd.arg(svg).arg("-o").arg(png);
-	
+
 	let status = cmd.status()?;
-		if status.success() {
-			Ok(())
-		} else {
-			bail!("inkscape failed with exit code {:?}", status.code())
-		}
+	if status.success() {
+		Ok(())
+	} else {
+		bail!("inkscape failed with exit code {:?}", status.code())
+	}
 }
 
 impl<'a> Renderer<'a> {
@@ -153,6 +143,7 @@ impl<'a> Renderer<'a> {
 			format_date(project.lecture.date)
 		);
 		let target = directory.join(&slug);
+		fs::create_dir_all(&target)?;
 
 		Ok(Self {
 			directory,
@@ -161,20 +152,24 @@ impl<'a> Renderer<'a> {
 		})
 	}
 
+	pub(crate) fn recording_mp4(&self) -> PathBuf {
+		self.target.join("recording.mp4")
+	}
+
 	pub(crate) fn preprocess(&self, project: &mut Project) -> anyhow::Result<()> {
 		assert!(!project.progress.preprocessed);
 
 		let recording_txt = self.target.join("recording.txt");
 		let mut file = File::create(&recording_txt)?;
 		for filename in &project.source.files {
-			writeln!(file, "file {:?}", self.directory.join(filename).to_string());
+			writeln!(file, "file '{}'", self.directory.join(filename).to_string());
 		}
 		drop(file);
 
-		println!("\x1B[1m ==> Concatenating Video and Normalising Audio ...");
+		println!("\x1B[1m ==> Concatenating Video and Normalising Audio ...\x1B[0m");
 		let source_sample_rate =
 			ffprobe_audio("stream=sample_rate", &recording_txt)?.parse()?;
-		let recording_mp4 = self.target.join("recording.mp4");
+		let recording_mp4 = self.recording_mp4();
 		let mut ffmpeg = Ffmpeg::new(recording_mp4.clone());
 		ffmpeg.add_input(FfmpegInput {
 			concat: true,
@@ -201,6 +196,8 @@ impl<'a> Renderer<'a> {
 			source_sample_rate
 		});
 
+		println!("\x1B[1m ==> Preparing assets ...\x1B[0m");
+
 		// render intro to svg then mp4
 		let intro_svg = self.target.join("intro.svg");
 		fs::write(
@@ -225,7 +222,7 @@ impl<'a> Renderer<'a> {
 		svg2mp4(outro_svg, outro_mp4, Time {
 			seconds: 5,
 			micros: 0
-		});
+		})?;
 
 		// copy logo then render to png
 		let logo_svg = self.target.join("logo.svg");
@@ -246,7 +243,11 @@ impl<'a> Renderer<'a> {
 			))
 		)?;
 		let fastforward_png = self.target.join("fastforward.png");
-		svg2png(&fastforward_svg, &fastforward_png, 128 * 1920 / source_res.width())?;
+		svg2png(
+			&fastforward_svg,
+			&fastforward_png,
+			128 * 1920 / source_res.width()
+		)?;
 
 		Ok(())
 	}