Skip to content

Commit

Permalink
feat: Recording improvements + playback optimisation
Browse files Browse the repository at this point in the history
  • Loading branch information
richiemcilroy committed Jun 4, 2024
1 parent b0762c4 commit 87abd72
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 99 deletions.
4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ impl MediaRecorder {
println!("Starting audio recording and processing...");
let audio_output_chunk_pattern = format!("{}/audio_recording_%03d.aac", audio_file_path_owned);
let audio_segment_list_filename = format!("{}/segment_list.txt", audio_file_path_owned);
let video_output_chunk_pattern = format!("{}/video_recording_%03d.mp4", video_file_path_owned);
let video_output_chunk_pattern = format!("{}/video_recording_%03d.ts", video_file_path_owned);
let video_segment_list_filename = format!("{}/segment_list.txt", video_file_path_owned);

let mut audio_filters = Vec::new();
Expand Down Expand Up @@ -458,7 +458,7 @@ impl MediaRecorder {
"-segment_time", "3",
"-segment_time_delta", "0.01",
"-segment_list", &video_segment_list_filename,
"-segment_format", "mp4",
"-segment_format", "ts",
"-movflags", "frag_keyframe+empty_moov",
"-reset_timestamps", "1",
&video_output_chunk_pattern,
Expand Down
97 changes: 90 additions & 7 deletions apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ use reqwest;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::process::{Command, Output};
use std::str;
use std::fs;
use regex::Regex;
use serde_json::Value as JsonValue;

use crate::recording::RecordingOptions;
use crate::utils::{ffmpeg_path_as_str};

pub async fn upload_file(
options: Option<RecordingOptions>,
Expand All @@ -14,6 +19,9 @@ pub async fn upload_file(
if let Some(ref options) = options {
println!("Uploading video...");

let duration = get_video_duration(&file_path).map_err(|e| format!("Failed to get video duration: {}", e))?;
let duration_str = duration.to_string();

let file_name = Path::new(&file_path)
.file_name()
.and_then(|name| name.to_str())
Expand All @@ -24,14 +32,33 @@ pub async fn upload_file(

let server_url_base: &'static str = dotenv_codegen::dotenv!("NEXT_PUBLIC_URL");
let server_url = format!("{}/api/upload/signed", server_url_base);

let body: serde_json::Value;

if file_type == "video" {
let (codec_name, width, height, frame_rate, bit_rate) = log_video_info(&file_path).map_err(|e| format!("Failed to log video info: {}", e))?;

body = serde_json::json!({
"userId": options.user_id,
"fileKey": file_key,
"awsBucket": options.aws_bucket,
"awsRegion": options.aws_region,
"duration": duration_str,
"resolution": format!("{}x{}", width, height),
"framerate": frame_rate,
"bandwidth": bit_rate,
"videoCodec": codec_name,
});
} else {

// Create the request body for the Next.js handler
let body = serde_json::json!({
"userId": options.user_id,
"fileKey": file_key,
"awsBucket": options.aws_bucket,
"awsRegion": options.aws_region,
});
body = serde_json::json!({
"userId": options.user_id,
"fileKey": file_key,
"awsBucket": options.aws_bucket,
"awsRegion": options.aws_region,
"duration": duration_str,
});
}

let client = reqwest::Client::new();
let server_response = client.post(server_url)
Expand Down Expand Up @@ -117,4 +144,60 @@ pub async fn upload_file(
} else {
return Err("No recording options provided".to_string());
}
}

pub fn get_video_duration(file_path: &str) -> Result<f64, std::io::Error> {
let ffmpeg_binary_path_str = ffmpeg_path_as_str().unwrap().to_owned();

let output = Command::new(ffmpeg_binary_path_str)
.arg("-i")
.arg(file_path)
.output()?;

let output_str = str::from_utf8(&output.stderr).unwrap();
let duration_regex = Regex::new(r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}").unwrap();
let caps = duration_regex.captures(output_str).unwrap();

let hours: f64 = caps.get(1).unwrap().as_str().parse().unwrap();
let minutes: f64 = caps.get(2).unwrap().as_str().parse().unwrap();
let seconds: f64 = caps.get(3).unwrap().as_str().parse().unwrap();

let duration = hours * 3600.0 + minutes * 60.0 + seconds;

Ok(duration)
}

fn log_video_info(file_path: &str) -> Result<(String, String, String, String, String), String> {
let output: Output = Command::new("ffprobe")
.arg("-v")
.arg("error")
.arg("-show_entries")
.arg("stream=bit_rate,codec_name,height,width,r_frame_rate")
.arg("-of")
.arg("default=noprint_wrappers=1:nokey=1")
.arg(file_path)
.output()
.map_err(|e| format!("Failed to run ffprobe: {}", e))?;

if !output.status.success() {
return Err(format!(
"ffprobe exited with non-zero status: {}",
String::from_utf8_lossy(&output.stderr)
));
}

let info = String::from_utf8_lossy(&output.stdout);
let info_parts: Vec<&str> = info.split('\n').collect();
let codec_name = info_parts[0].to_string();
let width: String = info_parts[1].to_string();
let height: String = info_parts[2].to_string();

// Parse frame rate as a fraction and convert to float
let frame_rate_parts: Vec<&str> = info_parts[3].split('/').collect();
let frame_rate: f64 = frame_rate_parts[0].parse::<f64>().unwrap() / frame_rate_parts[1].parse::<f64>().unwrap();
let frame_rate: String = frame_rate.to_string();

let bit_rate: String = info_parts[4].to_string();

Ok((codec_name, width, height, frame_rate, bit_rate))
}
39 changes: 27 additions & 12 deletions apps/web/app/api/playlist/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,46 +153,61 @@ export async function GET(request: NextRequest) {
MaxKeys: 1,
});

let audioSegment;
const audioSegmentCommand = new ListObjectsV2Command({
Bucket: bucket,
Prefix: audioPrefix,
MaxKeys: 1,
});

const [videoSegment, audioSegment] = await Promise.all([
try {
audioSegment = await s3Client.send(audioSegmentCommand);
} catch (error) {
console.warn("No audio segment found for this video", error);
}

console.log("audioSegment", audioSegment);

const [videoSegment] = await Promise.all([
s3Client.send(videoSegmentCommand),
s3Client.send(audioSegmentCommand),
]);

const [videoMetadata, audioMetadata] = await Promise.all([
let audioMetadata;
const [videoMetadata] = await Promise.all([
s3Client.send(
new HeadObjectCommand({
Bucket: bucket,
Key: videoSegment.Contents?.[0]?.Key ?? "",
})
),
s3Client.send(
]);

if (audioSegment?.KeyCount && audioSegment?.KeyCount > 0) {
audioMetadata = await s3Client.send(
new HeadObjectCommand({
Bucket: bucket,
Key: audioSegment.Contents?.[0]?.Key ?? "",
})
),
]);
);
}

const generatedPlaylist = await generateMasterPlaylist(
videoMetadata?.Metadata?.resolution ?? "",
videoMetadata?.Metadata?.bandwidth ?? "",
process.env.NEXT_PUBLIC_URL +
"/api/playlist?userId=" +
userId +
"&videoId=" +
videoId +
"&videoType=video",
process.env.NEXT_PUBLIC_URL +
"/api/playlist?userId=" +
userId +
"&videoId=" +
videoId +
"&videoType=audio",
audioMetadata
? process.env.NEXT_PUBLIC_URL +
"/api/playlist?userId=" +
userId +
"&videoId=" +
videoId +
"&videoType=audio"
: null,
video.xStreamInfo ?? ""
);

Expand Down
Loading

1 comment on commit 87abd72

@vercel
Copy link

@vercel vercel bot commented on 87abd72 Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.