diff --git a/flumi/Assets/Icons/pause.svg b/flumi/Assets/Icons/pause.svg new file mode 100644 index 0000000..f3948de --- /dev/null +++ b/flumi/Assets/Icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flumi/Assets/Icons/pause.svg.import b/flumi/Assets/Icons/pause.svg.import new file mode 100644 index 0000000..cc38674 --- /dev/null +++ b/flumi/Assets/Icons/pause.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cuucwb1qq2vog" +path="res://.godot/imported/pause.svg-b569a82cdc28c1abec0d4398e766838d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/pause.svg" +dest_files=["res://.godot/imported/pause.svg-b569a82cdc28c1abec0d4398e766838d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/flumi/Scenes/Tags/audio.tscn b/flumi/Scenes/Tags/audio.tscn index fe4a9f8..3b82063 100644 --- a/flumi/Scenes/Tags/audio.tscn +++ b/flumi/Scenes/Tags/audio.tscn @@ -7,10 +7,10 @@ [ext_resource type="Texture2D" uid="uid://dmktpcmm6klre" path="res://Assets/Icons/volume-2.svg" id="4_5j8mp"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xqesv"] -content_margin_left = 15.0 -content_margin_top = 15.0 -content_margin_right = 15.0 -content_margin_bottom = 15.0 +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 bg_color = Color(0.105882, 0.105882, 0.105882, 1) corner_radius_top_left = 15 corner_radius_top_right = 15 @@ -81,7 +81,7 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_xqesv") layout_mode = 2 [node name="Play" type="Button" parent="PanelContainer/HBoxContainer"] -custom_minimum_size = Vector2(45, 0) +custom_minimum_size = Vector2(45, 45) layout_mode = 2 mouse_default_cursor_shape = 2 theme_override_styles/focus = SubResource("StyleBoxEmpty_1hbfy") @@ -108,12 +108,9 @@ theme_override_icons/grabber_disabled = ExtResource("3_gfofq") theme_override_styles/slider = SubResource("StyleBoxFlat_1hbfy") theme_override_styles/grabber_area = SubResource("StyleBoxFlat_naep2") theme_override_styles/grabber_area_highlight = SubResource("StyleBoxFlat_ccpdr") -value = 50.0 -editable = false -scrollable = false [node name="Volume" type="Button" parent="PanelContainer/HBoxContainer"] -custom_minimum_size = Vector2(45, 0) +custom_minimum_size = Vector2(45, 45) layout_mode = 2 mouse_default_cursor_shape = 2 theme_override_styles/focus = SubResource("StyleBoxEmpty_1hbfy") @@ -138,7 +135,12 @@ theme_override_icons/grabber = ExtResource("3_gfofq") theme_override_styles/slider = SubResource("StyleBoxFlat_1hbfy") theme_override_styles/grabber_area = SubResource("StyleBoxFlat_naep2") theme_override_styles/grabber_area_highlight = SubResource("StyleBoxFlat_ccpdr") +value = 50.0 +[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] + +[connection signal="pressed" from="PanelContainer/HBoxContainer/Play" to="." method="_on_play_pressed"] +[connection signal="value_changed" from="PanelContainer/HBoxContainer/HSlider" to="." method="_on_progress_slider_value_changed"] [connection signal="mouse_entered" from="PanelContainer/HBoxContainer/Volume" to="." method="_on_volume_mouse_entered"] [connection signal="mouse_exited" from="PanelContainer/HBoxContainer/Volume" to="." method="_on_volume_mouse_exited"] [connection signal="pressed" from="PanelContainer/HBoxContainer/Volume" to="." method="_on_volume_pressed"] diff --git a/flumi/Scenes/main.tscn b/flumi/Scenes/main.tscn index 2f5039a..a72adce 100644 --- a/flumi/Scenes/main.tscn +++ b/flumi/Scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=28 format=3 uid="uid://bytm7bt2s4ak8"] +[gd_scene load_steps=27 format=3 uid="uid://bytm7bt2s4ak8"] [ext_resource type="Script" uid="uid://bg5iqnwic1rio" path="res://Scripts/main.gd" id="1_8q3xr"] [ext_resource type="Texture2D" uid="uid://df1m4j7uxi63v" path="res://Assets/Icons/chevron-down.svg" id="2_6bp64"] @@ -11,7 +11,6 @@ [ext_resource type="Texture2D" uid="uid://cehbtwq6gq0cn" path="res://Assets/Icons/plus.svg" id="5_ynf5e"] [ext_resource type="Script" uid="uid://bgqglerkcylxx" path="res://addons/SmoothScroll/SmoothScrollContainer.gd" id="10_d1ilt"] [ext_resource type="Script" uid="uid://b7h0k2h2qwlqv" path="res://addons/SmoothScroll/scroll_damper/expo_scroll_damper.gd" id="11_6iyac"] -[ext_resource type="PackedScene" uid="uid://b7w3dqcvof88f" path="res://Scenes/Tags/audio.tscn" id="12_6iyac"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_344ge"] @@ -223,9 +222,6 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_override_constants/separation = 22 -[node name="Audio" parent="VBoxContainer/ScrollContainer/WebsiteContainer" instance=ExtResource("12_6iyac")] -layout_mode = 2 - [node name="WebsiteBackground" type="Panel" parent="."] unique_name_in_owner = true z_index = -1 diff --git a/flumi/Scripts/B9/Lua.gd b/flumi/Scripts/B9/Lua.gd index 96ee6ad..843e46c 100644 --- a/flumi/Scripts/B9/Lua.gd +++ b/flumi/Scripts/B9/Lua.gd @@ -14,6 +14,7 @@ class EventSubscription: var connected_signal: String = "" var connected_node: Node = null var callback_func: Callable + var wrapper_func: Callable var dom_parser: HTMLParser var event_subscriptions: Dictionary = {} diff --git a/flumi/Scripts/Constants.gd b/flumi/Scripts/Constants.gd index b8df525..db50667 100644 --- a/flumi/Scripts/Constants.gd +++ b/flumi/Scripts/Constants.gd @@ -2657,6 +2657,141 @@ var HTML_CONTENT_TRANSFORM_TEST = """ """.to_utf8_buffer() -# Set the active HTML content to use the transform demo +var HTML_CONTENT_AUDIO_TEST = """ + Audio Tag Demo - HTML5 Audio Testing + + + + + + + + +

