
The volume control's slider is drawn in a rectangle shrunken by its slider handle's size, so the handle did not move 1:1 with the user's mouse movement. To fix this, it will now check for a mousedown in the volume control with a rectangle sized to fit any possible position of the handle, but the volume value result will be calculated based on the center of the handle instead. This allows it to move 1:1 with the mouse cursor. Co-authored-by: trflynn89 <trflynn89@serenityos.org>
287 lines
11 KiB
C++
287 lines
11 KiB
C++
/*
|
|
* Copyright (c) 2020, the SerenityOS developers.
|
|
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <AK/ByteBuffer.h>
|
|
#include <AK/Optional.h>
|
|
#include <AK/Time.h>
|
|
#include <AK/Variant.h>
|
|
#include <Kernel/API/KeyCode.h>
|
|
#include <LibGfx/Rect.h>
|
|
#include <LibJS/Heap/MarkedVector.h>
|
|
#include <LibJS/SafeFunction.h>
|
|
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
|
|
#include <LibWeb/HTML/CORSSettingAttribute.h>
|
|
#include <LibWeb/HTML/EventLoop/Task.h>
|
|
#include <LibWeb/HTML/HTMLElement.h>
|
|
#include <LibWeb/PixelUnits.h>
|
|
#include <LibWeb/WebIDL/DOMException.h>
|
|
#include <math.h>
|
|
|
|
namespace Web::HTML {
|
|
|
|
enum class MediaSeekMode {
|
|
Accurate,
|
|
ApproximateForSpeed,
|
|
};
|
|
|
|
class SourceElementSelector;
|
|
|
|
class HTMLMediaElement : public HTMLElement {
|
|
WEB_PLATFORM_OBJECT(HTMLMediaElement, HTMLElement);
|
|
|
|
public:
|
|
virtual ~HTMLMediaElement() override;
|
|
|
|
virtual bool is_focusable() const override { return true; }
|
|
|
|
void queue_a_media_element_task(JS::SafeFunction<void()> steps);
|
|
|
|
JS::GCPtr<MediaError> error() const { return m_error; }
|
|
WebIDL::ExceptionOr<void> set_decoder_error(String error_message);
|
|
|
|
String const& current_src() const { return m_current_src; }
|
|
WebIDL::ExceptionOr<void> select_resource();
|
|
|
|
enum class NetworkState : u16 {
|
|
Empty,
|
|
Idle,
|
|
Loading,
|
|
NoSource,
|
|
};
|
|
NetworkState network_state() const { return m_network_state; }
|
|
|
|
WebIDL::ExceptionOr<JS::NonnullGCPtr<TimeRanges>> buffered() const;
|
|
|
|
WebIDL::ExceptionOr<Bindings::CanPlayTypeResult> can_play_type(DeprecatedString const& type) const;
|
|
|
|
enum class ReadyState : u16 {
|
|
HaveNothing,
|
|
HaveMetadata,
|
|
HaveCurrentData,
|
|
HaveFutureData,
|
|
HaveEnoughData,
|
|
};
|
|
ReadyState ready_state() const { return m_ready_state; }
|
|
bool blocked() const;
|
|
bool stalled() const;
|
|
|
|
bool seeking() const { return m_seeking; }
|
|
void set_seeking(bool);
|
|
|
|
WebIDL::ExceptionOr<void> load();
|
|
|
|
double current_time() const;
|
|
void set_current_time(double);
|
|
void fast_seek(double);
|
|
|
|
double current_playback_position() const { return m_current_playback_position; }
|
|
void set_current_playback_position(double);
|
|
|
|
double duration() const;
|
|
bool show_poster() const { return m_show_poster; }
|
|
bool paused() const { return m_paused; }
|
|
bool ended() const;
|
|
bool potentially_playing() const;
|
|
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> play();
|
|
WebIDL::ExceptionOr<void> pause();
|
|
WebIDL::ExceptionOr<void> toggle_playback();
|
|
|
|
double volume() const { return m_volume; }
|
|
WebIDL::ExceptionOr<void> set_volume(double);
|
|
|
|
bool muted() const { return m_muted; }
|
|
void set_muted(bool);
|
|
|
|
double effective_media_volume() const;
|
|
|
|
JS::NonnullGCPtr<AudioTrackList> audio_tracks() const { return *m_audio_tracks; }
|
|
JS::NonnullGCPtr<VideoTrackList> video_tracks() const { return *m_video_tracks; }
|
|
|
|
WebIDL::ExceptionOr<void> handle_keydown(Badge<Web::EventHandler>, KeyCode);
|
|
|
|
enum class MouseTrackingComponent {
|
|
Timeline,
|
|
Volume,
|
|
};
|
|
void set_layout_mouse_tracking_component(Badge<Painting::MediaPaintable>, Optional<MouseTrackingComponent> mouse_tracking_component) { m_mouse_tracking_component = move(mouse_tracking_component); }
|
|
Optional<MouseTrackingComponent> const& layout_mouse_tracking_component(Badge<Painting::MediaPaintable>) const { return m_mouse_tracking_component; }
|
|
|
|
void set_layout_mouse_position(Badge<Painting::MediaPaintable>, Optional<CSSPixelPoint> mouse_position) { m_mouse_position = move(mouse_position); }
|
|
Optional<CSSPixelPoint> const& layout_mouse_position(Badge<Painting::MediaPaintable>) const { return m_mouse_position; }
|
|
|
|
void set_layout_display_time(Badge<Painting::MediaPaintable>, Optional<double> display_time);
|
|
double layout_display_time(Badge<Painting::MediaPaintable>) const;
|
|
|
|
struct CachedLayoutBoxes {
|
|
Optional<CSSPixelRect> control_box_rect;
|
|
Optional<CSSPixelRect> playback_button_rect;
|
|
Optional<CSSPixelRect> timeline_rect;
|
|
Optional<CSSPixelRect> speaker_button_rect;
|
|
Optional<CSSPixelRect> volume_rect;
|
|
Optional<CSSPixelRect> volume_scrub_rect;
|
|
};
|
|
CachedLayoutBoxes& cached_layout_boxes(Badge<Painting::MediaPaintable>) const { return m_layout_boxes; }
|
|
|
|
protected:
|
|
HTMLMediaElement(DOM::Document&, DOM::QualifiedName);
|
|
|
|
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
|
|
virtual void visit_edges(Cell::Visitor&) override;
|
|
|
|
virtual void attribute_changed(DeprecatedFlyString const& name, DeprecatedString const& value) override;
|
|
virtual void removed_from(DOM::Node*) override;
|
|
virtual void children_changed() override;
|
|
|
|
// Override in subclasses to handle implementation-specific behavior when the element state changes
|
|
// to playing or paused, e.g. to start/stop play timers.
|
|
virtual void on_playing() { }
|
|
virtual void on_paused() { }
|
|
|
|
// Override in subclasses to handle implementation-specific seeking behavior. When seeking is complete,
|
|
// subclasses must invoke set_current_playback_position() to unblock the user agent.
|
|
virtual void on_seek(double, MediaSeekMode) { m_seek_in_progress = false; }
|
|
|
|
virtual void on_volume_change() { }
|
|
|
|
private:
|
|
friend SourceElementSelector;
|
|
|
|
struct EntireResource { };
|
|
using ByteRange = Variant<EntireResource>; // FIXME: This will need to include "until end" and an actual byte range.
|
|
|
|
Task::Source media_element_event_task_source() const { return m_media_element_event_task_source.source; }
|
|
|
|
WebIDL::ExceptionOr<void> load_element();
|
|
WebIDL::ExceptionOr<void> fetch_resource(AK::URL const&, Function<void(String)> failure_callback);
|
|
static bool verify_response(JS::NonnullGCPtr<Fetch::Infrastructure::Response>, ByteRange const&);
|
|
WebIDL::ExceptionOr<void> process_media_data(Function<void(String)> failure_callback);
|
|
WebIDL::ExceptionOr<void> handle_media_source_failure(Span<JS::NonnullGCPtr<WebIDL::Promise>> promises, String error_message);
|
|
void forget_media_resource_specific_tracks();
|
|
void set_ready_state(ReadyState);
|
|
|
|
WebIDL::ExceptionOr<void> play_element();
|
|
WebIDL::ExceptionOr<void> pause_element();
|
|
void seek_element(double playback_position, MediaSeekMode = MediaSeekMode::Accurate);
|
|
void notify_about_playing();
|
|
void set_show_poster(bool);
|
|
void set_paused(bool);
|
|
void set_duration(double);
|
|
|
|
void volume_or_muted_attribute_changed();
|
|
|
|
bool is_eligible_for_autoplay() const;
|
|
bool has_ended_playback() const;
|
|
WebIDL::ExceptionOr<void> reached_end_of_media_playback();
|
|
|
|
WebIDL::ExceptionOr<void> dispatch_time_update_event();
|
|
|
|
enum class TimeMarchesOnReason {
|
|
NormalPlayback,
|
|
Other,
|
|
};
|
|
void time_marches_on(TimeMarchesOnReason = TimeMarchesOnReason::NormalPlayback);
|
|
|
|
JS::MarkedVector<JS::NonnullGCPtr<WebIDL::Promise>> take_pending_play_promises();
|
|
void resolve_pending_play_promises(ReadonlySpan<JS::NonnullGCPtr<WebIDL::Promise>> promises);
|
|
void reject_pending_play_promises(ReadonlySpan<JS::NonnullGCPtr<WebIDL::Promise>> promises, JS::NonnullGCPtr<WebIDL::DOMException> error);
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#reject-pending-play-promises
|
|
template<typename ErrorType>
|
|
void reject_pending_play_promises(ReadonlySpan<JS::NonnullGCPtr<WebIDL::Promise>> promises, FlyString const& message)
|
|
{
|
|
auto& realm = this->realm();
|
|
|
|
auto error = ErrorType::create(realm, message.to_deprecated_fly_string());
|
|
reject_pending_play_promises(promises, error);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#media-element-event-task-source
|
|
UniqueTaskSource m_media_element_event_task_source {};
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-error
|
|
JS::GCPtr<MediaError> m_error;
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-crossorigin
|
|
CORSSettingAttribute m_crossorigin { CORSSettingAttribute::NoCORS };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-currentsrc
|
|
String m_current_src;
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-networkstate
|
|
NetworkState m_network_state { NetworkState::Empty };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-readystate
|
|
ReadyState m_ready_state { ReadyState::HaveNothing };
|
|
bool m_first_data_load_event_since_load_start { false };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-seeking
|
|
bool m_seeking { false };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#current-playback-position
|
|
double m_current_playback_position { 0 };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#official-playback-position
|
|
double m_official_playback_position { 0 };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#default-playback-start-position
|
|
double m_default_playback_start_position { 0 };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#show-poster-flag
|
|
bool m_show_poster { true };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-duration
|
|
double m_duration { NAN };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#list-of-pending-play-promises
|
|
JS::MarkedVector<JS::NonnullGCPtr<WebIDL::Promise>> m_pending_play_promises;
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-paused
|
|
bool m_paused { true };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-volume
|
|
double m_volume { 1.0 };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-muted
|
|
bool m_muted { false };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-audiotracks
|
|
JS::GCPtr<AudioTrackList> m_audio_tracks;
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#dom-media-videotracks
|
|
JS::GCPtr<VideoTrackList> m_video_tracks;
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#media-data
|
|
ByteBuffer m_media_data;
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#can-autoplay-flag
|
|
bool m_can_autoplay { true };
|
|
|
|
// https://html.spec.whatwg.org/multipage/media.html#delaying-the-load-event-flag
|
|
Optional<DOM::DocumentLoadEventDelayer> m_delaying_the_load_event;
|
|
|
|
bool m_running_time_update_event_handler { false };
|
|
Optional<MonotonicTime> m_last_time_update_event_time;
|
|
|
|
JS::GCPtr<DOM::DocumentObserver> m_document_observer;
|
|
|
|
JS::GCPtr<SourceElementSelector> m_source_element_selector;
|
|
|
|
JS::GCPtr<Fetch::Infrastructure::FetchController> m_fetch_controller;
|
|
|
|
bool m_seek_in_progress = false;
|
|
|
|
// Cached state for layout.
|
|
Optional<MouseTrackingComponent> m_mouse_tracking_component;
|
|
bool m_tracking_mouse_position_while_playing { false };
|
|
Optional<CSSPixelPoint> m_mouse_position;
|
|
Optional<double> m_display_time;
|
|
mutable CachedLayoutBoxes m_layout_boxes;
|
|
};
|
|
|
|
}
|