/src/serenity/Userland/Libraries/LibWeb/Painting/VideoPaintable.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Array.h> |
8 | | #include <LibGfx/AntiAliasingPainter.h> |
9 | | #include <LibWeb/DOM/Document.h> |
10 | | #include <LibWeb/HTML/HTMLMediaElement.h> |
11 | | #include <LibWeb/HTML/HTMLVideoElement.h> |
12 | | #include <LibWeb/HTML/VideoTrackList.h> |
13 | | #include <LibWeb/Layout/VideoBox.h> |
14 | | #include <LibWeb/Painting/BorderRadiusCornerClipper.h> |
15 | | #include <LibWeb/Painting/VideoPaintable.h> |
16 | | |
17 | | namespace Web::Painting { |
18 | | |
19 | | static constexpr auto control_box_color = Gfx::Color::from_rgb(0x26'26'26); |
20 | | static constexpr auto control_highlight_color = Gfx::Color::from_rgb(0x1d'99'f3); |
21 | | |
22 | | JS_DEFINE_ALLOCATOR(VideoPaintable); |
23 | | |
24 | | static constexpr Gfx::Color control_button_color(bool is_hovered) |
25 | 0 | { |
26 | 0 | if (!is_hovered) |
27 | 0 | return Color::White; |
28 | 0 | return control_highlight_color; |
29 | 0 | } |
30 | | |
31 | | JS::NonnullGCPtr<VideoPaintable> VideoPaintable::create(Layout::VideoBox const& layout_box) |
32 | 0 | { |
33 | 0 | return layout_box.heap().allocate_without_realm<VideoPaintable>(layout_box); |
34 | 0 | } |
35 | | |
36 | | VideoPaintable::VideoPaintable(Layout::VideoBox const& layout_box) |
37 | 0 | : MediaPaintable(layout_box) |
38 | 0 | { |
39 | 0 | } |
40 | | |
41 | | Layout::VideoBox& VideoPaintable::layout_box() |
42 | 0 | { |
43 | 0 | return static_cast<Layout::VideoBox&>(layout_node()); |
44 | 0 | } |
45 | | |
46 | | Layout::VideoBox const& VideoPaintable::layout_box() const |
47 | 0 | { |
48 | 0 | return static_cast<Layout::VideoBox const&>(layout_node()); |
49 | 0 | } |
50 | | |
51 | | void VideoPaintable::paint(PaintContext& context, PaintPhase phase) const |
52 | 0 | { |
53 | 0 | if (!is_visible()) |
54 | 0 | return; |
55 | | |
56 | 0 | Base::paint(context, phase); |
57 | |
|
58 | 0 | if (phase != PaintPhase::Foreground) |
59 | 0 | return; |
60 | | |
61 | 0 | DisplayListRecorderStateSaver saver { context.display_list_recorder() }; |
62 | |
|
63 | 0 | auto video_rect = context.rounded_device_rect(absolute_rect()); |
64 | 0 | context.display_list_recorder().add_clip_rect(video_rect.to_type<int>()); |
65 | |
|
66 | 0 | ScopedCornerRadiusClip corner_clip { context, video_rect, normalized_border_radii_data(ShrinkRadiiForBorders::Yes) }; |
67 | |
|
68 | 0 | auto const& video_element = layout_box().dom_node(); |
69 | 0 | auto mouse_position = MediaPaintable::mouse_position(context, video_element); |
70 | |
|
71 | 0 | auto const& current_frame = video_element.current_frame(); |
72 | 0 | auto const& poster_frame = video_element.poster_frame(); |
73 | |
|
74 | 0 | auto current_playback_position = video_element.current_playback_position(); |
75 | 0 | auto ready_state = video_element.ready_state(); |
76 | |
|
77 | 0 | enum class Representation { |
78 | 0 | Unknown, |
79 | 0 | FirstVideoFrame, |
80 | 0 | CurrentVideoFrame, |
81 | 0 | LastRenderedVideoFrame, |
82 | 0 | PosterFrame, |
83 | 0 | TransparentBlack, |
84 | 0 | }; |
85 | |
|
86 | 0 | auto representation = Representation::Unknown; |
87 | | |
88 | | // https://html.spec.whatwg.org/multipage/media.html#the-video-element:the-video-element-7 |
89 | | // A video element represents what is given for the first matching condition in the list below: |
90 | | |
91 | | // -> When no video data is available (the element's readyState attribute is either HAVE_NOTHING, or HAVE_METADATA |
92 | | // but no video data has yet been obtained at all, or the element's readyState attribute is any subsequent value |
93 | | // but the media resource does not have a video channel) |
94 | 0 | if (ready_state == HTML::HTMLMediaElement::ReadyState::HaveNothing |
95 | 0 | || (ready_state >= HTML::HTMLMediaElement::ReadyState::HaveMetadata && video_element.video_tracks()->length() == 0)) { |
96 | | // The video element represents its poster frame, if any, or else transparent black with no intrinsic dimensions. |
97 | 0 | representation = poster_frame ? Representation::PosterFrame : Representation::TransparentBlack; |
98 | 0 | } |
99 | | |
100 | | // -> When the video element is paused, the current playback position is the first frame of video, and the element's |
101 | | // show poster flag is set |
102 | 0 | else if (video_element.paused() && current_playback_position == 0 && video_element.show_poster()) { |
103 | | // The video element represents its poster frame, if any, or else the first frame of the video. |
104 | 0 | representation = poster_frame ? Representation::PosterFrame : Representation::FirstVideoFrame; |
105 | 0 | } |
106 | | |
107 | | // -> When the video element is paused, and the frame of video corresponding to the current playback position |
108 | | // is not available (e.g. because the video is seeking or buffering) |
109 | | // -> When the video element is neither potentially playing nor paused (e.g. when seeking or stalled) |
110 | 0 | else if ( |
111 | 0 | (video_element.paused() && current_playback_position != current_frame.position) |
112 | 0 | || (!video_element.potentially_playing() && !video_element.paused())) { |
113 | | // The video element represents the last frame of the video to have been rendered. |
114 | 0 | representation = Representation::LastRenderedVideoFrame; |
115 | 0 | } |
116 | | |
117 | | // -> When the video element is paused |
118 | 0 | else if (video_element.paused()) { |
119 | | // The video element represents the frame of video corresponding to the current playback position. |
120 | 0 | representation = Representation::CurrentVideoFrame; |
121 | 0 | } |
122 | | |
123 | | // -> Otherwise (the video element has a video channel and is potentially playing) |
124 | 0 | else { |
125 | | // The video element represents the frame of video at the continuously increasing "current" position. When the |
126 | | // current playback position changes such that the last frame rendered is no longer the frame corresponding to |
127 | | // the current playback position in the video, the new frame must be rendered. |
128 | 0 | representation = Representation::CurrentVideoFrame; |
129 | 0 | } |
130 | |
|
131 | 0 | auto paint_frame = [&](auto const& frame) { |
132 | 0 | auto scaling_mode = to_gfx_scaling_mode(computed_values().image_rendering(), frame->rect(), video_rect.to_type<int>()); |
133 | 0 | context.display_list_recorder().draw_scaled_bitmap(video_rect.to_type<int>(), *frame, frame->rect(), scaling_mode); |
134 | 0 | }; |
135 | |
|
136 | 0 | auto paint_transparent_black = [&]() { |
137 | 0 | static constexpr auto transparent_black = Gfx::Color::from_argb(0x00'00'00'00); |
138 | 0 | context.display_list_recorder().fill_rect(video_rect.to_type<int>(), transparent_black); |
139 | 0 | }; |
140 | |
|
141 | 0 | auto paint_loaded_video_controls = [&]() { |
142 | 0 | auto is_hovered = document().hovered_node() == &video_element; |
143 | 0 | auto is_paused = video_element.paused(); |
144 | |
|
145 | 0 | if (is_hovered || is_paused) |
146 | 0 | paint_media_controls(context, video_element, video_rect, mouse_position); |
147 | 0 | }; |
148 | |
|
149 | 0 | auto paint_user_agent_controls = video_element.has_attribute(HTML::AttributeNames::controls) || video_element.is_scripting_disabled(); |
150 | |
|
151 | 0 | switch (representation) { |
152 | 0 | case Representation::FirstVideoFrame: |
153 | 0 | case Representation::CurrentVideoFrame: |
154 | 0 | case Representation::LastRenderedVideoFrame: |
155 | | // FIXME: We likely need to cache all (or a subset of) decoded video frames along with their position. We at least |
156 | | // will need the first video frame and the last-rendered video frame. |
157 | 0 | if (current_frame.frame) |
158 | 0 | paint_frame(current_frame.frame); |
159 | 0 | if (paint_user_agent_controls) |
160 | 0 | paint_loaded_video_controls(); |
161 | 0 | break; |
162 | | |
163 | 0 | case Representation::PosterFrame: |
164 | 0 | VERIFY(poster_frame); |
165 | 0 | paint_frame(poster_frame); |
166 | 0 | if (paint_user_agent_controls) |
167 | 0 | paint_placeholder_video_controls(context, video_rect, mouse_position); |
168 | 0 | break; |
169 | | |
170 | 0 | case Representation::TransparentBlack: |
171 | 0 | paint_transparent_black(); |
172 | 0 | if (paint_user_agent_controls) |
173 | 0 | paint_placeholder_video_controls(context, video_rect, mouse_position); |
174 | 0 | break; |
175 | | |
176 | 0 | case Representation::Unknown: |
177 | 0 | VERIFY_NOT_REACHED(); |
178 | 0 | } |
179 | 0 | } |
180 | | |
181 | | void VideoPaintable::paint_placeholder_video_controls(PaintContext& context, DevicePixelRect video_rect, Optional<DevicePixelPoint> const& mouse_position) const |
182 | 0 | { |
183 | 0 | auto maximum_control_box_size = context.rounded_device_pixels(100); |
184 | 0 | auto maximum_playback_button_size = context.rounded_device_pixels(40); |
185 | |
|
186 | 0 | auto center = video_rect.center(); |
187 | |
|
188 | 0 | auto control_box_size = min(maximum_control_box_size, min(video_rect.width(), video_rect.height()) * 4 / 5); |
189 | 0 | auto control_box_offset_x = control_box_size / 2; |
190 | 0 | auto control_box_offset_y = control_box_size / 2; |
191 | |
|
192 | 0 | auto control_box_location = center.translated(-control_box_offset_x, -control_box_offset_y); |
193 | 0 | DevicePixelRect control_box_rect { control_box_location, { control_box_size, control_box_size } }; |
194 | |
|
195 | 0 | auto playback_button_size = min(maximum_playback_button_size, min(video_rect.width(), video_rect.height()) * 2 / 5); |
196 | 0 | auto playback_button_offset_x = playback_button_size / 2; |
197 | 0 | auto playback_button_offset_y = playback_button_size / 2; |
198 | | |
199 | | // We want to center the play button on its center of mass, which is not the midpoint of its vertices. |
200 | | // To do so, reduce its desired x offset by a factor of tan(30 degrees) / 2 (about 0.288685). |
201 | 0 | playback_button_offset_x -= 0.288685f * static_cast<float>(static_cast<DevicePixels::Type>(playback_button_offset_x)); |
202 | |
|
203 | 0 | auto playback_button_location = center.translated(-playback_button_offset_x, -playback_button_offset_y); |
204 | |
|
205 | 0 | Array<Gfx::IntPoint, 3> play_button_coordinates { { |
206 | 0 | { 0, 0 }, |
207 | 0 | { static_cast<int>(playback_button_size), static_cast<int>(playback_button_size) / 2 }, |
208 | 0 | { 0, static_cast<int>(playback_button_size) }, |
209 | 0 | } }; |
210 | |
|
211 | 0 | auto playback_button_is_hovered = mouse_position.has_value() && control_box_rect.contains(*mouse_position); |
212 | 0 | auto playback_button_color = control_button_color(playback_button_is_hovered); |
213 | |
|
214 | 0 | context.display_list_recorder().fill_ellipse(control_box_rect.to_type<int>(), control_box_color); |
215 | 0 | fill_triangle(context.display_list_recorder(), playback_button_location.to_type<int>(), play_button_coordinates, playback_button_color); |
216 | 0 | } |
217 | | |
218 | | } |