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
+
+
+ ▶️ Play
+ ⏸️ Pause
+ 🔉 Vol 30%
+ 🔊 Vol 70%
+ 🔁 Toggle Loop
+
+
+local audio = gurt.select("#controlled-audio")
+audio.play()
+audio.volume = 0.5
+audio.loop = true
+
+
+
+
+
Programmatic Audio
+
Create audio instances with Audio.new()
+
+ 🎵 Create Audio
+ ▶️ Play
+ ⏸️ Pause
+
+
+
+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", [])