1use std::sync::Arc;
13use std::time::Duration;
14
15use gpui::prelude::*;
16use gpui::{
17 canvas, div, px, size, Bounds, ContentMask, Context, Corners, EventEmitter, Hsla, IntoElement,
18 RenderImage, SharedString, Window,
19};
20
21use super::{fit_rect, VideoFeed, VideoFit, VideoFrame};
22use crate::devtools::Probed;
23use crate::theme::{theme, Size};
24
25const POLL: Duration = Duration::from_millis(8);
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum VideoEvent {
32 Resized { width: u32, height: u32 },
33}
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct VideoStats {
38 pub shown: u64,
39 pub dropped: u64,
40}
41
42pub struct VideoView {
44 image: Option<Arc<RenderImage>>,
45 dims: Option<(u32, u32)>,
46 retired: Vec<Arc<RenderImage>>,
47 fit: VideoFit,
48 background: Option<Hsla>,
49 width: Option<f32>,
50 height: Option<f32>,
51 radius: Option<Size>,
52 placeholder: SharedString,
53 shown: u64,
54 feeds: Vec<VideoFeed>,
55}
56
57impl EventEmitter<VideoEvent> for VideoView {}
58
59impl VideoView {
60 pub fn new(_cx: &mut Context<Self>) -> Self {
61 VideoView {
62 image: None,
63 dims: None,
64 retired: Vec::new(),
65 fit: VideoFit::default(),
66 background: None,
67 width: None,
68 height: None,
69 radius: None,
70 placeholder: "No signal".into(),
71 shown: 0,
72 feeds: Vec::new(),
73 }
74 }
75
76 pub fn fit(mut self, fit: VideoFit) -> Self {
78 self.fit = fit;
79 self
80 }
81
82 pub fn background(mut self, color: impl Into<Hsla>) -> Self {
84 self.background = Some(color.into());
85 self
86 }
87
88 pub fn width(mut self, width: f32) -> Self {
90 self.width = Some(width);
91 self
92 }
93
94 pub fn height(mut self, height: f32) -> Self {
96 self.height = Some(height);
97 self
98 }
99
100 pub fn radius(mut self, radius: Size) -> Self {
102 self.radius = Some(radius);
103 self
104 }
105
106 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
108 self.placeholder = text.into();
109 self
110 }
111
112 pub fn push_frame(&mut self, frame: VideoFrame, cx: &mut Context<Self>) {
114 let dims = (frame.width(), frame.height());
115 let Some(image) = frame.into_image() else {
116 return;
117 };
118 if let Some(old) = self.image.replace(image) {
119 self.retired.push(old);
120 }
121 self.shown += 1;
122 if self.dims.replace(dims) != Some(dims) {
123 cx.emit(VideoEvent::Resized {
124 width: dims.0,
125 height: dims.1,
126 });
127 }
128 cx.notify();
129 }
130
131 pub fn clear(&mut self, cx: &mut Context<Self>) {
133 if let Some(old) = self.image.take() {
134 self.retired.push(old);
135 }
136 self.dims = None;
137 cx.notify();
138 }
139
140 pub fn frame_size(&self) -> Option<(u32, u32)> {
142 self.dims
143 }
144
145 pub fn stats(&self) -> VideoStats {
147 let sent: u64 = self.feeds.iter().map(VideoFeed::sent).sum();
148 VideoStats {
149 shown: self.shown,
150 dropped: sent.saturating_sub(self.shown),
151 }
152 }
153
154 pub fn feed(&mut self, cx: &mut Context<Self>) -> VideoFeed {
157 let feed = VideoFeed::new();
158 let poller = feed.clone();
159 self.feeds.push(feed.clone());
160 cx.spawn(async move |this, cx| loop {
161 cx.background_executor().timer(POLL).await;
162 if poller.orphaned() {
163 break;
164 }
165 let Some(frame) = poller.take() else {
166 continue;
167 };
168 if this
169 .update(cx, |view, cx| view.push_frame(frame, cx))
170 .is_err()
171 {
172 break;
173 }
174 })
175 .detach();
176 feed
177 }
178}
179
180impl Render for VideoView {
181 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
182 for old in self.retired.drain(..) {
183 let _ = window.drop_image(old);
184 }
185 let t = theme(cx);
186 let bg = self.background.unwrap_or_else(|| t.surface().hsla());
187 let dimmed = t.dimmed().hsla();
188 let radius = t.radius(self.radius.unwrap_or(t.default_radius));
189
190 let mut root = div()
191 .id("guise-videoview")
192 .flex()
193 .items_center()
194 .justify_center()
195 .overflow_hidden()
196 .bg(bg)
197 .rounded(px(radius));
198 root = match self.width {
199 Some(w) => root.w(px(w)),
200 None => root.w_full(),
201 };
202 root = match self.height {
203 Some(h) => root.h(px(h)),
204 None => root.h_full(),
205 };
206
207 let (image, dims) = (self.image.clone(), self.dims);
208 let fit = self.fit;
209 let root = match (image, dims) {
210 (Some(image), Some((w, h))) => root.child(
211 canvas(
212 |_, _, _| (),
213 move |bounds, _, window, _| {
214 let dst = (f32::from(bounds.size.width), f32::from(bounds.size.height));
215 let (x, y, fw, fh) = fit_rect((w as f32, h as f32), dst, fit);
216 let target = Bounds::new(
217 bounds.origin + gpui::point(px(x), px(y)),
218 size(px(fw), px(fh)),
219 );
220 let corners = if fw >= dst.0 - 0.5 && fh >= dst.1 - 0.5 {
223 Corners::all(px(radius))
224 } else {
225 Corners::default()
226 };
227 window.with_content_mask(Some(ContentMask { bounds }), |window| {
228 let _ = window.paint_image(target, corners, image.clone(), 0, false);
229 });
230 },
231 )
232 .size_full(),
233 ),
234 _ => root.text_color(dimmed).child(self.placeholder.clone()),
235 };
236 root
237 .probe("VideoView")
238 .attr("fit", format!("{:?}", self.fit))
239 .attr("frames", format!("{}", self.shown))
240 }
241}