This commit is contained in:
Face
2025-08-11 17:08:39 +03:00
parent 0ca930ed93
commit 513b0bf83a
13 changed files with 849 additions and 39 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pause-icon lucide-pause"><rect x="14" y="3" width="5" height="18" rx="1"/><rect x="5" y="3" width="5" height="18" rx="1"/></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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 = {}

View File

@@ -2657,6 +2657,141 @@ var HTML_CONTENT_TRANSFORM_TEST = """<head>
</body>
""".to_utf8_buffer()
# Set the active HTML content to use the transform demo
var HTML_CONTENT_AUDIO_TEST = """<head>
<title>Audio Tag Demo - HTML5 Audio Testing</title>
<icon src="https://picsum.photos/32/32?random=audio">
<meta name="theme-color" content="#10b981">
<meta name="description" content="Testing HTML5 audio functionality">
<style>
body { bg-[#f0f9ff] p-8 font-family-system }
h1 { text-[#10b981] text-4xl font-bold text-center mb-8 }
h2 { text-[#059669] text-2xl font-semibold mb-4 }
h3 { text-[#047857] text-xl font-semibold mb-3 }
.section { bg-white p-6 rounded-xl shadow-lg mb-6 }
.audio-demo { mb-4 p-4 bg-[#f0fdf4] border border-[#bbf7d0] rounded-lg }
.code-block { bg-[#1e293b] text-[#e2e8f0] p-3 rounded font-mono text-sm mb-3 }
.description { text-[#475569] mb-3 }
.btn { bg-[#10b981] text-white px-4 py-2 rounded hover:bg-[#059669] active:bg-[#047857] cursor-pointer }
.control-group { flex gap-4 items-center mb-3 }
</style>
<script>
print('hi')
local programmaticAudio = nil
local controlledAudio = gurt.select("#controlled-audio")
local status = gurt.select("#programmatic-status")
gurt.select("#play-btn"):on("click", function()
controlledAudio:play()
end)
gurt.select("#pause-btn"):on("click", function()
controlledAudio:pause()
end)
gurt.select("#vol-low-btn"):on("click", function()
controlledAudio.volume = 0.3
end)
local test = gurt.setTimeout(function()
print('removed')
programmaticAudio:play()
end, 3000)
gurt.select("#vol-high-btn"):on("click", function()
controlledAudio.volume = 0.7
end)
gurt.select("#toggle-loop-btn"):on("click", function()
controlledAudio.loop = false
end)
gurt.select("#create-audio-btn"):on("click", function()
programmaticAudio = Audio.new("http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg")
programmaticAudio.volume = 0.3
end)
gurt.select("#play-prog-btn"):on("click", function()
programmaticAudio:play()
end)
gurt.select("#pause-prog-btn"):on("click", function()
programmaticAudio:pause()
end)
</script>
</head>
<body>
<h1>🎵 HTML5 Audio Tag Demo</h1>
<!-- Basic Audio Elements -->
<div style="section">
<h2>📀 Basic Audio Elements</h2>
<div style="audio-demo">
<h3>Audio with Controls</h3>
<div style="description">Standard audio element with visible controls</div>
<div style="code-block">&lt;audio src="http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg" controls&gt;&lt;/audio&gt;</div>
<audio src="http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg" controls="true"></audio>
</div>
<div style="audio-demo">
<h3>Audio with Loop</h3>
<div style="description">Audio element that loops automatically</div>
<div style="code-block">&lt;audio src="http://commondatastorage.googleapis.com/codeskulptor-demos/DDR_assets/Kangaroo_MusiQue_-_The_Neverwritten_Role_Playing_Game.mp3" controls loop&gt;&lt;/audio&gt;</div>
<audio src="http://commondatastorage.googleapis.com/codeskulptor-demos/DDR_assets/Kangaroo_MusiQue_-_The_Neverwritten_Role_Playing_Game.mp3" controls="true" loop="true"></audio>
</div>
<div style="audio-demo">
<h3>Audio with Mute</h3>
<div style="description">Audio element that starts muted</div>
<div style="code-block">&lt;audio src="https://www.kozco.com/tech/LRMonoPhase4.wav" controls muted&gt;&lt;/audio&gt;</div>
<audio src="https://www.kozco.com/tech/LRMonoPhase4.wav" controls="true" muted="true"></audio>
</div>
</div>
<!-- Lua Audio Control -->
<div style="section">
<h2>🎮 Lua Audio Control</h2>
<div style="audio-demo">
<h3>DOM Audio Control</h3>
<div style="description">Control audio elements via Lua scripting</div>
<audio id="controlled-audio" src="http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg" controls="true"></audio>
<div style="control-group">
<button id="play-btn" style="btn">▶️ Play</button>
<button id="pause-btn" style="btn">⏸️ Pause</button>
<button id="vol-low-btn" style="btn">🔉 Vol 30%</button>
<button id="vol-high-btn" style="btn">🔊 Vol 70%</button>
<button id="toggle-loop-btn" style="btn">🔁 Toggle Loop</button>
</div>
<div style="code-block">
local audio = gurt.select("#controlled-audio")
audio.play()
audio.volume = 0.5
audio.loop = true
</div>
</div>
<div style="audio-demo">
<h3>Programmatic Audio</h3>
<div style="description">Create audio instances with Audio.new()</div>
<div style="control-group">
<button id="create-audio-btn" style="btn">🎵 Create Audio</button>
<button id="play-prog-btn" style="btn">▶️ Play</button>
<button id="pause-prog-btn" style="btn">⏸️ Pause</button>
</div>
<div id="programmatic-status" style="text-[#059669] font-semibold"></div>
<div style="code-block">
local audio = Audio.new("http://commondatastorage.googleapis.com/codeskulptor-assets/Evillaugh.ogg")
audio.volume = 0.3
audio.play()
</div>
</div>
</div>
</body>
""".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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
uid://defnka61xoi8d

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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", [])