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
6use super::*;
7use canvas::recording::{self, video};
8use std::path::Path;
9
10impl 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)]
114pub 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)]
134pub 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)]
142pub 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)]
153pub 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)]
175pub 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)]
188pub 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)]
195pub 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)]
202pub struct Movie {
203 pictures: video::Pictures,
204 sound: Vec<u8>,
205}
206
207#[unsafe(no_mangle)]
208pub 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)]
217pub 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)]
240pub 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)]
253pub 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}