Skip to content

Commit 0cf13bd

Browse files
Merge pull request #67 from CapSoftware/brendonovich/cap-18-desktopweb-app-on-stop-recording-immediately-open-share-url
Build HLS stream on client and serve directly from s3
2 parents 31bdf79 + 4cdafd7 commit 0cf13bd

File tree

20 files changed

+1185
-745
lines changed

20 files changed

+1185
-745
lines changed

apps/desktop/src-tauri/src/main.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use tauri::{
1010
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu,
1111
};
1212
use tauri_plugin_positioner::{Position, WindowExt};
13-
use tokio::sync::Mutex;
13+
use tokio::sync::{oneshot, Mutex};
1414
use tracing::Level;
1515
use tracing_subscriber::prelude::*;
1616
use window_shadows::set_shadow;
@@ -201,13 +201,10 @@ fn main() {
201201
.path_resolver()
202202
.app_data_dir()
203203
.unwrap_or_else(|| PathBuf::new());
204+
204205
let recording_state = RecordingState {
205-
media_process: None,
206-
recording_options: None,
207-
shutdown_flag: Arc::new(AtomicBool::new(false)),
208-
video_uploading_finished: Arc::new(AtomicBool::new(false)),
209-
audio_uploading_finished: Arc::new(AtomicBool::new(false)),
210-
data_dir: Some(data_directory),
206+
active_recording: None,
207+
data_dir: data_directory,
211208
max_screen_width: max_width as usize,
212209
max_screen_height: max_height as usize,
213210
};

apps/desktop/src-tauri/src/media/mod.rs

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,7 @@ pub struct MediaRecorder {
6565
ffmpeg_stdin: Option<ChildStdin>,
6666
device_name: Option<String>,
6767
start_time: Option<Instant>,
68-
audio_file_path: Option<PathBuf>,
69-
video_file_path: Option<PathBuf>,
68+
chunks_dir: PathBuf,
7069
audio_pipe_task: Option<JoinHandle<()>>,
7170
video_pipe_task: Option<JoinHandle<()>>,
7271
}
@@ -81,8 +80,7 @@ impl MediaRecorder {
8180
&mut self,
8281
options: RecordingOptions,
8382
screenshot_dir: &Path,
84-
audio_chunks_dir: &Path,
85-
video_chunks_dir: &Path,
83+
recording_dir: &Path,
8684
custom_device: Option<&str>,
8785
max_screen_width: usize,
8886
max_screen_height: usize,
@@ -126,17 +124,15 @@ impl MediaRecorder {
126124
video_capturer.start(video_start_time.clone(), screenshot_dir, options_clone);
127125

128126
tracing::info!("Starting audio recording and processing...");
129-
let audio_chunk_pattern = audio_chunks_dir.join("audio_recording_%03d.aac");
130-
let video_chunk_pattern = video_chunks_dir.join("video_recording_%03d.ts");
131-
let audio_segment_list_filename = audio_chunks_dir.join("segment_list.txt");
132-
let video_segment_list_filename = video_chunks_dir.join("segment_list.txt");
127+
let segment_pattern_path = recording_dir.join("segment_%03d.ts");
128+
let playlist_path = recording_dir.join("stream.m3u8");
133129

134-
let video_pipe_path = video_chunks_dir.join("pipe.pipe");
130+
let video_pipe_path = recording_dir.join("video.pipe");
135131

136132
std::fs::remove_file(&video_pipe_path).ok();
137133
create_named_pipe(&video_pipe_path).map_err(|e| e.to_string())?;
138134

139-
let audio_pipe_path = audio_chunks_dir.join("pipe.pipe");
135+
let audio_pipe_path = recording_dir.join("audio.pipe");
140136

141137
std::fs::remove_file(&audio_pipe_path).ok();
142138
create_named_pipe(&audio_pipe_path).map_err(|e| e.to_string())?;
@@ -167,21 +163,7 @@ impl MediaRecorder {
167163
.args(["-f", "rawvideo", "-pix_fmt", "bgra"])
168164
.args(["-s", &size, "-r", &fps])
169165
.args(["-thread_queue_size", "4096", "-i"])
170-
.arg(&video_pipe_path)
171-
// video out
172-
.args([
173-
"-vf",
174-
&format!("fps={fps},scale=in_range=full:out_range=limited"),
175-
])
176-
.args(["-c:v", "libx264", "-preset", "ultrafast"])
177-
.args(["-pix_fmt", "yuv420p", "-tune", "zerolatency"])
178-
.args(["-vsync", "1", "-force_key_frames", "expr:gte(t,n_forced*3)"])
179-
.args(["-f", "segment", "-movflags", "frag_keyframe+empty_moov"])
180-
.args(["-reset_timestamps", "1", "-an"])
181-
.args(["-segment_time", "3"])
182-
.args(["-segment_format", "ts"])
183-
.args(["-segment_time_delta", "0.01", "-segment_list"])
184-
.args([&video_segment_list_filename, &video_chunk_pattern]);
166+
.arg(&video_pipe_path);
185167

186168
if self.audio_enabled {
187169
if let Some((TimeOffsetTarget::Audio, args)) = &time_offset {
@@ -197,19 +179,46 @@ impl MediaRecorder {
197179
// audio in
198180
.args(["-f", sample_format, "-ar", &sample_rate_str])
199181
.args(["-ac", &channels_str, "-thread_queue_size", "4096", "-i"])
200-
.arg(&audio_pipe_path)
201-
// out
182+
.arg(&audio_pipe_path);
183+
// out
184+
// .args(["-f", "hls", "-async", "1"])
185+
// .args(["-segment_time", "3", "-segment_time_delta", "0.01"])
186+
// .args(["-reset_timestamps", "1", "-vn", "-segment_list"])
187+
// .args([&audio_segment_list_filename, &audio_chunk_pattern]);
188+
}
189+
190+
ffmpeg_command
191+
.args(["-f", "hls"])
192+
.args(["-hls_time", "5", "-hls_playlist_type", "vod"])
193+
.args(["-hls_flags", "independent_segments"])
194+
.args(["-master_pl_name", "master.m3u8"])
195+
.args(["-hls_segment_type", "mpegts"])
196+
.arg("-hls_segment_filename")
197+
.arg(&segment_pattern_path)
198+
// video
199+
.args(["-codec:v", "libx264", "-preset", "ultrafast"])
200+
.args(["-pix_fmt", "yuv420p", "-tune", "zerolatency"])
201+
.args(["-vsync", "1", "-force_key_frames", "expr:gte(t,n_forced*3)"])
202+
.args(["-movflags", "frag_keyframe+empty_moov"])
203+
.args([
204+
"-vf",
205+
&format!("fps={fps},scale=in_range=full:out_range=limited"),
206+
]);
207+
208+
if self.audio_enabled {
209+
ffmpeg_command
210+
// audio
211+
.args(["-codec:a", "aac", "-b:a", "128k", "-async", "1"])
202212
.args([
203213
"-af",
204214
"aresample=async=1:min_hard_comp=0.100000:first_pts=0",
205-
])
206-
.args(["-codec:a", "aac", "-b:a", "128k"])
207-
.args(["-async", "1", "-f", "segment"])
208-
.args(["-segment_time", "3", "-segment_time_delta", "0.01"])
209-
.args(["-reset_timestamps", "1", "-vn", "-segment_list"])
210-
.args([&audio_segment_list_filename, &audio_chunk_pattern]);
215+
]);
216+
} else {
217+
ffmpeg_command.args(["-an"]);
211218
}
212219

220+
ffmpeg_command.arg(&playlist_path);
221+
213222
tracing::trace!("Starting FFmpeg process...");
214223

215224
let (ffmpeg_child, ffmpeg_stdin) = self
@@ -226,8 +235,7 @@ impl MediaRecorder {
226235
self.video_pipe_task = Some(tokio::spawn(video_capturer.collect_frames(video_pipe_path)));
227236

228237
self.start_time = Some(Instant::now());
229-
self.audio_file_path = Some(audio_chunks_dir.to_path_buf());
230-
self.video_file_path = Some(video_chunks_dir.to_path_buf());
238+
self.chunks_dir = recording_dir.to_path_buf();
231239
self.ffmpeg_process = Some(ffmpeg_child);
232240
self.ffmpeg_stdin = Some(ffmpeg_stdin);
233241
self.device_name = self.audio_capturer.as_ref().map(|c| c.device_name.clone());

apps/desktop/src-tauri/src/media/video.rs

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
use image::codecs::jpeg::JpegEncoder;
2-
use image::{ ImageBuffer, Rgba };
3-
use scap::{ capturer::{ Capturer, Options, Resolution }, frame::{ Frame, FrameType } };
4-
use std::{ future::Future, path::{ Path, PathBuf }, sync::Arc, time::Duration };
2+
use image::{ImageBuffer, Rgba};
3+
use scap::{
4+
capturer::{Capturer, Options, Resolution},
5+
frame::{Frame, FrameType},
6+
};
7+
use std::{
8+
future::Future,
9+
path::{Path, PathBuf},
10+
sync::Arc,
11+
time::Duration,
12+
};
513
use tokio::sync::mpsc::error::TrySendError;
6-
use tokio::{ fs::File, io::AsyncWriteExt, sync::mpsc };
14+
use tokio::{fs::File, io::AsyncWriteExt, sync::mpsc};
715

8-
use super::{ Instant, RecordingOptions, SharedFlag, SharedInstant };
16+
use super::{Instant, RecordingOptions, SharedFlag, SharedInstant};
917
use crate::app::config;
10-
use crate::upload::{ upload_file, FileType };
18+
use crate::upload::{upload_recording_asset, RecordingAssetType};
1119

1220
pub struct VideoCapturer {
1321
capturer: Option<Capturer>,
@@ -47,9 +55,10 @@ impl VideoCapturer {
4755
&mut self,
4856
start_time: SharedInstant,
4957
screenshot_dir: impl AsRef<Path>,
50-
recording_options: RecordingOptions
58+
recording_options: RecordingOptions,
5159
) {
52-
let mut capturer = self.capturer
60+
let mut capturer = self
61+
.capturer
5362
.take()
5463
.expect("Video capturing thread has already been started!");
5564
let (sender, receiver) = mpsc::channel(2048);
@@ -81,22 +90,19 @@ impl VideoCapturer {
8190
let width = frame.width;
8291
let height = frame.height;
8392
let frame_data = match frame.width == 0 && frame.height == 0 {
84-
true =>
85-
match last_frame.take() {
86-
Some(data) => data,
87-
None => {
88-
tracing::error!(
89-
"Somehow got an idle frame before any complete frame"
90-
);
91-
continue;
92-
}
93+
true => match last_frame.take() {
94+
Some(data) => data,
95+
None => {
96+
tracing::error!(
97+
"Somehow got an idle frame before any complete frame"
98+
);
99+
continue;
93100
}
101+
},
94102
false => Arc::new(frame.data),
95103
};
96104

97-
if
98-
now - capture_start_time >= take_screenshot_delay &&
99-
!screenshot_captured
105+
if now - capture_start_time >= take_screenshot_delay && !screenshot_captured
100106
{
101107
screenshot_captured = true;
102108
let mut frame_data_clone = (*frame_data).clone();
@@ -109,16 +115,14 @@ impl VideoCapturer {
109115
let image: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::from_raw(
110116
width.try_into().unwrap(),
111117
height.try_into().unwrap(),
112-
frame_data_clone
113-
).expect("Failed to create image buffer");
118+
frame_data_clone,
119+
)
120+
.expect("Failed to create image buffer");
114121

115-
let mut output_file = std::fs::File
116-
::create(&screenshot_path)
122+
let mut output_file = std::fs::File::create(&screenshot_path)
117123
.expect("Failed to create output file");
118-
let mut encoder = JpegEncoder::new_with_quality(
119-
&mut output_file,
120-
70
121-
);
124+
let mut encoder =
125+
JpegEncoder::new_with_quality(&mut output_file, 70);
122126

123127
if let Err(e) = encoder.encode_image(&image) {
124128
tracing::warn!("Failed to save screenshot: {}", e);
@@ -132,27 +136,20 @@ impl VideoCapturer {
132136
if !config::is_local_mode() {
133137
let rt = tokio::runtime::Runtime::new().unwrap();
134138
rt.block_on(async move {
135-
let upload_task = tokio::spawn(
136-
upload_file(
137-
Some(options_clone),
138-
screenshot_path.clone(),
139-
FileType::Screenshot
140-
)
141-
);
139+
let upload_task = tokio::spawn(upload_recording_asset(
140+
options_clone,
141+
screenshot_path.clone(),
142+
RecordingAssetType::ScreenCapture,
143+
));
142144
match upload_task.await {
143-
Ok(result) =>
144-
match result {
145-
Ok(_) =>
146-
tracing::info!(
147-
"Screenshot successfully uploaded"
148-
),
149-
Err(e) => {
150-
tracing::warn!(
151-
"Failed to upload file: {}",
152-
e
153-
)
154-
}
145+
Ok(result) => match result {
146+
Ok(_) => tracing::info!(
147+
"Screenshot successfully uploaded"
148+
),
149+
Err(e) => {
150+
tracing::warn!("Failed to upload file: {}", e)
155151
}
152+
},
156153
Err(e) => {
157154
tracing::error!("Failed to join task: {}", e)
158155
}
@@ -210,7 +207,8 @@ impl VideoCapturer {
210207

211208
pub fn collect_frames(&mut self, destination: PathBuf) -> impl Future<Output = ()> + 'static {
212209
tracing::trace!("Starting video channel senders...");
213-
let mut receiver = self.frame_receiver
210+
let mut receiver = self
211+
.frame_receiver
214212
.take()
215213
.expect("Video frame collection already started!");
216214
let should_stop = self.should_stop.clone();
@@ -219,7 +217,9 @@ impl VideoCapturer {
219217
let mut pipe = File::create(destination).await.unwrap();
220218

221219
while let Some(bytes) = receiver.recv().await {
222-
pipe.write_all(&bytes).await.expect("Failed to write video data to FFmpeg stdin");
220+
pipe.write_all(&bytes)
221+
.await
222+
.expect("Failed to write video data to FFmpeg stdin");
223223

224224
if should_stop.get() {
225225
receiver.close();

0 commit comments

Comments
 (0)