Coverage Report

Created: 2026-11-13 08:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/HTML/HTMLVideoElement.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2020, the SerenityOS developers.
3
 * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
4
 *
5
 * SPDX-License-Identifier: BSD-2-Clause
6
 */
7
8
#include <LibGfx/Bitmap.h>
9
#include <LibWeb/Bindings/HTMLVideoElementPrototype.h>
10
#include <LibWeb/Bindings/Intrinsics.h>
11
#include <LibWeb/DOM/Document.h>
12
#include <LibWeb/Fetch/Fetching/Fetching.h>
13
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
14
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
15
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
16
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
17
#include <LibWeb/HTML/HTMLVideoElement.h>
18
#include <LibWeb/HTML/VideoTrack.h>
19
#include <LibWeb/HTML/VideoTrackList.h>
20
#include <LibWeb/Layout/VideoBox.h>
21
#include <LibWeb/Painting/Paintable.h>
22
#include <LibWeb/Platform/ImageCodecPlugin.h>
23
24
namespace Web::HTML {
25
26
JS_DEFINE_ALLOCATOR(HTMLVideoElement);
27
28
HTMLVideoElement::HTMLVideoElement(DOM::Document& document, DOM::QualifiedName qualified_name)
29
0
    : HTMLMediaElement(document, move(qualified_name))
30
0
{
31
0
}
32
33
0
HTMLVideoElement::~HTMLVideoElement() = default;
34
35
void HTMLVideoElement::initialize(JS::Realm& realm)
36
0
{
37
0
    Base::initialize(realm);
38
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLVideoElement);
39
0
}
40
41
void HTMLVideoElement::finalize()
42
0
{
43
0
    Base::finalize();
44
45
0
    for (auto video_track : video_tracks()->video_tracks())
46
0
        video_track->stop_video({});
47
0
}
48
49
void HTMLVideoElement::visit_edges(Cell::Visitor& visitor)
50
0
{
51
0
    Base::visit_edges(visitor);
52
0
    visitor.visit(m_video_track);
53
0
    visitor.visit(m_fetch_controller);
54
0
}
55
56
void HTMLVideoElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value)
57
0
{
58
0
    Base::attribute_changed(name, old_value, value);
59
60
0
    if (name == HTML::AttributeNames::poster) {
61
0
        determine_element_poster_frame(value).release_value_but_fixme_should_propagate_errors();
62
0
    }
63
0
}
64
65
JS::GCPtr<Layout::Node> HTMLVideoElement::create_layout_node(NonnullRefPtr<CSS::StyleProperties> style)
66
0
{
67
0
    return heap().allocate_without_realm<Layout::VideoBox>(document(), *this, move(style));
68
0
}
69
70
Layout::VideoBox* HTMLVideoElement::layout_node()
71
0
{
72
0
    return static_cast<Layout::VideoBox*>(Node::layout_node());
73
0
}
74
75
Layout::VideoBox const* HTMLVideoElement::layout_node() const
76
0
{
77
0
    return static_cast<Layout::VideoBox const*>(Node::layout_node());
78
0
}
79
80
// https://html.spec.whatwg.org/multipage/media.html#dom-video-videowidth
81
u32 HTMLVideoElement::video_width() const
82
0
{
83
    // The videoWidth IDL attribute must return the intrinsic width of the video in CSS pixels. The videoHeight IDL
84
    // attribute must return the intrinsic height of the video in CSS pixels. If the element's readyState attribute
85
    // is HAVE_NOTHING, then the attributes must return 0.
86
0
    if (ready_state() == ReadyState::HaveNothing)
87
0
        return 0;
88
0
    return m_video_width;
89
0
}
90
91
// https://html.spec.whatwg.org/multipage/media.html#dom-video-videoheight
92
u32 HTMLVideoElement::video_height() const
93
0
{
94
    // The videoWidth IDL attribute must return the intrinsic width of the video in CSS pixels. The videoHeight IDL
95
    // attribute must return the intrinsic height of the video in CSS pixels. If the element's readyState attribute
96
    // is HAVE_NOTHING, then the attributes must return 0.
97
0
    if (ready_state() == ReadyState::HaveNothing)
98
0
        return 0;
99
0
    return m_video_height;
100
0
}
101
102
void HTMLVideoElement::set_video_track(JS::GCPtr<HTML::VideoTrack> video_track)
103
0
{
104
0
    set_needs_style_update(true);
105
0
    document().set_needs_layout();
106
107
0
    if (m_video_track)
108
0
        m_video_track->pause_video({});
109
110
0
    m_video_track = video_track;
111
0
}
112
113
void HTMLVideoElement::set_current_frame(Badge<VideoTrack>, RefPtr<Gfx::Bitmap> frame, double position)
114
0
{
115
0
    m_current_frame = { move(frame), position };
116
0
    if (paintable())
117
0
        paintable()->set_needs_display();
118
0
}
119
120
void HTMLVideoElement::on_playing()
121
0
{
122
0
    if (m_video_track)
123
0
        m_video_track->play_video({});
124
0
}
125
126
void HTMLVideoElement::on_paused()
127
0
{
128
0
    if (m_video_track)
129
0
        m_video_track->pause_video({});
130
0
}
131
132
void HTMLVideoElement::on_seek(double position, MediaSeekMode seek_mode)
133
0
{
134
0
    if (m_video_track)
135
0
        m_video_track->seek(AK::Duration::from_milliseconds(position * 1000.0), seek_mode);
136
0
}
137
138
// https://html.spec.whatwg.org/multipage/media.html#attr-video-poster
139
WebIDL::ExceptionOr<void> HTMLVideoElement::determine_element_poster_frame(Optional<String> const& poster)
140
0
{
141
0
    auto& realm = this->realm();
142
0
    auto& vm = realm.vm();
143
144
0
    m_poster_frame = nullptr;
145
146
    // 1. If there is an existing instance of this algorithm running for this video element, abort that instance of
147
    //    this algorithm without changing the poster frame.
148
0
    if (m_fetch_controller)
149
0
        m_fetch_controller->stop_fetch();
150
151
    // 2. If the poster attribute's value is the empty string or if the attribute is absent, then there is no poster
152
    //    frame; return.
153
0
    if (!poster.has_value() || poster->is_empty())
154
0
        return {};
155
156
    // 3. Parse the poster attribute's value relative to the element's node document. If this fails, then there is no
157
    //    poster frame; return.
158
0
    auto url_record = document().parse_url(*poster);
159
0
    if (!url_record.is_valid())
160
0
        return {};
161
162
    // 4. Let request be a new request whose URL is the resulting URL record, client is the element's node document's
163
    //    relevant settings object, destination is "image", initiator type is "video", credentials mode is "include",
164
    //    and whose use-URL-credentials flag is set.
165
0
    auto request = Fetch::Infrastructure::Request::create(vm);
166
0
    request->set_url(move(url_record));
167
0
    request->set_client(&document().relevant_settings_object());
168
0
    request->set_destination(Fetch::Infrastructure::Request::Destination::Image);
169
0
    request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Video);
