| 1 | //! Record Audio, Record Video and playback, stored as the desktop stores them |
| 2 | //! (`canvas::recording`): the host records with the platform's devices and hands over a PCM |
| 3 | //! WAV file, or a video's pictures and sound as `Movie` takes them; a recording plays from |
| 4 | //! the files `sb_view_play_request` writes. |
| 5 | |
| 6 | use super::*; |
| 7 | use canvas::recording::{self, video}; |
| 8 | use std::path::Path; |
| 9 | |
| 10 | impl Canvas { |
| 11 | /// Starts recording at the caret, its line saying when from the long `date` and short |
| 12 | /// `time`; whether it started. |
| 13 | pub(crate) fn start_recording(&mut self, video: bool, date: &str, time: &str) -> Result<bool> { |
| 14 | let label = recording::label(video, date, time); |
| 15 | Ok(self.page.start_recording(&label)?.0.is_some()) |
| 16 | } |
| 17 | |
| 18 | /// Puts the recording made, `recorded`, where it started: a PCM WAV file, or with |
| 19 | /// `video` an AVI file from `Movie`. None forgets the recording, whose file never came. |
| 20 | pub(crate) fn finish_recording( |
| 21 | &mut self, |
| 22 | recorded: Option<Vec<u8>>, |
| 23 | video: bool, |
| 24 | ) -> Result<bool> { |
| 25 | let editor = &mut self.page.editor; |
| 26 | let Some(id) = editor.recording() else { |
| 27 | return Ok(false); |
| 28 | }; |
| 29 | let Some(recorded) = recorded else { |
| 30 | editor.cancel_recording(); |
| 31 | return Ok(false); |
| 32 | }; |
| 33 | let file = recording::file(id, &editor.page()?.title, video, recorded)?; |
| 34 | Ok(moved(self.page.finish_recording(file)?)) |
| 35 | } |
| 36 | |
| 37 | /// The recording a tap asked to play, taken, with its files written to `folder` to play |
| 38 | /// from: see `sb_view_play_request`. |
| 39 | pub(crate) fn take_play(&mut self, folder: &Path) -> Result<Option<serde_json::Value>> { |
| 40 | let Some((file, at)) = self.play.take() else { |
| 41 | return Ok(None); |
| 42 | }; |
| 43 | let bytes = file.bytes.clone().ok_or("The recording has no data")?; |
| 44 | let stem = Path::new(&file.filename) |
| 45 | .file_stem() |
| 46 | .map_or_else(String::new, |stem| stem.to_string_lossy().into_owned()); |
| 47 | std::fs::create_dir_all(folder)?; |
| 48 | let sound = match recording::sound(&bytes) { |
| 49 | Some(wave) => { |
| 50 | let path = folder.join(format!("{stem}.wav")); |
| 51 | std::fs::write(&path, wave)?; |
| 52 | path |
| 53 | } |
| 54 | None => { |
| 55 | let path = folder.join(file.filename.replace('/', "_")); |
| 56 | std::fs::write(&path, &bytes)?; |
| 57 | path |
| 58 | } |
| 59 | }; |
| 60 | let movie = video::Movie::parse(&bytes) |
| 61 | .map(|movie| -> Result<_> { |
| 62 | let path = folder.join(format!("{stem}.avi")); |
| 63 | std::fs::write(&path, &bytes)?; |
| 64 | // An empty frame repeats the picture before it. |
| 65 | let mut shown = [0; 2]; |
| 66 | let frames: Vec<[usize; 2]> = movie |
| 67 | .frames |
| 68 | .iter() |
| 69 | .map(|range| { |
| 70 | if !range.is_empty() { |
| 71 | shown = [range.start, range.end]; |
| 72 | } |
| 73 | shown |
| 74 | }) |
| 75 | .collect(); |
| 76 | Ok(serde_json::json!({ |
| 77 | "path": path, |
| 78 | "frames": frames, |
| 79 | "frame_us": movie.frame_us, |
| 80 | "size": video::SIZE, |
| 81 | })) |
| 82 | }) |
| 83 | .transpose()?; |
| 84 | self.playing = file.recording.map(|recording| recording.id); |
| 85 | Ok(Some(serde_json::json!({ |
| 86 | "name": stem, |
| 87 | "sound": sound, |
| 88 | "video": movie, |
| 89 | "at_ms": at, |
| 90 | }))) |
| 91 | } |
| 92 | |
| 93 | /// See Playback: highlights the note linked last at or before `at_ms` into the recording |
| 94 | /// playing, or with none ends playback. |
| 95 | pub(crate) fn played(&mut self, at_ms: Option<u32>) -> Result<bool> { |
| 96 | if at_ms.is_none() { |
| 97 | self.playing = None; |
| 98 | } |
| 99 | let note = self |
| 100 | .playing |
| 101 | .zip(at_ms) |
| 102 | .and_then(|(id, at)| self.page.editor.played_note(id, at)) |
| 103 | .and_then(|note| canvas::search::paragraph_match(&self.page.editor, note)); |
| 104 | Ok(moved(self.page.set_played(note)?)) |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /// Starts recording audio, or with `video` video, at the caret, its line saying when from |
| 109 | /// the long `date` and short `time`; whether it started. |
| 110 | /// |
| 111 | /// # Safety |
| 112 | /// `date` and `time` are NUL-terminated UTF-8. |
| 113 | #[unsafe(no_mangle)] |
| 114 | pub unsafe extern "C" fn sb_view_start_recording( |
| 115 | view: &mut View, |
| 116 | video: bool, |
| 117 | date: *const c_char, |
| 118 | time: *const c_char, |
| 119 | ) -> bool { |
| 120 | if view.read_only || view.reading.is_some() { |
| 121 | return false; |
| 122 | } |
| 123 | let started = view |
| 124 | .canvas |
| 125 | .start_recording(video, &string(date), &string(time)); |
| 126 | let started = report(started).unwrap_or(false); |
| 127 | view.stored(Ok(started)); |
| 128 | started |
| 129 | } |
| 130 | |
| 131 | /// How far the recording under way has got in milliseconds, its pauses left out, or -1; |
| 132 | /// `paused` gets whether it is paused. |
| 133 | #[unsafe(no_mangle)] |
| 134 | pub extern "C" fn sb_view_recording(view: &View, paused: &mut bool) -> i64 { |
| 135 | let editor = &view.canvas.page.editor; |
| 136 | *paused = editor.recording_paused(); |
| 137 | editor.recording_ms().map_or(-1, i64::from) |
| 138 | } |
| 139 | |
| 140 | /// Pauses or resumes the recording: nothing written while it is paused links to it. |
| 141 | #[unsafe(no_mangle)] |
| 142 | pub extern "C" fn sb_view_pause_recording(view: &mut View, paused: bool) { |
| 143 | view.canvas.page.editor.pause_recording(paused); |
| 144 | } |
| 145 | |
| 146 | /// Stops recording: the file recorded, `length` bytes of mono 16-bit PCM WAV or with |
| 147 | /// `video` the AVI file `sb_movie_finish` made, goes where the recording started. Null |
| 148 | /// `bytes` forgets the recording. |
| 149 | /// |
| 150 | /// # Safety |
| 151 | /// `bytes` is null or holds `length` bytes. |
| 152 | #[unsafe(no_mangle)] |
| 153 | pub unsafe extern "C" fn sb_view_finish_recording( |
| 154 | view: &mut View, |
| 155 | bytes: *const u8, |
| 156 | length: usize, |
| 157 | video: bool, |
| 158 | ) -> bool { |
| 159 | // SAFETY: the caller's contract. |
| 160 | let recorded = (!bytes.is_null()).then(|| unsafe { std::slice::from_raw_parts(bytes, length) }); |
| 161 | let result = view |
| 162 | .canvas |
| 163 | .finish_recording(recorded.map(<[u8]>::to_vec), video); |
| 164 | view.stored(result) |
| 165 | } |
| 166 | |
| 167 | /// The recording the last tap asked to play, taken, as JSON: its `name`, the `sound` file |
| 168 | /// written to `folder` that the platform's player plays, from `at_ms`; and for a video its |
| 169 | /// `video` file `path`, the byte range of the JPEG picture each of its `frames` shows, |
| 170 | /// `frame_us` apart, and their `size`. Null when none was asked for. |
| 171 | /// |
| 172 | /// # Safety |
| 173 | /// `folder` is NUL-terminated UTF-8. |
| 174 | #[unsafe(no_mangle)] |
| 175 | pub unsafe extern "C" fn sb_view_play_request( |
| 176 | view: &mut View, |
| 177 | folder: *const c_char, |
| 178 | ) -> *mut c_char { |
| 179 | let request = view.canvas.take_play(Path::new(&string(folder))); |
| 180 | report(request) |
| 181 | .flatten() |
| 182 | .map_or(std::ptr::null_mut(), |json| owned(json.to_string())) |
| 183 | } |
| 184 | |
| 185 | /// The recording last asked to play is at `at_ms`, whose linked note the page highlights; |
| 186 | /// -1 ends playback. Whether the page changed. |
| 187 | #[unsafe(no_mangle)] |
| 188 | pub extern "C" fn sb_view_played(view: &mut View, at_ms: i64) -> bool { |
| 189 | let at = u32::try_from(at_ms).ok(); |
| 190 | report(view.canvas.played(at)).unwrap_or(false) |
| 191 | } |
| 192 | |
| 193 | /// The rate the host records sound at, as mono 16-bit PCM: the desktop's. |
| 194 | #[unsafe(no_mangle)] |
| 195 | pub extern "C" fn sb_recording_rate() -> u32 { |
| 196 | recording::RATE |
| 197 | } |
| 198 | |
| 199 | /// A video recording's pictures and sound as the camera gave them, which become the AVI |
| 200 | /// file the desktop records. |
| 201 | #[derive(Default)] |
| 202 | pub struct Movie { |
| 203 | pictures: video::Pictures, |
| 204 | sound: Vec<u8>, |
| 205 | } |
| 206 | |
| 207 | #[unsafe(no_mangle)] |
| 208 | pub extern "C" fn sb_movie_new() -> *mut Movie { |
| 209 | Box::into_raw(Box::default()) |
| 210 | } |
| 211 | |
| 212 | /// The picture taken `at_us` microseconds in: `height` rows of `stride` bytes of BGRA. |
| 213 | /// |
| 214 | /// # Safety |
| 215 | /// `bgra` holds `height` × `stride` bytes. |
| 216 | #[unsafe(no_mangle)] |
| 217 | pub unsafe extern "C" fn sb_movie_picture( |
| 218 | movie: &mut Movie, |
| 219 | at_us: u64, |
| 220 | bgra: *const u8, |
| 221 | width: u32, |
| 222 | height: u32, |
| 223 | stride: usize, |
| 224 | ) { |
| 225 | // SAFETY: the caller's contract. |
| 226 | let bgra = unsafe { std::slice::from_raw_parts(bgra, height as usize * stride) }; |
| 227 | movie.pictures.take_bgra( |
| 228 | at_us, |
| 229 | [width, height].map(|side| side as usize), |
| 230 | stride, |
| 231 | bgra, |
| 232 | ); |
| 233 | } |
| 234 | |
| 235 | /// More of the sound, mono 16-bit little-endian PCM at `canvas::recording::RATE`. |
| 236 | /// |
| 237 | /// # Safety |
| 238 | /// `pcm` holds `length` bytes. |
| 239 | #[unsafe(no_mangle)] |
| 240 | pub unsafe extern "C" fn sb_movie_sound(movie: &mut Movie, pcm: *const u8, length: usize) { |
| 241 | // SAFETY: the caller's contract. |
| 242 | movie |
| 243 | .sound |
| 244 | .extend_from_slice(unsafe { std::slice::from_raw_parts(pcm, length) }); |
| 245 | } |
| 246 | |
| 247 | /// The AVI file of a movie `duration_us` long, `length` bytes freed with `sb_bytes_free`, |
| 248 | /// or null when it has no pictures or passes 4 GiB. |
| 249 | /// |
| 250 | /// # Safety |
| 251 | /// `movie` came from `sb_movie_new` and is not used again. |
| 252 | #[unsafe(no_mangle)] |
| 253 | pub unsafe extern "C" fn sb_movie_finish( |
| 254 | movie: *mut Movie, |
| 255 | duration_us: u64, |
| 256 | length: &mut usize, |
| 257 | ) -> *mut u8 { |
| 258 | // SAFETY: the caller's contract. |
| 259 | let movie = unsafe { Box::from_raw(movie) }; |
| 260 | let frames = movie.pictures.finish(duration_us); |
| 261 | if frames.is_empty() { |
| 262 | return std::ptr::null_mut(); |
| 263 | } |
| 264 | let sound = video::Pcm { |
| 265 | rate: recording::RATE, |
| 266 | channels: 1, |
| 267 | data: movie.sound, |
| 268 | }; |
| 269 | let Some(avi) = video::write(video::SIZE, &frames, &sound) else { |
| 270 | return std::ptr::null_mut(); |
| 271 | }; |
| 272 | *length = avi.len(); |
| 273 | Box::into_raw(avi.into_boxed_slice()).cast() |
| 274 | } |