1use crate::{
2 makepad_derive_widget::*, makepad_draw::*, makepad_platform::event::video_playback::*,
3 widget::*, image_cache::ImageCacheImpl,
4};
5
6live_design! {
7 link widgets;
8 use link::shaders::*;
9
10 pub VideoBase = {{Video}} {}
11 pub Video = <VideoBase> {
12 width: 100, height: 100
13
14 draw_bg: {
15 shape: Solid,
16 fill: Image
17 texture video_texture: textureOES
18 texture thumbnail_texture: texture2d
19 uniform show_thumbnail: 0.0
20
21 instance opacity: 1.0
22 instance image_scale: vec2(1.0, 1.0)
23 instance image_pan: vec2(0.5, 0.5)
24
25 uniform source_size: vec2(1.0, 1.0)
26 uniform target_size: vec2(-1.0, -1.0)
27
28 fn get_color_scale_pan(self) -> vec4 {
29 if self.target_size.x <= 0.0 && self.target_size.y <= 0.0 {
32 if self.show_thumbnail > 0.0 {
33 return sample2d(self.thumbnail_texture, self.pos).xyzw;
34 } else {
35 return sample2dOES(self.video_texture, self.pos);
36 }
37 }
38
39 let scale = self.image_scale;
40 let pan = self.image_pan;
41 let source_aspect_ratio = self.source_size.x / self.source_size.y;
42 let target_aspect_ratio = self.target_size.x / self.target_size.y;
43
44 if (source_aspect_ratio != target_aspect_ratio) {
46 if (source_aspect_ratio > target_aspect_ratio) {
47 scale.x = target_aspect_ratio / source_aspect_ratio;
48 scale.y = 1.0;
49 } else {
50 scale.x = 1.0;
51 scale.y = source_aspect_ratio / target_aspect_ratio;
52 }
53 }
54
55 let pan_range_x = max(0.0, (1.0 - scale.x));
57 let pan_range_y = max(0.0, (1.0 - scale.y));
58
59 let adjusted_pan_x = pan_range_x * pan.x;
61 let adjusted_pan_y = pan_range_y * pan.y;
62 let adjusted_pan = vec2(adjusted_pan_x, adjusted_pan_y);
63 let adjusted_pos = (self.pos * scale) + adjusted_pan;
64
65 if self.show_thumbnail > 0.5 {
66 return sample2d(self.thumbnail_texture, adjusted_pos).xyzw;
67 } else {
68 return sample2dOES(self.video_texture, adjusted_pos);
69 }
70 }
71
72 fn pixel(self) -> vec4 {
73 let color = self.get_color_scale_pan();
74 return Pal::premul(vec4(color.xyz, color.w * self.opacity));
75 }
76 }
77 }
78}
79
80#[derive(Live, Widget)]
108pub struct Video {
109 #[redraw] #[live]
111 draw_bg: DrawColor,
112 #[walk]
113 walk: Walk,
114 #[live]
115 layout: Layout,
116 #[live]
117 scale: f64,
118
119 #[live]
121 source: VideoDataSource,
122 #[rust]
123 video_texture: Option<Texture>,
124 #[rust]
125 video_texture_handle: Option<u32>,
126 #[live]
128 thumbnail_source: Option<LiveDependency>,
129 #[rust]
130 thumbnail_texture: Option<Texture>,
131
132 #[live(false)]
134 is_looping: bool,
135 #[live(false)]
136 hold_to_pause: bool,
137 #[live(false)]
138 autoplay: bool,
139 #[live(false)]
140 mute: bool,
141 #[rust]
142 playback_state: PlaybackState,
143 #[rust]
144 should_prepare_playback: bool,
145 #[rust]
146 audio_state: AudioState,
147 #[live(false)]
149 show_thumbnail_before_playback: bool,
150
151 #[rust(false)]
153 should_dispatch_texture_updates: bool,
154
155 #[rust]
157 video_width: usize,
158 #[rust]
159 video_height: usize,
160 #[rust]
161 total_duration: u128,
162
163 #[rust]
164 id: LiveId,
165}
166
167impl VideoRef {
168 pub fn prepare_playback(&self, cx: &mut Cx) {
174 if let Some(mut inner) = self.borrow_mut() {
175 inner.prepare_playback(cx);
176 }
177 }
178
179 pub fn begin_playback(&self, cx: &mut Cx) {
181 if let Some(mut inner) = self.borrow_mut() {
182 inner.begin_playback(cx);
183 }
184 }
185
186 pub fn pause_playback(&self, cx: &mut Cx) {
188 if let Some(mut inner) = self.borrow_mut() {
189 inner.pause_playback(cx);
190 }
191 }
192
193 pub fn resume_playback(&self, cx: &mut Cx) {
195 if let Some(mut inner) = self.borrow_mut() {
196 inner.resume_playback(cx);
197 }
198 }
199
200 pub fn mute_playback(&self, cx: &mut Cx) {
202 if let Some(mut inner) = self.borrow_mut() {
203 inner.mute_playback(cx);
204 }
205 }
206
207 pub fn unmute_playback(&self, cx: &mut Cx) {
209 if let Some(mut inner) = self.borrow_mut() {
210 inner.unmute_playback(cx);
211 }
212 }
213
214 pub fn stop_and_cleanup_resources(&self, cx: &mut Cx) {
219 if let Some(mut inner) = self.borrow_mut() {
220 inner.stop_and_cleanup_resources(cx);
221 }
222 }
223
224 pub fn set_source(&self, source: VideoDataSource) {
226 if let Some(mut inner) = self.borrow_mut() {
227 inner.set_source(source);
228 }
229 }
230
231 pub fn should_dispatch_texture_updates(&self, should_dispatch: bool) {
234 if let Some(mut inner) = self.borrow_mut() {
235 inner.should_dispatch_texture_updates = should_dispatch;
236 }
237 }
238
239 pub fn set_thumbnail_texture(&self, cx: &mut Cx, texture: Option<Texture>) {
240 if let Some(mut inner) = self.borrow_mut() {
241 inner.thumbnail_texture = texture;
242 inner.load_thumbnail_image(cx);
243 }
244 }
245
246 pub fn is_unprepared(&self) -> bool {
247 if let Some(inner) = self.borrow() {
248 return inner.playback_state == PlaybackState::Unprepared
249 }
250 false
251 }
252
253 pub fn is_preparing(&self) -> bool {
254 if let Some(inner) = self.borrow() {
255 return inner.playback_state == PlaybackState::Preparing
256 }
257 false
258 }
259
260 pub fn is_prepared(&self) -> bool {
261 if let Some(inner) = self.borrow() {
262 return inner.playback_state == PlaybackState::Prepared
263 }
264 false
265 }
266
267 pub fn is_playing(&self) -> bool {
268 if let Some(inner) = self.borrow() {
269 return inner.playback_state == PlaybackState::Playing
270 }
271 false
272 }
273
274 pub fn is_paused(&self) -> bool {
275 if let Some(inner) = self.borrow() {
276 return inner.playback_state == PlaybackState::Paused
277 }
278 false
279 }
280
281 pub fn has_completed(&self) -> bool {
282 if let Some(inner) = self.borrow() {
283 return inner.playback_state == PlaybackState::Completed
284 }
285 false
286 }
287
288 pub fn is_cleaning_up(&self) -> bool {
289 if let Some(inner) = self.borrow() {
290 return inner.playback_state == PlaybackState::CleaningUp
291 }
292 false
293 }
294
295 pub fn is_muted(&self) -> bool {
296 if let Some(inner) = self.borrow() {
297 return inner.audio_state == AudioState::Muted
298 }
299 false
300 }
301}
302
303#[derive(Default, PartialEq, Debug)]
304enum PlaybackState {
305 #[default]
306 Unprepared,
307 Preparing,
308 Prepared,
309 Playing,
310 Paused,
311 Completed,
313 CleaningUp,
318}
319
320#[derive(Default, PartialEq, Debug)]
321enum AudioState {
322 #[default]
323 Playing,
324 Muted,
325}
326
327impl LiveHook for Video {
328 #[allow(unused)]
329 fn after_new_from_doc(&mut self, cx: &mut Cx) {
330 self.id = LiveId::unique();
331
332 #[cfg(target_os = "android")]
333 {
334 if self.video_texture.is_none() {
335 let new_texture = Texture::new_with_format(cx, TextureFormat::VideoRGB);
336 self.video_texture = Some(new_texture);
337 }
338 let texture = self.video_texture.as_mut().unwrap();
339 self.draw_bg.draw_vars.set_texture(0, &texture);
340 }
341
342 #[cfg(not(target_os = "android"))]
343 error!("Video Widget is currently only supported on Android.");
344
345 match cx.os_type() {
346 OsType::Android(params) => {
347 if params.is_emulator {
348 panic!("Video Widget is currently only supported on real devices. (unreliable support for external textures on some emulators hosts)");
349 }
350 },
351 _ => {}
352 }
353
354 self.should_prepare_playback = self.autoplay;
355 }
356
357 fn after_apply(&mut self, cx: &mut Cx, _apply: &mut Apply, _index: usize, _nodes: &[LiveNode]) {
358 self.lazy_create_image_cache(cx);
359 self.thumbnail_texture = Some(Texture::new(cx));
360
361 let target_w = self.walk.width.fixed_or_zero();
362 let target_h = self.walk.height.fixed_or_zero();
363 self.draw_bg
364 .set_uniform(cx, id!(target_size), &[target_w as f32, target_h as f32]);
365
366 if self.show_thumbnail_before_playback {
367 self.load_thumbnail_image(cx);
368 self.draw_bg
369 .set_uniform(cx, id!(show_thumbnail), &[1.0]);
370 }
371 }
372}
373
374#[derive(Clone, Debug, DefaultNone)]
375pub enum VideoAction {
376 None,
377 PlaybackPrepared,
378 PlaybackBegan,
379 TextureUpdated,
380 PlaybackCompleted,
381 PlayerReset,
382 SecondaryClicked {
384 abs: DVec2,
385 modifiers: KeyModifiers,
386 }
387}
388
389impl Widget for Video {
390
391 fn draw_walk(&mut self, cx: &mut Cx2d, _scope:&mut Scope, walk: Walk) -> DrawStep {
392 if let Some(texture) = &self.thumbnail_texture {
393 self.draw_bg.draw_vars.set_texture(1, texture);
394 }
395
396 self.draw_bg.draw_walk(cx, walk);
397 DrawStep::done()
398 }
399
400 fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope){
401 let uid = self.widget_uid();
402 match event{
403 Event::VideoPlaybackPrepared(event)=> if event.video_id == self.id {
404 self.handle_playback_prepared(cx, event);
405 cx.widget_action(uid, &scope.path, VideoAction::PlaybackPrepared);
406 }
407 Event::VideoTextureUpdated(event)=>if event.video_id == self.id {
408 self.redraw(cx);
409 if self.playback_state == PlaybackState::Prepared {
410 self.playback_state = PlaybackState::Playing;
411 cx.widget_action(uid, &scope.path, VideoAction::PlaybackBegan);
412 self.draw_bg
413 .set_uniform(cx, id!(show_thumbnail), &[0.0]);
414 }
415 if self.should_dispatch_texture_updates {
416 cx.widget_action(uid, &scope.path, VideoAction::TextureUpdated);
417 }
418 }
419 Event::VideoPlaybackCompleted(event) => if event.video_id == self.id {
420 if !self.is_looping {
421 self.playback_state = PlaybackState::Completed;
422 cx.widget_action(uid, &scope.path, VideoAction::PlaybackCompleted);
423 }
424 }
425 Event::VideoPlaybackResourcesReleased(event) => if event.video_id == self.id {
426 self.playback_state = PlaybackState::Unprepared;
427 cx.widget_action(uid, &scope.path, VideoAction::PlayerReset);
428 }
429 Event::TextureHandleReady(event) => {
430 if event.texture_id == self.video_texture.clone().unwrap().texture_id() {
431 self.video_texture_handle = Some(event.handle);
432 self.maybe_prepare_playback(cx);
433 }
434 }
435 _=>()
436 }
437
438 self.handle_gestures(cx, event, scope);
439 self.handle_activity_events(cx, event);
440 self.handle_errors(event);
441 }
442}
443
444impl ImageCacheImpl for Video {
445 fn get_texture(&self, _id:usize) -> &Option<Texture> {
446 &self.thumbnail_texture
447 }
448
449 fn set_texture(&mut self, texture: Option<Texture>, _id:usize) {
450 self.thumbnail_texture = texture;
451 }
452}
453
454impl Video {
455 fn maybe_prepare_playback(&mut self, cx: &mut Cx) {
456 if self.playback_state == PlaybackState::Unprepared && self.should_prepare_playback {
457 if self.video_texture_handle.is_none() {
458 return;
460 }
461
462 let source = match &self.source {
463 VideoDataSource::Dependency { path } => match cx.get_dependency(path.as_str()) {
464 Ok(data) => VideoSource::InMemory(data),
465 Err(e) => {
466 error!(
467 "Attempted to prepare playback: resource not found {} {}",
468 path.as_str(),
469 e
470 );
471 return;
472 }
473 },
474 VideoDataSource::Network { url } => VideoSource::Network(url.to_string()),
475 VideoDataSource::Filesystem { path } => VideoSource::Filesystem(path.to_string()),
476 };
477
478 cx.prepare_video_playback(
479 self.id,
480 source,
481 self.video_texture_handle.unwrap(),
482 self.autoplay,
483 self.is_looping,
484 );
485
486 self.playback_state = PlaybackState::Preparing;
487 self.should_prepare_playback = false;
488 }
489 }
490
491 fn handle_playback_prepared(&mut self, cx: &mut Cx, event: &VideoPlaybackPreparedEvent) {
492 self.playback_state = PlaybackState::Prepared;
493 self.video_width = event.video_width as usize;
494 self.video_height = event.video_height as usize;
495 self.total_duration = event.duration;
496
497 self.draw_bg
498 .set_uniform(cx, id!(source_size), &[self.video_width as f32, self.video_height as f32]);
499
500 if self.mute && self.audio_state != AudioState::Muted {
501 cx.mute_video_playback(self.id);
502 }
503 }
504
505 fn handle_gestures(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
506 match event.hits(cx, self.draw_bg.area()) {
507 Hit::FingerDown(fe) if fe.is_primary_hit() => {
508 if self.hold_to_pause {
509 self.pause_playback(cx);
510 }
511 }
512 Hit::FingerDown(fe) if fe.mouse_button().is_some_and(|mb| mb.is_secondary()) => {
513 self.handle_secondary_click(cx, scope, fe.abs, fe.modifiers);
514 }
515 Hit::FingerLongPress(lp) => {
516 self.handle_secondary_click(cx, scope, lp.abs, Default::default());
519 }
520 Hit::FingerUp(fe) if fe.is_primary_hit() => {
521 if self.hold_to_pause {
522 self.resume_playback(cx);
523 }
524 }
525 _ => (),
526 }
527 }
528
529 fn handle_activity_events(&mut self, cx: &mut Cx, event: &Event) {
530 match event {
531 Event::Pause => self.pause_playback(cx),
532 Event::Resume => self.resume_playback(cx),
533 _ => (),
534 }
535 }
536
537 fn handle_errors(&mut self, event: &Event) {
538 if let Event::VideoDecodingError(event) = event {
539 if event.video_id == self.id {
540 error!(
541 "Error decoding video with id {} : {}",
542 self.id.0, event.error
543 );
544 }
545 }
546 }
547
548 fn prepare_playback(&mut self, cx: &mut Cx) {
549 if self.playback_state == PlaybackState::Unprepared {
550 self.should_prepare_playback = true;
551 self.maybe_prepare_playback(cx);
552 }
553 }
554
555 fn begin_playback(&mut self, cx: &mut Cx) {
556 if self.playback_state == PlaybackState::Unprepared {
557 self.should_prepare_playback = true;
558 self.autoplay = true;
559 self.maybe_prepare_playback(cx);
560 } else if self.playback_state == PlaybackState::Prepared {
561 cx.begin_video_playback(self.id);
562 }
563 }
564
565 fn handle_secondary_click(
566 &mut self,
567 cx: &mut Cx,
568 scope: &mut Scope,
569 abs: DVec2,
570 modifiers: KeyModifiers,
571 ) {
572 cx.widget_action(
573 self.widget_uid(),
574 &scope.path,
575 VideoAction::SecondaryClicked {
576 abs,
577 modifiers,
578 }
579 );
580 }
581
582 fn pause_playback(&mut self, cx: &mut Cx) {
583 if self.playback_state != PlaybackState::Paused {
584 cx.pause_video_playback(self.id);
585 self.playback_state = PlaybackState::Paused;
586 }
587 }
588
589 fn resume_playback(&mut self, cx: &mut Cx) {
590 if self.playback_state == PlaybackState::Paused {
591 cx.resume_video_playback(self.id);
592 self.playback_state = PlaybackState::Playing;
593 }
594 }
595
596 fn mute_playback(&mut self, cx: &mut Cx) {
597 if self.playback_state == PlaybackState::Playing || self.playback_state == PlaybackState::Paused || self.playback_state == PlaybackState::Prepared {
598 cx.mute_video_playback(self.id);
599 self.audio_state = AudioState::Muted;
600 }
601 }
602
603 fn unmute_playback(&mut self, cx: &mut Cx) {
604 if self.playback_state == PlaybackState::Playing || self.playback_state == PlaybackState::Paused || self.playback_state == PlaybackState::Prepared
605 && self.audio_state == AudioState::Muted {
606 cx.unmute_video_playback(self.id);
607 self.audio_state = AudioState::Playing;
608 }
609 }
610
611 fn stop_and_cleanup_resources(&mut self, cx: &mut Cx) {
612 if self.playback_state != PlaybackState::Unprepared
613 && self.playback_state != PlaybackState::Preparing
614 && self.playback_state != PlaybackState::CleaningUp {
615 cx.cleanup_video_playback_resources(self.id);
616
617 self.playback_state = PlaybackState::CleaningUp;
618 self.autoplay = false;
619 self.should_prepare_playback = false;
620 }
621 }
622
623 fn set_source(&mut self, source: VideoDataSource) {
624 if self.playback_state == PlaybackState::Unprepared {
625 self.source = source;
626 } else {
627 error!(
628 "Attempted to set source while player {} state is: {:?}",
629 self.id.0,
630 self.playback_state
631 );
632 }
633 }
634
635 fn load_thumbnail_image(&mut self, cx: &mut Cx) {
636 if let Some(path) = self.thumbnail_source.clone() {
637 let path_str = path.as_str();
638
639 if path_str.len() > 0 {
640 let _ = self.load_image_dep_by_path(cx, path_str, 0);
641 }
642 }
643 }
644}
645
646#[derive(Clone, Debug, Live, LiveHook)]
654#[live_ignore]
655pub enum VideoDataSource {
656 #[live {path: LiveDependency::default()}]
657 Dependency { path: LiveDependency },
658 #[pick {url: "".to_string()}]
659 Network { url: String },
660 #[live {path: "".to_string()}]
661 Filesystem { path: String },
662}