170
0
    request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::Include);
171
0
    request->set_use_url_credentials(true);
172
173
    // 5. Fetch request. This must delay the load event of the element's node document.
174
0
    Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
175
0
    m_load_event_delayer.emplace(document());
176
177
0
    fetch_algorithms_input.process_response = [this](auto response) mutable {
178
0
        ScopeGuard guard { [&] { m_load_event_delayer.clear(); } };
179
180
0
        auto& realm = this->realm();
181
0
        auto& global = document().realm().global_object();
182
183
0
        if (response->is_network_error())
184
0
            return;
185
186
0
        if (response->type() == Fetch::Infrastructure::Response::Type::Opaque || response->type() == Fetch::Infrastructure::Response::Type::OpaqueRedirect) {
187
0
            auto& filtered_response = static_cast<Fetch::Infrastructure::FilteredResponse&>(*response);
188
0
            response = filtered_response.internal_response();
189
0
        }
190
191
0
        auto on_image_data_read = JS::create_heap_function(heap(), [this](ByteBuffer image_data) mutable {
192
0
            m_fetch_controller = nullptr;
193
194
            // 6. If an image is thus obtained, the poster frame is that image. Otherwise, there is no poster frame.
195
0
            (void)Platform::ImageCodecPlugin::the().decode_image(
196
0
                image_data,
197
0
                [strong_this = JS::Handle(*this)](Web::Platform::DecodedImage& image) -> ErrorOr<void> {
198
0
                    if (!image.frames.is_empty())
199
0
                        strong_this->m_poster_frame = move(image.frames[0].bitmap);
200
0
                    return {};
201
0
                },
202
0
                [](auto&) {});
203
0
        });
204
205
0
        VERIFY(response->body());
206
0
        auto empty_algorithm = JS::create_heap_function(heap(), [](JS::Value) { });
207
208
0
        response->body()->fully_read(realm, on_image_data_read, empty_algorithm, JS::NonnullGCPtr { global });
209
0
    };
210
211
0
    m_fetch_controller = TRY(Fetch::Fetching::fetch(realm, request, Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input))));
212
213
0
    return {};
214
0
}
215
216
}