🎵 HTML5 Audio Tag Demo

+ + +
+

📀 Basic Audio Elements

+ +
+

Audio with Controls

+
Standard audio element with visible controls
+
<audio src="http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg" controls></audio>
+ +
+ +
+

Audio with Loop

+
Audio element that loops automatically
+
<audio src="http://commondatastorage.googleapis.com/codeskulptor-demos/DDR_assets/Kangaroo_MusiQue_-_The_Neverwritten_Role_Playing_Game.mp3" controls loop></audio>
+ +
+ +
+

Audio with Mute

+
Audio element that starts muted
+
<audio src="https://www.kozco.com/tech/LRMonoPhase4.wav" controls muted></audio>
+ +
+
+ + +
+

🎮 Lua Audio Control

+ +
+

DOM Audio Control

+
Control audio elements via Lua scripting
+ +
+ + + + + +
+
+local audio = gurt.select("#controlled-audio") +audio.play() +audio.volume = 0.5 +audio.loop = true +
+
+ +
+

Programmatic Audio

+
Create audio instances with Audio.new()
+
+ + + +
+
+
+local audio = Audio.new("http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg") +audio.volume = 0.3 +audio.play() +
+
+
+ +""".to_utf8_buffer() + +# Set the active HTML content to use the audio demo func _ready(): - HTML_CONTENT = HTML_CONTENT_TRANSFORM_TEST + HTML_CONTENT = HTML_CONTENT_AUDIO_TEST diff --git a/flumi/Scripts/Tags/audio.gd b/flumi/Scripts/Tags/audio.gd index ece40aa..385950a 100644 --- a/flumi/Scripts/Tags/audio.gd +++ b/flumi/Scripts/Tags/audio.gd @@ -1,31 +1,377 @@ +class_name HTMLAudio extends VBoxContainer @onready var popup_panel: PopupPanel = $PopupPanel +@onready var play_button: Button = $PanelContainer/HBoxContainer/Play +@onready var time_label: RichTextLabel = $PanelContainer/HBoxContainer/RichTextLabel +@onready var progress_slider: HSlider = $PanelContainer/HBoxContainer/HSlider @onready var volume_button: Button = $PanelContainer/HBoxContainer/Volume @onready var volume_slider: VSlider = $PopupPanel/VSlider +@onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer +const PLAY_ICON = preload("res://Assets/Icons/play.svg") +const PAUSE_ICON = preload("res://Assets/Icons/pause.svg") const VOLUME_OFF = preload("res://Assets/Icons/volume-off.svg") const VOLUME_2 = preload("res://Assets/Icons/volume-2.svg") +var current_element: HTMLParser.HTMLElement +var current_parser: HTMLParser var is_muted = false -var initial_volume_value = 0.0 +var initial_volume_value = 50.0 +var is_playing = false +var user_initiated_play = false +var progress_timer: Timer +var updating_slider = false +var in_user_click_context = false + +var volume: float = 0.5: + set(value): + volume = clamp(value, 0.0, 1.0) + if audio_player: + audio_player.volume_db = linear_to_db(volume) + if volume_slider: + volume_slider.value = volume * 100 + get: + return volume + +var loop: bool = false: + set(value): + loop = value + get: + return loop + +var muted: bool = false: + set(value): + muted = value + is_muted = value + update_volume_display() + get: + return is_muted func _ready(): - popup_panel.hide() - initial_volume_value = volume_slider.value + if popup_panel: + popup_panel.hide() + + if volume_slider: + initial_volume_value = volume_slider.value + volume = initial_volume_value / 100.0 + + if play_button: + play_button.icon = PLAY_ICON + + if volume_button: + volume_button.icon = VOLUME_2 + + # Set up audio player + if audio_player: + audio_player.finished.connect(_on_audio_finished) + + progress_timer = Timer.new() + progress_timer.wait_time = 0.5 + progress_timer.timeout.connect(_on_progress_timer_timeout) + add_child(progress_timer) + +func init(element: HTMLParser.HTMLElement, parser: HTMLParser) -> void: + current_element = element + current_parser = parser + + # Parse attributes + var src = element.get_attribute("src") + var controls = element.has_attribute("controls") + var loop_attr = element.has_attribute("loop") + var muted_attr = element.has_attribute("muted") + + if not controls: + visible = false + + if src.is_empty(): + return + + loop = loop_attr + if muted_attr: + muted = true + is_muted = true + update_volume_display() + + load_audio_async(src) + parser.register_dom_node(element, self) + +func load_audio_async(src: String) -> void: + if not is_inside_tree(): + await tree_entered + + reset_stream_state() + + if not src.begins_with("http"): + return + + var http_request = HTTPRequest.new() + add_child(http_request) + + http_request.download_chunk_size = 65536 + http_request.timeout = 30 + + http_request.request_completed.connect(_on_audio_download_completed) + var error = http_request.request(src) + + if error != OK: + http_request.queue_free() + return + +func _on_audio_download_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): + var http_request = get_children().filter(func(child): return child is HTTPRequest)[0] + http_request.queue_free() + + if response_code != 200: + return + + if body.size() == 0: + return + + var content_type = "" + for header in headers: + if header.to_lower().begins_with("content-type:"): + content_type = header.split(":")[1].strip_edges().to_lower() + break + + var audio_stream: AudioStream + + if "ogg" in content_type or "vorbis" in content_type: + audio_stream = AudioStreamOggVorbis.load_from_buffer(body) + if not audio_stream: + return + elif "wav" in content_type or "wave" in content_type: + audio_stream = AudioStreamWAV.new() + audio_stream.data = body + audio_stream.format = AudioStreamWAV.FORMAT_16_BITS + audio_stream.mix_rate = 44100 + audio_stream.stereo = true + audio_stream.loop_mode = AudioStreamWAV.LOOP_DISABLED + elif "mp3" in content_type or "mpeg" in content_type: + audio_stream = AudioStreamMP3.load_from_buffer(body) + if not audio_stream: + audio_stream = AudioStreamMP3.new() + audio_stream.data = body + else: + return + + if audio_stream: + if audio_player: + audio_player.stream = audio_stream + + on_stream_loaded() + +func reset_stream_state(): + cached_duration = -1.0 + stream_load_failed = false + +func on_stream_loaded(): + reset_stream_state() + + progress_slider.editable = true + progress_slider.scrollable = true + + update_duration_display() + + if volume_slider: + volume_slider.value = volume * 100 + + if is_muted: + update_volume_display() + +func update_duration_display(): + if time_label: + var duration = get_duration() + time_label.text = "00:00/" + format_time(duration) + +var last_progress_update: float = 0.0 + +func _on_progress_timer_timeout(): + if not is_playing or not audio_player.stream or updating_slider: + return + + var current_pos = audio_player.get_playback_position() + var total_length = audio_player.stream.get_length() + + var time_diff = abs(current_pos - last_progress_update) + if time_diff < 0.1: + return + + last_progress_update = current_pos + + if total_length > 0 and progress_slider: + updating_slider = true + progress_slider.value = (current_pos / total_length) * 100 + updating_slider = false + + if time_label: + time_label.text = format_time(current_pos) + "/" + format_time(total_length) + + +var cached_duration: float = -1.0 +var stream_load_failed: bool = false + +func get_stream_length_safe() -> float: + if not audio_player or not audio_player.stream: + return 0.0 + + if cached_duration > 0: + return cached_duration + + if stream_load_failed: + return 0.0 + + var length = 0.0 + if audio_player.stream.has_method("get_length"): + if audio_player.stream is AudioStreamOggVorbis: + var ogg_stream = audio_player.stream as AudioStreamOggVorbis + if not ogg_stream.packet_sequence: + stream_load_failed = true + return 0.0 + + length = audio_player.stream.get_length() + + if length <= 0 or is_nan(length) or is_inf(length): + if audio_player.stream is AudioStreamOggVorbis: + stream_load_failed = true + length = 0.0 + else: + cached_duration = length + + return length + +func format_time(seconds: float) -> String: + var mins = int(seconds / 60) + var secs = int(seconds) % 60 + return "%02d:%02d" % [mins, secs] + +func update_volume_display(): + if volume_button: + if is_muted: + volume_button.icon = VOLUME_OFF + else: + volume_button.icon = VOLUME_2 + + if audio_player: + if is_muted: + audio_player.volume_db = -80 + else: + audio_player.volume_db = linear_to_db(volume) + +func play() -> bool: + if not audio_player or not audio_player.stream: + return false + + if stream_load_failed: + return false + + if not user_initiated_play: + return false + + if audio_player.stream is AudioStreamOggVorbis: + var ogg_stream = audio_player.stream as AudioStreamOggVorbis + if not ogg_stream.packet_sequence or ogg_stream.packet_sequence.granule_positions.size() == 0: + stream_load_failed = true + return false + + if audio_player.stream_paused: + audio_player.stream_paused = false + else: + audio_player.play() + + is_playing = true + if visible and play_button: + play_button.icon = PAUSE_ICON + + if progress_timer: + progress_timer.start() + + return true + +func pause() -> void: + if audio_player: + audio_player.stream_paused = true + is_playing = false + if visible and play_button: + play_button.icon = PLAY_ICON + + progress_timer.stop() + +func stop() -> void: + if audio_player: + audio_player.stop() + is_playing = false + if visible and play_button: + play_button.icon = PLAY_ICON + + progress_timer.stop() + +func get_current_time() -> float: + if audio_player: + return audio_player.get_playback_position() + return 0.0 + +func get_duration() -> float: + if audio_player and audio_player.stream: + return audio_player.stream.get_length() + return 0.0 + +func set_current_time(time: float) -> void: + if audio_player and audio_player.stream: + if audio_player.stream_paused: + audio_player.stream_paused = false + audio_player.play(time) + audio_player.stream_paused = true + else: + audio_player.seek(time) + +func _on_play_pressed(): + user_initiated_play = true + if is_playing: + pause() + else: + play() + user_initiated_play = false + +func _on_progress_slider_value_changed(value: float): + if updating_slider: + return + + if audio_player and audio_player.stream and progress_slider.editable: + var total_length = audio_player.stream.get_length() + if total_length > 0: + var target_time = (value / 100.0) * total_length + set_current_time(target_time) + +func _on_audio_finished(): + if loop and audio_player: + audio_player.play() + else: + is_playing = false + + if visible and progress_slider: + progress_slider.value = 100 + if visible and time_label: + var total_length = get_duration() + time_label.text = format_time(total_length) + "/" + format_time(total_length) + + if visible and play_button: + play_button.icon = PLAY_ICON + + if progress_timer: + progress_timer.stop() func _on_volume_pressed(): is_muted = !is_muted + update_volume_display() - if is_muted: - volume_button.icon = VOLUME_OFF - else: - volume_button.icon = VOLUME_2 - - if popup_panel.is_visible(): + if popup_panel and popup_panel.is_visible(): popup_panel.hide() func _on_volume_mouse_entered(): + if not popup_panel or not volume_button: + return + if popup_panel.is_visible(): return @@ -36,6 +382,9 @@ func _on_volume_mouse_entered(): popup_panel.show() func _on_volume_mouse_exited(): + if not volume_slider or not popup_panel: + return + if volume_slider.value == initial_volume_value: await get_tree().create_timer(0.3).timeout @@ -48,8 +397,18 @@ func _on_volume_mouse_exited(): popup_panel.hide() func _on_popup_panel_focus_exited() -> void: - popup_panel.hide() - initial_volume_value = volume_slider.value + if popup_panel: + popup_panel.hide() + if volume_slider: + initial_volume_value = volume_slider.value func _on_volume_slider_value_changed(value: float) -> void: initial_volume_value = value + volume = value / 100.0 + if not is_muted: + update_volume_display() + +func _deferred_play_with_user_context(is_user_initiated: bool) -> void: + user_initiated_play = is_user_initiated + play() + user_initiated_play = false diff --git a/flumi/Scripts/Utils/Lua/Audio.gd b/flumi/Scripts/Utils/Lua/Audio.gd new file mode 100644 index 0000000..2751bc4 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Audio.gd @@ -0,0 +1,236 @@ +class_name LuaAudioUtils +extends RefCounted + +static var last_user_event_time: int = 0 +static var user_event_window_ms: int = 100 + +static func setup_audio_api(vm: LuauVM): + vm.lua_newtable() + vm.lua_pushcallable(_lua_audio_new_handler, "Audio.new") + vm.lua_setfield(-2, "new") + vm.lua_setglobal("Audio") + +static func mark_user_event(): + last_user_event_time = Time.get_ticks_msec() + +static func _check_if_likely_user_event(current_time: int) -> bool: + var time_since_user_event = current_time - last_user_event_time + return time_since_user_event < user_event_window_ms + +static func _defer_audio_setup(audio_node: HTMLAudio): + var main_scene = Engine.get_main_loop().current_scene + main_scene.add_child(audio_node) + audio_node.visible = false + + var element = audio_node.current_element + var src = element.get_attribute("src") + if not src.is_empty(): + audio_node.loop = element.has_attribute("loop") + if element.has_attribute("muted"): + audio_node.muted = true + audio_node.load_audio_async(src) + +static func _lua_audio_new_handler(vm: LuauVM) -> int: + var url: String = vm.luaL_checkstring(1) + + var audio_scene = preload("res://Scenes/Tags/audio.tscn") + var audio_node = audio_scene.instantiate() as HTMLAudio + + var dummy_element = HTMLParser.HTMLElement.new("audio") + dummy_element.set_attribute("src", url) + dummy_element.set_attribute("controls", "false") + + audio_node.current_element = dummy_element + audio_node.current_parser = null + audio_node.visible = false + + audio_node.set_meta("deferred_url", url) + + _defer_audio_setup.call_deferred(audio_node) + + vm.lua_newtable() + + vm.lua_pushobject(audio_node) + vm.lua_setfield(-2, "_audio_node") + + vm.lua_pushcallable(_lua_audio_play_handler, "Audio.play") + vm.lua_setfield(-2, "play") + + vm.lua_pushcallable(_lua_audio_pause_handler, "Audio.pause") + vm.lua_setfield(-2, "pause") + + vm.lua_pushcallable(_lua_audio_stop_handler, "Audio.stop") + vm.lua_setfield(-2, "stop") + + # Set up metatable for property access + vm.lua_newtable() + vm.lua_pushcallable(_lua_audio_index_handler, "Audio.__index") + vm.lua_setfield(-2, "__index") + vm.lua_pushcallable(_lua_audio_newindex_handler, "Audio.__newindex") + vm.lua_setfield(-2, "__newindex") + vm.lua_setmetatable(-2) + + return 1 + +static func _get_audio_node_from_table(vm: LuauVM) -> HTMLAudio: + vm.lua_getfield(1, "_audio_node") + var audio_node = vm.lua_toobject(-1) as HTMLAudio + vm.lua_pop(1) + return audio_node + +static func _lua_audio_play_handler(vm: LuauVM) -> int: + var audio_node = _get_audio_node_from_table(vm) + if audio_node: + var current_time = Time.get_ticks_msec() + var is_likely_user_event = _check_if_likely_user_event(current_time) + audio_node.call_deferred("_deferred_play_with_user_context", is_likely_user_event) + vm.lua_pushboolean(true) + else: + vm.lua_pushboolean(false) + return 1 + +static func _lua_audio_pause_handler(vm: LuauVM) -> int: + var audio_node = _get_audio_node_from_table(vm) + if audio_node: + audio_node.call_deferred("pause") + return 0 + +static func _lua_audio_stop_handler(vm: LuauVM) -> int: + var audio_node = _get_audio_node_from_table(vm) + if audio_node: + audio_node.call_deferred("stop") + return 0 + +# Property access handlers for programmatic audio +static func _lua_audio_index_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var key: String = vm.luaL_checkstring(2) + + var audio_node = _get_audio_node_from_table(vm) + if not audio_node: + vm.lua_pushnil() + return 1 + + match key: + "volume": + vm.lua_pushnumber(audio_node.volume) + return 1 + "loop": + vm.lua_pushboolean(audio_node.loop) + return 1 + "currentTime": + vm.lua_pushnumber(audio_node.get_current_time()) + return 1 + "duration": + vm.lua_pushnumber(audio_node.get_duration()) + return 1 + _: + # Look up other methods/properties in the table itself + vm.lua_rawget(1) + return 1 + +static func _lua_audio_newindex_handler(vm: LuauVM) -> int: + vm.luaL_checktype(1, vm.LUA_TTABLE) + var key: String = vm.luaL_checkstring(2) + var value = vm.lua_tovariant(3) + + var audio_node = _get_audio_node_from_table(vm) + if not audio_node: + return 0 + + match key: + "volume": + audio_node.call_deferred("set", "volume", float(value)) + return 0 + "loop": + audio_node.call_deferred("set", "loop", bool(value)) + return 0 + "currentTime": + audio_node.call_deferred("set_current_time", float(value)) + return 0 + _: + vm.lua_rawset(1) + return 0 + +static func _dom_audio_play_handler(vm: LuauVM) -> int: + var element_id: String = vm.luaL_checkstring(1) + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + mark_user_event() + var audio_node = _get_dom_audio_node(element_id, lua_api) + if audio_node: + audio_node.call_deferred("_deferred_play_with_user_context", true) + return 0 + +static func _dom_audio_pause_handler(vm: LuauVM) -> int: + var element_id: String = vm.luaL_checkstring(1) + var lua_api = vm.get_meta("lua_api") as LuaAPI + if not lua_api: + return 0 + + var audio_node = _get_dom_audio_node(element_id, lua_api) + if audio_node: + audio_node.call_deferred("pause") + return 0 + +static func _get_dom_audio_node(element_id: String, lua_api) -> HTMLAudio: + return lua_api.dom_parser.parse_result.dom_nodes.get(element_id, null) as HTMLAudio + +static func handle_dom_audio_index(vm: LuauVM, element_id: String, key: String) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + var audio_node = _get_dom_audio_node(element_id, lua_api) + if not audio_node: + vm.lua_pushnil() + return 1 + + match key: + "play": + var play_code = "return function(self) _dom_audio_play('" + element_id + "') end" + vm.load_string(play_code, "audio.play_closure") + if vm.lua_pcall(0, 1, 0) == vm.LUA_OK: + return 1 + else: + vm.lua_pop(1) + vm.lua_pushnil() + return 1 + "pause": + var pause_code = "return function(self) _dom_audio_pause('" + element_id + "') end" + vm.load_string(pause_code, "audio.pause_closure") + if vm.lua_pcall(0, 1, 0) == vm.LUA_OK: + return 1 + else: + vm.lua_pop(1) + vm.lua_pushnil() + return 1 + "volume": + vm.lua_pushnumber(audio_node.volume) + return 1 + "loop": + vm.lua_pushboolean(audio_node.loop) + return 1 + "currentTime": + vm.lua_pushnumber(audio_node.get_current_time()) + return 1 + "duration": + vm.lua_pushnumber(audio_node.get_duration()) + return 1 + + return 0 + +static func handle_dom_audio_newindex(vm: LuauVM, element_id: String, key: String, value: Variant) -> int: + var lua_api = vm.get_meta("lua_api") as LuaAPI + var audio_node = _get_dom_audio_node(element_id, lua_api) + if not audio_node: + return 0 + + match key: + "volume": + audio_node.call_deferred("set", "volume", float(value)) + "loop": + audio_node.call_deferred("set", "loop", bool(value)) + "currentTime": + audio_node.call_deferred("set_current_time", float(value)) + + return 0 diff --git a/flumi/Scripts/Utils/Lua/Audio.gd.uid b/flumi/Scripts/Utils/Lua/Audio.gd.uid new file mode 100644 index 0000000..18c76f2 --- /dev/null +++ b/flumi/Scripts/Utils/Lua/Audio.gd.uid @@ -0,0 +1 @@ +uid://defnka61xoi8d diff --git a/flumi/Scripts/Utils/Lua/DOM.gd b/flumi/Scripts/Utils/Lua/DOM.gd index 75e2680..6c71831 100644 --- a/flumi/Scripts/Utils/Lua/DOM.gd +++ b/flumi/Scripts/Utils/Lua/DOM.gd @@ -477,6 +477,12 @@ static func create_element_wrapper(vm: LuauVM, element: HTMLParser.HTMLElement, static func add_element_methods(vm: LuauVM, lua_api: LuaAPI) -> void: vm.set_meta("lua_api", lua_api) + vm.lua_pushcallable(LuaAudioUtils._dom_audio_play_handler, "_dom_audio_play") + vm.lua_setglobal("_dom_audio_play") + + vm.lua_pushcallable(LuaAudioUtils._dom_audio_pause_handler, "_dom_audio_pause") + vm.lua_setglobal("_dom_audio_pause") + vm.lua_pushcallable(LuaDOMUtils._element_on_wrapper, "element.on") vm.lua_setfield(-2, "on") @@ -827,10 +833,20 @@ static func _element_index_wrapper(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) var key: String = vm.luaL_checkstring(2) + var lua_api = vm.get_meta("lua_api") as LuaAPI + + vm.lua_getfield(1, "_tag_name") + var tag_name: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + if tag_name == "audio": + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + return LuaAudioUtils.handle_dom_audio_index(vm, element_id, key) + match key: "text": - # Get lua_api from VM metadata - var lua_api = vm.get_meta("lua_api") as LuaAPI if lua_api: # Get element ID and find the element vm.lua_getfield(1, "_element_id") @@ -846,8 +862,6 @@ static func _element_index_wrapper(vm: LuauVM) -> int: vm.lua_pushstring("") return 1 "children": - # Get lua_api from VM metadata - var lua_api = vm.get_meta("lua_api") as LuaAPI if lua_api: # Get element ID and find the element vm.lua_getfield(1, "_element_id") @@ -870,7 +884,6 @@ static func _element_index_wrapper(vm: LuauVM) -> int: return 1 _: # Check for DOM traversal properties first - var lua_api = vm.get_meta("lua_api") as LuaAPI if lua_api: match key: "parent": @@ -991,6 +1004,16 @@ static func _element_newindex_wrapper(vm: LuauVM) -> int: var key: String = vm.luaL_checkstring(2) var value = vm.lua_tovariant(3) + vm.lua_getfield(1, "_tag_name") + var tag_name: String = vm.lua_tostring(-1) + vm.lua_pop(1) + + if tag_name == "audio": + vm.lua_getfield(1, "_element_id") + var element_id: String = vm.lua_tostring(-1) + vm.lua_pop(1) + return LuaAudioUtils.handle_dom_audio_newindex(vm, element_id, key, value) + match key: "text": var text: String = str(value) # Convert value to string diff --git a/flumi/Scripts/Utils/Lua/Event.gd b/flumi/Scripts/Utils/Lua/Event.gd index fa00c98..f345978 100644 --- a/flumi/Scripts/Utils/Lua/Event.gd +++ b/flumi/Scripts/Utils/Lua/Event.gd @@ -14,14 +14,22 @@ static func connect_element_event(signal_node: Node, event_name: String, subscri match event_name: "click": if signal_node.has_signal("pressed"): - signal_node.pressed.connect(subscription.lua_api._on_event_triggered.bind(subscription)) + var wrapper = func(): + LuaAudioUtils.mark_user_event() + subscription.lua_api._on_event_triggered(subscription) + signal_node.pressed.connect(wrapper) subscription.connected_signal = "pressed" subscription.connected_node = signal_node if signal_node != subscription.lua_api.get_dom_node(signal_node.get_parent(), "signal") else null + subscription.wrapper_func = wrapper return true elif signal_node is Control: - signal_node.gui_input.connect(subscription.lua_api._on_gui_input_click.bind(subscription)) + var wrapper = func(event: InputEvent): + LuaAudioUtils.mark_user_event() + subscription.lua_api._on_gui_input_click(subscription, event) + signal_node.gui_input.connect(wrapper) subscription.connected_signal = "gui_input" subscription.connected_node = signal_node + subscription.wrapper_func = wrapper return true "mousedown", "mouseup": if signal_node is Control: @@ -212,10 +220,16 @@ static func disconnect_subscription(subscription, lua_api) -> void: match subscription.connected_signal: "pressed": if target_node.has_signal("pressed"): - target_node.pressed.disconnect(lua_api._on_event_triggered.bind(subscription)) + if subscription.has("wrapper_func") and subscription.wrapper_func: + target_node.pressed.disconnect(subscription.wrapper_func) + else: + target_node.pressed.disconnect(lua_api._on_event_triggered.bind(subscription)) "gui_input": if target_node.has_signal("gui_input"): - target_node.gui_input.disconnect(lua_api._on_gui_input_click.bind(subscription)) + if subscription.has("wrapper_func") and subscription.wrapper_func: + target_node.gui_input.disconnect(subscription.wrapper_func) + else: + target_node.gui_input.disconnect(lua_api._on_gui_input_click.bind(subscription)) "gui_input_mouse": if target_node.has_signal("gui_input"): target_node.gui_input.disconnect(lua_api._on_gui_input_mouse_universal.bind(target_node)) diff --git a/flumi/Scripts/Utils/Lua/ThreadedVM.gd b/flumi/Scripts/Utils/Lua/ThreadedVM.gd index e3e7d3e..a39c038 100644 --- a/flumi/Scripts/Utils/Lua/ThreadedVM.gd +++ b/flumi/Scripts/Utils/Lua/ThreadedVM.gd @@ -333,6 +333,7 @@ func _setup_additional_lua_apis(): LuaNetworkUtils.setup_network_api(lua_vm) LuaJSONUtils.setup_json_api(lua_vm) LuaWebSocketUtils.setup_websocket_api(lua_vm) + LuaAudioUtils.setup_audio_api(lua_vm) func _threaded_table_tostring_handler(vm: LuauVM) -> int: vm.luaL_checktype(1, vm.LUA_TTABLE) diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd index 2c9efe9..197de52 100644 --- a/flumi/Scripts/main.gd +++ b/flumi/Scripts/main.gd @@ -28,6 +28,7 @@ const SELECT = preload("res://Scenes/Tags/select.tscn") const OPTION = preload("res://Scenes/Tags/option.tscn") const TEXTAREA = preload("res://Scenes/Tags/textarea.tscn") const DIV = preload("res://Scenes/Tags/div.tscn") +const AUDIO = preload("res://Scenes/Tags/audio.tscn") const MIN_SIZE = Vector2i(750, 200) @@ -126,7 +127,7 @@ func render() -> void: var inline_node = await create_element_node(inline_element, parser) if inline_node: # Input elements register their own DOM nodes in their init() function - if inline_element.tag_name not in ["input", "textarea", "select", "button"]: + if inline_element.tag_name not in ["input", "textarea", "select", "button", "audio"]: parser.register_dom_node(inline_element, inline_node) safe_add_child(hbox, inline_node) @@ -142,7 +143,7 @@ func render() -> void: var element_node = await create_element_node(element, parser) if element_node: # Input elements register their own DOM nodes in their init() function - if element.tag_name not in ["input", "textarea", "select", "button"]: + if element.tag_name not in ["input", "textarea", "select", "button", "audio"]: parser.register_dom_node(element, element_node) # ul/ol handle their own adding @@ -279,7 +280,7 @@ func create_element_node(element: HTMLParser.HTMLElement, parser: HTMLParser) -> var child_node = await create_element_node(child_element, parser) if child_node and is_instance_valid(container_for_children): # Input elements register their own DOM nodes in their init() function - if child_element.tag_name not in ["input", "textarea", "select", "button"]: + if child_element.tag_name not in ["input", "textarea", "select", "button", "audio"]: parser.register_dom_node(child_element, child_node) safe_add_child(container_for_children, child_node) @@ -360,6 +361,9 @@ func create_element_node_internal(element: HTMLParser.HTMLElement, parser: HTMLP "textarea": node = TEXTAREA.instantiate() node.init(element, parser) + "audio": + node = AUDIO.instantiate() + node.init(element, parser) "div": var styles = parser.get_element_styles_with_inheritance(element, "", []) var hover_styles = parser.get_element_styles_with_inheritance(element, "hover", [])