refactor: split long functions and fix AGENTS.md violations
All checks were successful
Build plugin / build (push) Successful in 1m8s
All checks were successful
Build plugin / build (push) Successful in 1m8s
- Rename goos/goarch to os_name/arch_name (no domain jargon) - Split _install_ffmpeg into _detect_os, _detect_arch, _build_ffmpeg_url, _download_url_bytes, _extract_ffmpeg - Split _build_animated_gif_texture into _extract_gif_frames and _assemble_animated_texture - Split _parse_gif_frame_delays into _skip_gif_header, _parse_gif_extension, _skip_gif_image_descriptor - Replace _download_gif_bytes with _download_url_bytes (shared helper) - All functions now under 50 lines
This commit is contained in:
parent
208dc58f28
commit
3f5c45a99d
1 changed files with 106 additions and 114 deletions
|
|
@ -260,32 +260,52 @@ func _ensure_ffmpeg() -> String:
|
||||||
## the same way the itch plugin's butler installer does, then fetches
|
## the same way the itch plugin's butler installer does, then fetches
|
||||||
## the matching BtbN pre-built binary.
|
## the matching BtbN pre-built binary.
|
||||||
func _install_ffmpeg() -> bool:
|
func _install_ffmpeg() -> bool:
|
||||||
var goos := "linux"
|
var os_name := _detect_os()
|
||||||
var goarch := "64"
|
var arch_name := _detect_arch()
|
||||||
|
var archive_url := _build_ffmpeg_url(os_name, arch_name)
|
||||||
|
var body := await _download_url_bytes(archive_url)
|
||||||
|
if body.is_empty():
|
||||||
|
return false
|
||||||
|
return _extract_ffmpeg(body, os_name)
|
||||||
|
|
||||||
|
|
||||||
|
## Detects the current OS as a short name for ffmpeg builds.
|
||||||
|
func _detect_os() -> String:
|
||||||
if OS.get_name() == "Windows":
|
if OS.get_name() == "Windows":
|
||||||
goos = "win"
|
return "win"
|
||||||
if OS.get_name() == "macOS":
|
if OS.get_name() == "macOS":
|
||||||
goos = "macos"
|
return "macos"
|
||||||
|
return "linux"
|
||||||
|
|
||||||
|
|
||||||
|
## Detects the CPU architecture as a short name for ffmpeg builds.
|
||||||
|
func _detect_arch() -> String:
|
||||||
if Engine.has_method("get_architecture_name"):
|
if Engine.has_method("get_architecture_name"):
|
||||||
var arch: String = Engine.get_architecture_name()
|
var arch: String = Engine.get_architecture_name()
|
||||||
if "arm64" in arch or "aarch64" in arch:
|
if "arm64" in arch or "aarch64" in arch:
|
||||||
goarch = "arm64"
|
return "arm64"
|
||||||
|
return "64"
|
||||||
|
|
||||||
var platform_slug := goos + goarch
|
|
||||||
|
## Builds the download URL for the ffmpeg archive matching the given platform.
|
||||||
|
func _build_ffmpeg_url(os_name: String, arch_name: String) -> String:
|
||||||
|
var platform_slug := os_name + arch_name
|
||||||
var archive_name := "ffmpeg-master-latest-%s-gpl" % platform_slug
|
var archive_name := "ffmpeg-master-latest-%s-gpl" % platform_slug
|
||||||
var is_win: bool = goos == "win"
|
var ext := "zip" if os_name == "win" else "tar.xz"
|
||||||
var archive_url := "%s/%s.%s" % [FFMPEG_BUILDS_URL, archive_name,
|
return "%s/%s.%s" % [FFMPEG_BUILDS_URL, archive_name, ext]
|
||||||
"zip" if is_win else "tar.xz"]
|
|
||||||
|
|
||||||
|
|
||||||
|
## Downloads bytes from a URL. Returns empty PackedByteArray on failure.
|
||||||
|
func _download_url_bytes(url: String) -> PackedByteArray:
|
||||||
var http := HTTPRequest.new()
|
var http := HTTPRequest.new()
|
||||||
add_child.call_deferred(http)
|
add_child.call_deferred(http)
|
||||||
await http.ready
|
await http.ready
|
||||||
|
|
||||||
if http.request(archive_url) != OK:
|
if http.request(url) != OK:
|
||||||
logger.error("Error requesting ffmpeg archive: " + archive_url)
|
logger.error("Error requesting: " + url)
|
||||||
remove_child(http)
|
remove_child(http)
|
||||||
http.queue_free()
|
http.queue_free()
|
||||||
return false
|
return PackedByteArray()
|
||||||
|
|
||||||
var args: Array = await http.request_completed
|
var args: Array = await http.request_completed
|
||||||
var result: int = args[0]
|
var result: int = args[0]
|
||||||
|
|
@ -295,11 +315,16 @@ func _install_ffmpeg() -> bool:
|
||||||
http.queue_free()
|
http.queue_free()
|
||||||
|
|
||||||
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
|
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
|
||||||
logger.error("ffmpeg download failed: HTTP %d for %s" % [response_code, archive_url])
|
logger.error("Download failed: HTTP %d for %s" % [response_code, url])
|
||||||
return false
|
return PackedByteArray()
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
## Extracts the ffmpeg binary from the downloaded archive.
|
||||||
|
func _extract_ffmpeg(body: PackedByteArray, os_name: String) -> bool:
|
||||||
var globalized_dir := ProjectSettings.globalize_path(BOXART_DIR)
|
var globalized_dir := ProjectSettings.globalize_path(BOXART_DIR)
|
||||||
DirAccess.make_dir_recursive_absolute(globalized_dir)
|
DirAccess.make_dir_recursive_absolute(globalized_dir)
|
||||||
|
var is_win: bool = os_name == "win"
|
||||||
var ext := "zip" if is_win else "tar.xz"
|
var ext := "zip" if is_win else "tar.xz"
|
||||||
var archive_path := globalized_dir + "/ffmpeg." + ext
|
var archive_path := globalized_dir + "/ffmpeg." + ext
|
||||||
var file := FileAccess.open(archive_path, FileAccess.WRITE)
|
var file := FileAccess.open(archive_path, FileAccess.WRITE)
|
||||||
|
|
@ -388,53 +413,7 @@ func _fetch_gif_as_texture(url: String, cache_flags: int) -> Texture2D:
|
||||||
|
|
||||||
## Downloads GIF bytes from the given URL. Returns empty PackedByteArray on failure.
|
## Downloads GIF bytes from the given URL. Returns empty PackedByteArray on failure.
|
||||||
func _download_gif_bytes(url: String) -> PackedByteArray:
|
func _download_gif_bytes(url: String) -> PackedByteArray:
|
||||||
var http := HTTPClient.new()
|
return await _download_url_bytes(url)
|
||||||
var parts := url.split("/")
|
|
||||||
var use_tls := parts[0] == "https:"
|
|
||||||
var err := http.connect_to_host(parts[2], 443 if use_tls else 80, TLSOptions.client() if use_tls else null)
|
|
||||||
if err != OK:
|
|
||||||
logger.warn("GIF fetch: HTTP connect failed for %s: %d" % [url.left(LOG_URL_MAX_LEN), err])
|
|
||||||
return PackedByteArray()
|
|
||||||
var deadline := Time.get_ticks_msec() + HTTP_TIMEOUT_MS
|
|
||||||
while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
|
|
||||||
http.poll()
|
|
||||||
if Time.get_ticks_msec() > deadline:
|
|
||||||
http.close()
|
|
||||||
logger.warn("GIF fetch: connect timed out")
|
|
||||||
return PackedByteArray()
|
|
||||||
OS.delay_msec(CONNECT_POLL_DELAY_MS)
|
|
||||||
if http.get_status() != HTTPClient.STATUS_CONNECTED:
|
|
||||||
logger.warn("GIF fetch: not connected (status=%d)" % http.get_status())
|
|
||||||
http.close()
|
|
||||||
return PackedByteArray()
|
|
||||||
|
|
||||||
http.request(HTTPClient.METHOD_GET, "/" + "/".join(parts.slice(3)), PackedStringArray())
|
|
||||||
deadline = Time.get_ticks_msec() + HTTP_TIMEOUT_MS
|
|
||||||
while http.get_status() == HTTPClient.STATUS_REQUESTING:
|
|
||||||
http.poll()
|
|
||||||
if Time.get_ticks_msec() > deadline:
|
|
||||||
http.close()
|
|
||||||
logger.warn("GIF fetch: request timed out")
|
|
||||||
return PackedByteArray()
|
|
||||||
OS.delay_msec(READ_POLL_DELAY_MS)
|
|
||||||
|
|
||||||
var body := PackedByteArray()
|
|
||||||
while http.get_status() == HTTPClient.STATUS_BODY:
|
|
||||||
http.poll()
|
|
||||||
var chunk: PackedByteArray = http.read_response_body_chunk()
|
|
||||||
if chunk.is_empty():
|
|
||||||
if Time.get_ticks_msec() > deadline:
|
|
||||||
http.close()
|
|
||||||
logger.warn("GIF fetch: body read timed out")
|
|
||||||
return PackedByteArray()
|
|
||||||
OS.delay_msec(READ_POLL_DELAY_MS)
|
|
||||||
continue
|
|
||||||
body.append_array(chunk)
|
|
||||||
http.close()
|
|
||||||
|
|
||||||
if body.is_empty():
|
|
||||||
logger.warn("GIF fetch: empty response for %s" % url.left(LOG_URL_MAX_LEN))
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
## Extracts the first frame of a GIF using ffmpeg. Returns a static ImageTexture.
|
## Extracts the first frame of a GIF using ffmpeg. Returns a static ImageTexture.
|
||||||
|
|
@ -465,10 +444,15 @@ func _build_static_gif_texture(ffmpeg_bin: String, gif_path: String, png_path: S
|
||||||
## Extracts all frames from a GIF using ffmpeg and builds an AnimatedTexture.
|
## Extracts all frames from a GIF using ffmpeg and builds an AnimatedTexture.
|
||||||
## The animation is paused by default — the focus handler unpauses it.
|
## The animation is paused by default — the focus handler unpauses it.
|
||||||
func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_dir: String, gif_bytes: PackedByteArray, url: String) -> AnimatedTexture:
|
func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_dir: String, gif_bytes: PackedByteArray, url: String) -> AnimatedTexture:
|
||||||
# Parse frame delays from the GIF binary.
|
|
||||||
var delays: Array = _parse_gif_frame_delays(gif_bytes)
|
var delays: Array = _parse_gif_frame_delays(gif_bytes)
|
||||||
|
var frame_files: PackedStringArray = _extract_gif_frames(ffmpeg_bin, gif_path, frames_dir)
|
||||||
|
if frame_files.is_empty():
|
||||||
|
return null
|
||||||
|
return _assemble_animated_texture(frame_files, frames_dir, delays, url)
|
||||||
|
|
||||||
# Extract all frames with ffmpeg.
|
|
||||||
|
## Extracts all frames from a GIF file using ffmpeg. Returns sorted frame filenames.
|
||||||
|
func _extract_gif_frames(ffmpeg_bin: String, gif_path: String, frames_dir: String) -> PackedStringArray:
|
||||||
DirAccess.make_dir_recursive_absolute(frames_dir)
|
DirAccess.make_dir_recursive_absolute(frames_dir)
|
||||||
var ffmpeg_args: PackedStringArray = [
|
var ffmpeg_args: PackedStringArray = [
|
||||||
"-y", "-i", gif_path,
|
"-y", "-i", gif_path,
|
||||||
|
|
@ -478,11 +462,10 @@ func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_di
|
||||||
var ret: Array = []
|
var ret: Array = []
|
||||||
OS.execute(ffmpeg_bin, ffmpeg_args, ret)
|
OS.execute(ffmpeg_bin, ffmpeg_args, ret)
|
||||||
|
|
||||||
# Collect extracted frame files.
|
|
||||||
var dir := DirAccess.open(frames_dir)
|
var dir := DirAccess.open(frames_dir)
|
||||||
if dir == null:
|
if dir == null:
|
||||||
logger.warn("GIF fetch: cannot open frames dir")
|
logger.warn("GIF fetch: cannot open frames dir")
|
||||||
return null
|
return PackedStringArray()
|
||||||
dir.list_dir_begin()
|
dir.list_dir_begin()
|
||||||
var frame_files: PackedStringArray = []
|
var frame_files: PackedStringArray = []
|
||||||
var fname := dir.get_next()
|
var fname := dir.get_next()
|
||||||
|
|
@ -492,13 +475,11 @@ func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_di
|
||||||
fname = dir.get_next()
|
fname = dir.get_next()
|
||||||
dir.list_dir_end()
|
dir.list_dir_end()
|
||||||
frame_files.sort()
|
frame_files.sort()
|
||||||
|
return frame_files
|
||||||
|
|
||||||
if frame_files.is_empty():
|
|
||||||
logger.warn("GIF fetch: no frames extracted for %s" % url.left(LOG_URL_MAX_LEN))
|
|
||||||
DirAccess.remove_absolute(frames_dir)
|
|
||||||
return null
|
|
||||||
|
|
||||||
# Build AnimatedTexture.
|
## Loads extracted frame images and assembles them into an AnimatedTexture.
|
||||||
|
func _assemble_animated_texture(frame_files: PackedStringArray, frames_dir: String, delays: Array, url: String) -> AnimatedTexture:
|
||||||
var anim_tex := AnimatedTexture.new()
|
var anim_tex := AnimatedTexture.new()
|
||||||
anim_tex.frames_count = frame_files.size()
|
anim_tex.frames_count = frame_files.size()
|
||||||
anim_tex.pause = true # Start paused — focus handler unpauses.
|
anim_tex.pause = true # Start paused — focus handler unpauses.
|
||||||
|
|
@ -511,7 +492,6 @@ func _build_animated_gif_texture(ffmpeg_bin: String, gif_path: String, frames_di
|
||||||
logger.warn("GIF fetch: failed to load frame %d" % i)
|
logger.warn("GIF fetch: failed to load frame %d" % i)
|
||||||
continue
|
continue
|
||||||
anim_tex.set_frame_texture(i, ImageTexture.create_from_image(image))
|
anim_tex.set_frame_texture(i, ImageTexture.create_from_image(image))
|
||||||
# Use parsed delay if available, otherwise default.
|
|
||||||
var delay_ms: int = delays[i] if i < delays.size() else GIF_DEFAULT_FRAME_DELAY_MS
|
var delay_ms: int = delays[i] if i < delays.size() else GIF_DEFAULT_FRAME_DELAY_MS
|
||||||
anim_tex.set_frame_delay(i, delay_ms / 1000.0) # AnimatedTexture uses seconds.
|
anim_tex.set_frame_delay(i, delay_ms / 1000.0) # AnimatedTexture uses seconds.
|
||||||
|
|
||||||
|
|
@ -528,56 +508,68 @@ func _parse_gif_frame_delays(data: PackedByteArray) -> Array:
|
||||||
if data.size() < 13:
|
if data.size() < 13:
|
||||||
return delays
|
return delays
|
||||||
|
|
||||||
# Skip header (6) + Logical Screen Descriptor (7).
|
var pos := _skip_gif_header(data)
|
||||||
var pos := 13
|
while pos < data.size() - 1:
|
||||||
# Skip Global Color Table if present.
|
var byte: int = data[pos]
|
||||||
|
if byte == 0x21:
|
||||||
|
var result: Array = _parse_gif_extension(data, pos)
|
||||||
|
if result[0] >= 0:
|
||||||
|
delays.append(result[0])
|
||||||
|
pos = result[1]
|
||||||
|
elif byte == 0x2C:
|
||||||
|
pos = _skip_gif_image_descriptor(data, pos)
|
||||||
|
elif byte == 0x3B:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
return delays
|
||||||
|
|
||||||
|
|
||||||
|
## Skips the GIF header, Logical Screen Descriptor, and Global Color Table.
|
||||||
|
## Returns the byte offset past all header data.
|
||||||
|
func _skip_gif_header(data: PackedByteArray) -> int:
|
||||||
|
var pos := 13 # Header (6) + Logical Screen Descriptor (7).
|
||||||
var packed_byte: int = data[10]
|
var packed_byte: int = data[10]
|
||||||
if packed_byte & 0x80 != 0:
|
if packed_byte & 0x80 != 0:
|
||||||
var gct_size := 3 * (1 << ((packed_byte & 0x07) + 1))
|
var gct_size := 3 * (1 << ((packed_byte & 0x07) + 1))
|
||||||
pos += gct_size
|
pos += gct_size
|
||||||
|
return pos
|
||||||
|
|
||||||
while pos < data.size() - 1:
|
|
||||||
var byte: int = data[pos]
|
## Parses an extension block starting at `pos`. Returns [delay_ms, new_pos].
|
||||||
if byte == 0x21:
|
## If the extension is not a Graphics Control Extension, returns [-1, new_pos].
|
||||||
# Extension block.
|
func _parse_gif_extension(data: PackedByteArray, pos: int) -> Array:
|
||||||
var label: int = data[pos + 1]
|
var label: int = data[pos + 1]
|
||||||
if label == 0xF9:
|
var delay_ms := -1
|
||||||
# Graphics Control Extension — contains frame delay.
|
if label == 0xF9:
|
||||||
var delay_cs: int = data[pos + 4] | (data[pos + 5] << 8) # Centiseconds.
|
var delay_cs: int = data[pos + 4] | (data[pos + 5] << 8)
|
||||||
var delay_ms: int = delay_cs * 10 if delay_cs > 0 else GIF_DEFAULT_FRAME_DELAY_MS
|
delay_ms = delay_cs * 10 if delay_cs > 0 else GIF_DEFAULT_FRAME_DELAY_MS
|
||||||
delays.append(delay_ms)
|
pos += 2
|
||||||
# Skip extension block.
|
while pos < data.size():
|
||||||
pos += 2
|
var block_size: int = data[pos]
|
||||||
while pos < data.size():
|
pos += 1
|
||||||
var block_size: int = data[pos]
|
if block_size == 0:
|
||||||
pos += 1
|
|
||||||
if block_size == 0:
|
|
||||||
break
|
|
||||||
pos += block_size
|
|
||||||
elif byte == 0x2C:
|
|
||||||
# Image descriptor — skip it.
|
|
||||||
pos += 10
|
|
||||||
# Skip Local Color Table if present.
|
|
||||||
var img_packed: int = data[pos - 1]
|
|
||||||
if img_packed & 0x80 != 0:
|
|
||||||
var lct_size := 3 * (1 << ((img_packed & 0x07) + 1))
|
|
||||||
pos += lct_size
|
|
||||||
# Skip LZW Minimum Code Size + sub-blocks.
|
|
||||||
pos += 1 # LZW min code size.
|
|
||||||
while pos < data.size():
|
|
||||||
var block_size: int = data[pos]
|
|
||||||
pos += 1
|
|
||||||
if block_size == 0:
|
|
||||||
break
|
|
||||||
pos += block_size
|
|
||||||
elif byte == 0x3B:
|
|
||||||
# Trailer.
|
|
||||||
break
|
break
|
||||||
else:
|
pos += block_size
|
||||||
# Unknown block — skip.
|
return [delay_ms, pos]
|
||||||
pos += 1
|
|
||||||
|
|
||||||
return delays
|
|
||||||
|
## Skips an image descriptor and its associated data at `pos`.
|
||||||
|
func _skip_gif_image_descriptor(data: PackedByteArray, pos: int) -> int:
|
||||||
|
pos += 10
|
||||||
|
var img_packed: int = data[pos - 1]
|
||||||
|
if img_packed & 0x80 != 0:
|
||||||
|
var lct_size := 3 * (1 << ((img_packed & 0x07) + 1))
|
||||||
|
pos += lct_size
|
||||||
|
pos += 1 # LZW Minimum Code Size.
|
||||||
|
while pos < data.size():
|
||||||
|
var block_size: int = data[pos]
|
||||||
|
pos += 1
|
||||||
|
if block_size == 0:
|
||||||
|
break
|
||||||
|
pos += block_size
|
||||||
|
return pos
|
||||||
|
|
||||||
|
|
||||||
## Combines two textures side-by-side into a single banner image.
|
## Combines two textures side-by-side into a single banner image.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue