Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions crates/chatd/src/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ fn new_call(conversation: Key, state: CallState) -> Live {
security: None,
picture: None,
self_view: None,
share_view: None,
};
Live {
intent: NEXT_INTENT.fetch_add(1, Ordering::Relaxed),
Expand Down Expand Up @@ -415,12 +416,14 @@ impl Live {
}
self.call.picture = picture;
}
MediaResponse::SelfView { path, width, height, generation } => {
let picture = (!path.is_empty()).then(|| CallPicture { path, width, height, generation, user_id: String::new(), kind: PictureKind::Camera });
if self.call.self_view == picture {
MediaResponse::SelfView { path, width, height, generation, share } => {
let kind = if share { PictureKind::Share } else { PictureKind::Camera };
let picture = (!path.is_empty()).then(|| CallPicture { path, width, height, generation, user_id: String::new(), kind });
let slot = if share { &mut self.call.share_view } else { &mut self.call.self_view };
if *slot == picture {
return false;
}
self.call.self_view = picture;
*slot = picture;
}
MediaResponse::Sending { share, camera } => {
let mut video: Vec<VideoStream> = self.call.video.iter().filter(|stream| stream.direction != Direction::Encode).cloned().collect();
Expand All @@ -430,7 +433,9 @@ impl Live {
if share {
video.insert(0, VideoStream { direction: Direction::Encode, content: Content::Share, source: String::new() });
}
if video == self.call.video {
// My preview of a share that is no longer sent goes with it.
let dropped = !share && self.call.share_view.take().is_some();
if video == self.call.video && !dropped {
return false;
}
self.call.video = video;
Expand Down Expand Up @@ -1741,6 +1746,34 @@ mod tests {
}
}

#[test]
fn my_camera_and_my_share_keep_their_own_self_views() {
let view = |path: &str, generation: u64, share: bool| MediaResponse::SelfView { path: path.into(), width: 640, height: 360, generation, share };
let mut live = live(CallState::Joined);
assert!(live.apply(view("camera.jpg", 1, false)));
assert!(live.apply(view("share.jpg", 1, true)));
let camera = live.call.self_view.clone().unwrap();
let share = live.call.share_view.clone().unwrap();
assert_eq!((camera.path.as_str(), camera.kind), ("camera.jpg", PictureKind::Camera));
assert_eq!((share.path.as_str(), share.kind), ("share.jpg", PictureKind::Share));
assert!(!live.apply(view("share.jpg", 1, true)), "the same picture is no news");

// The share going off takes only its own view.
assert!(live.apply(view("", 0, true)));
assert!(live.call.share_view.is_none());
assert!(live.call.self_view.is_some(), "the camera is still on");
}

#[test]
fn a_share_that_is_no_longer_sent_takes_its_view_with_it() {
let mut live = live(CallState::Joined);
assert!(live.apply(MediaResponse::SelfView { path: "share.jpg".into(), width: 640, height: 360, generation: 1, share: true }));
live.apply(MediaResponse::Sending { share: true, camera: false });
assert!(live.call.share_view.is_some(), "still sending");
assert!(live.apply(MediaResponse::Sending { share: false, camera: false }));
assert!(live.call.share_view.is_none());
}

#[test]
fn only_the_voice_connection_being_up_makes_a_call_joined() {
let mut live = live(CallState::Connecting);
Expand Down
7 changes: 4 additions & 3 deletions crates/mediad/src/discord_video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,16 @@ impl Drive {
match word {
Word::Frame { payload, rtp, .. } => self.send_frame(dave, socket, rtp as u32, &payload),
Word::Failed { words, .. } => out::send(&MediaResponse::Notice { detail: words }),
// The camera thread paints my own tile. A screen leg has no camera, so it sends none.
Word::SelfView(painted) => out::send(&match painted {
// The capture thread paints my own tile: my screen, from a screen leg.
Word::SelfView { share, painted } => out::send(&match painted {
Some(painted) => MediaResponse::SelfView {
path: painted.path.display().to_string(),
width: painted.width,
height: painted.height,
generation: painted.generation,
share,
},
None => MediaResponse::SelfView { path: String::new(), width: 0, height: 0, generation: 0 },
None => MediaResponse::SelfView { path: String::new(), width: 0, height: 0, generation: 0, share },
}),
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/mediad/src/picture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ impl Canvas {
}
}

/// A call that ends while a picture is on (the capture thread told to stop, with nothing turned
/// off first) leaves no picture of a screen or a face behind in the temp folder.
impl Drop for Canvas {
fn drop(&mut self) {
self.forget();
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
117 changes: 92 additions & 25 deletions crates/mediad/src/picture/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,47 @@ pub struct Outgoing {
camera: Camera,
cost: Cost,
camera_cost: GrabCost,
/// My own camera, painted for me (`selfie`).
selfie_canvas: Canvas,
selfie_at: Option<Instant>,
selfie_generation: u64,
selfie_painted: Option<Painted>,
/// My own camera, painted for me.
selfie: Selfie,
/// My own share, painted for me. Apart from the camera's: both can be on at once.
screen_selfie: Selfie,
}

/// One of my own pictures shrunk and written as a JPEG a few times a second, for my own tile.
#[derive(Default)]
struct Selfie {
canvas: Canvas,
at: Option<Instant>,
generation: u64,
painted: Option<Painted>,
}

impl Selfie {
/// Paints this grab, unless the last was painted less than [`super::SELF_VIEW_EVERY`] ago.
fn shoot(&mut self, grabbed: &Rgba) {
if self.at.is_some_and(|at| at.elapsed() < super::SELF_VIEW_EVERY) {
return;
}
self.at = Some(Instant::now());
let (width, height) = fit(grabbed.width, grabbed.height, super::SELF_VIEW.0, super::SELF_VIEW.1);
let rgb = super::screens::shrink(grabbed, width, height);
self.generation = self.generation.wrapping_add(1);
if let Some(painted) = self.canvas.paint(&rgb, width, height, self.generation) {
self.painted = Some(painted);
}
}

/// The last picture painted and not yet taken.
fn take(&mut self) -> Option<Painted> {
self.painted.take()
}

/// The picture's file goes, and the next shot is due at once.
fn forget(&mut self) {
self.canvas.forget();
self.painted = None;
self.at = None;
}
}

/// What a camera frame costs to read, in the log once a second or so, with the rate the camera
Expand Down Expand Up @@ -209,29 +245,22 @@ impl Outgoing {
let (width, height) = fit(grabbed.width, grabbed.height, quality.max.0, quality.max.1);
let planes = i420(&grabbed.pixels, grabbed.width, grabbed.height, width, height, grabbed.bgra);
let converted = Instant::now();
// The grab is the whole screen, so the preview costs one shrink a tenth of a second.
self.screen_selfie.shoot(&grabbed);
let encoded = coded(&mut self.share, width, height, quality.bps, quality.fps as f32, true)?.encode(&planes, &ask);
self.cost.note(began, captured, converted, Instant::now(), (grabbed.width, grabbed.height), (width, height));
encoded
}

/// My own camera for my own tile, a few times a second: the grab shrunk and written as a
/// JPEG, taken by [`Outgoing::self_view`].
fn selfie(&mut self, grabbed: &Rgba) {
if self.selfie_at.is_some_and(|at| at.elapsed() < super::SELF_VIEW_EVERY) {
return;
}
self.selfie_at = Some(Instant::now());
let (width, height) = fit(grabbed.width, grabbed.height, super::SELF_VIEW.0, super::SELF_VIEW.1);
let rgb = super::screens::shrink(grabbed, width, height);
self.selfie_generation = self.selfie_generation.wrapping_add(1);
if let Some(painted) = self.selfie_canvas.paint(&rgb, width, height, self.selfie_generation) {
self.selfie_painted = Some(painted);
}
/// My own camera as painted for me since last asked: a few times a second, the camera's
/// grab shrunk and written as a JPEG.
pub fn self_view(&mut self) -> Option<Painted> {
self.selfie.take()
}

/// The last self view painted and not yet taken.
pub fn self_view(&mut self) -> Option<Painted> {
self.selfie_painted.take()
/// My own share as painted for me since last asked, on the same terms as [`Outgoing::self_view`].
pub fn share_view(&mut self) -> Option<Painted> {
self.screen_selfie.take()
}

/// The camera's two layers, high then low, or why the camera could not be read. A layer with
Expand All @@ -243,7 +272,7 @@ impl Outgoing {
let began = Instant::now();
let grabbed = self.camera.frame()?;
self.camera_cost.note(began.elapsed(), (grabbed.width, grabbed.height));
self.selfie(&grabbed);
self.selfie.shoot(&grabbed);
let layer = |slot: &mut Option<Coded>, ask: Option<Ask>, most: (u32, u32), nominal: u32| match ask {
Some(ask) => {
let (width, height) = fit(grabbed.width, grabbed.height, most.0, most.1);
Expand All @@ -259,14 +288,13 @@ impl Outgoing {

pub fn close_camera(&mut self) {
self.camera.close();
self.selfie_canvas.forget();
self.selfie_painted = None;
self.selfie_at = None;
self.selfie.forget();
}

/// The share is off. Drop the encoder, and on Linux close the portal session so the recording indicator does not stay up.
pub fn close_share(&mut self) {
self.share = None;
self.screen_selfie.forget();
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
super::portal::stop_screen();
}
Expand Down Expand Up @@ -939,6 +967,45 @@ pub fn probe_camera() -> Result<(), String> {
mod tests {
use super::*;

fn screen() -> Rgba {
Rgba { width: 1920, height: 1080, pixels: vec![200; 1920 * 1080 * 4], bgra: true }
}

#[test]
fn a_self_view_is_shrunk_to_the_corner_size_painted_at_a_steady_rate_and_let_go() {
let mut outgoing = Outgoing::default();
outgoing.screen_selfie.shoot(&screen());
let first = outgoing.share_view().expect("the first grab is painted at once");
assert_eq!((first.width, first.height), super::super::SELF_VIEW);
assert!(first.path.exists());
assert!(outgoing.share_view().is_none(), "taken once");
assert!(outgoing.self_view().is_none(), "the camera's slot is a different one");

outgoing.screen_selfie.shoot(&screen());
assert!(outgoing.share_view().is_none(), "a grab inside the interval is not painted");

outgoing.screen_selfie.at = None;
outgoing.screen_selfie.shoot(&screen());
let next = outgoing.share_view().unwrap();
assert_ne!(next.generation, first.generation);
assert!(!first.path.exists(), "the file before it is gone");

outgoing.screen_selfie.forget();
assert!(!next.path.exists(), "and so is the last one when the share stops");
}

#[test]
fn a_call_that_ends_while_sharing_leaves_no_picture_of_the_screen_behind() {
let mut outgoing = Outgoing::default();
outgoing.screen_selfie.shoot(&screen());
outgoing.selfie.shoot(&screen());
let files = [outgoing.share_view().unwrap().path, outgoing.self_view().unwrap().path];
assert!(files.iter().all(|file| file.exists()));
// `Video::stop` ends the capture thread without turning the share off first.
drop(outgoing);
assert!(files.iter().all(|file| !file.exists()), "{files:?}");
}

#[test]
fn openh264_is_fetched_from_cisco() {
let name = library_name().unwrap_or("openh264-2.6.0-win64.dll");
Expand Down
10 changes: 10 additions & 0 deletions crates/mediad/src/picture/phone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ impl Outgoing {
Ok((high, low))
}

/// A phone paints no preview of its own camera or share for its call screen, so `Source` has
/// nothing to take from it.
pub fn self_view(&mut self) -> Option<Painted> {
None
}

pub fn share_view(&mut self) -> Option<Painted> {
None
}

/// The button is off: a phone's camera light goes off with it.
pub fn close_camera(&mut self) {
self.camera = None;
Expand Down
6 changes: 3 additions & 3 deletions crates/mediad/src/tiny.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,9 +1047,9 @@ impl Call {
out::send(&MediaResponse::Sending { share: false, camera: self.sending_camera });
}
}
Word::SelfView(painted) => out::send(&match painted {
Some(painted) => MediaResponse::SelfView { path: painted.path.display().to_string(), width: painted.width, height: painted.height, generation: painted.generation },
None => MediaResponse::SelfView { path: String::new(), width: 0, height: 0, generation: 0 },
Word::SelfView { share, painted } => out::send(&match painted {
Some(painted) => MediaResponse::SelfView { path: painted.path.display().to_string(), width: painted.width, height: painted.height, generation: painted.generation, share },
None => MediaResponse::SelfView { path: String::new(), width: 0, height: 0, generation: 0, share },
}),
Word::Failed { share: false, words } => {
if !self.camera_noted {
Expand Down
Loading
Loading