feat(subtitles): working video subtitles. TODO: enable/disable subtitles option
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
class_name SubtitleEntry
|
||||||
|
|
||||||
|
|
||||||
|
# Entry order. First is 1.
|
||||||
|
var id: int
|
||||||
|
# Start and end time in seconds.
|
||||||
|
var start_time: float
|
||||||
|
# End time in seconds.
|
||||||
|
var end_time: float
|
||||||
|
# Subtitle text.
|
||||||
|
var content: String
|
||||||
|
|
||||||
|
func _init(_id: int, _start_time: float, _end_time: float, _content: String) -> void:
|
||||||
|
id = _id
|
||||||
|
start_time = _start_time
|
||||||
|
end_time = _end_time
|
||||||
|
content = _content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://curj8kp4ivly0
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
@tool
|
||||||
|
class_name SubtitlesLabel
|
||||||
|
extends RichTextLabel
|
||||||
|
|
||||||
|
|
||||||
|
var _player: VideoStreamPlayer
|
||||||
|
var _subtitles: Array[SubtitleEntry] = []
|
||||||
|
var _current_entry: SubtitleEntry = null
|
||||||
|
var _template: String = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_player = $".."
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _player and _player.visible and _player.is_playing() and not _subtitles.is_empty():
|
||||||
|
_update_content(_player.stream_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_content(current_time: float) -> void:
|
||||||
|
if _current_entry and current_time > _current_entry.end_time:
|
||||||
|
_current_entry = null
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
if _current_entry == null:
|
||||||
|
for entry in _subtitles:
|
||||||
|
if current_time >= entry.start_time and current_time <= entry.end_time:
|
||||||
|
_current_entry = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
if _current_entry:
|
||||||
|
if _template.is_empty():
|
||||||
|
text = _current_entry.content
|
||||||
|
else:
|
||||||
|
text = _template % [_current_entry.content]
|
||||||
|
|
||||||
|
|
||||||
|
func parse_subtitles_file(path: String) -> Error:
|
||||||
|
_subtitles = []
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if not file:
|
||||||
|
return FAILED
|
||||||
|
|
||||||
|
var state := 0 # 0: read id, 1: read time, 2: read content
|
||||||
|
var current_id := 0
|
||||||
|
var start_time := 0.0
|
||||||
|
var end_time := 0.0
|
||||||
|
var content := ""
|
||||||
|
|
||||||
|
# Compile regex patterns
|
||||||
|
var time_regex = RegEx.new()
|
||||||
|
time_regex.compile("(?<start>[0-9,:]+)\\s*-->\\s*(?<end>[0-9,:]+)")
|
||||||
|
|
||||||
|
while not file.eof_reached():
|
||||||
|
var line := file.get_line().strip_edges()
|
||||||
|
|
||||||
|
match state:
|
||||||
|
0: # Read ID
|
||||||
|
if line.is_empty():
|
||||||
|
continue
|
||||||
|
current_id = line.to_int()
|
||||||
|
state = 1
|
||||||
|
|
||||||
|
1: # Read Time
|
||||||
|
if line.is_empty():
|
||||||
|
continue
|
||||||
|
var result = time_regex.search(line)
|
||||||
|
if result:
|
||||||
|
start_time = _parse_time_string(result.get_string("start"))
|
||||||
|
end_time = _parse_time_string(result.get_string("end"))
|
||||||
|
state = 2
|
||||||
|
|
||||||
|
2: # Read Content
|
||||||
|
if line.is_empty():
|
||||||
|
if not content.is_empty():
|
||||||
|
content = _process_content(content)
|
||||||
|
var entry = SubtitleEntry.new(current_id, start_time, end_time, content)
|
||||||
|
_subtitles.append(entry)
|
||||||
|
content = ""
|
||||||
|
state = 0
|
||||||
|
continue
|
||||||
|
if not content.is_empty():
|
||||||
|
content += "\n"
|
||||||
|
content += line
|
||||||
|
|
||||||
|
if not content.is_empty():
|
||||||
|
content = _process_content(content)
|
||||||
|
var entry = SubtitleEntry.new(current_id, start_time, end_time, content)
|
||||||
|
_subtitles.append(entry)
|
||||||
|
|
||||||
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
func _parse_time_string(time_str: String) -> float:
|
||||||
|
var time_split = time_str.replace(",", ".").split(":")
|
||||||
|
var hours := time_split[0].to_float()
|
||||||
|
var minutes := time_split[1].to_float()
|
||||||
|
var seconds := time_split[2].to_float()
|
||||||
|
return hours * 3600 + minutes * 60 + seconds
|
||||||
|
|
||||||
|
|
||||||
|
func _process_content(content: String) -> String:
|
||||||
|
content = content.replace("<b>", "[b]").replace("</b>", "[/b]").replace("{b}", "[b]").replace("{/b}", "[/b]")
|
||||||
|
content = content.replace("<i>", "[i]").replace("</i>", "[/i]").replace("{i}", "[i]").replace("{/i}", "[/i]")
|
||||||
|
content = content.replace("<u>", "[u]").replace("</u>", "[/u]").replace("{u}", "[u]").replace("{/u}", "[/u]")
|
||||||
|
content = content.replace("</font>", "[/color]")
|
||||||
|
|
||||||
|
var color_regex = RegEx.new()
|
||||||
|
color_regex.compile("<font\\s+color=[\"'](.+)[\"']>")
|
||||||
|
content = color_regex.sub(content, "[color=\\1]")
|
||||||
|
|
||||||
|
var line_pos_regex = RegEx.new()
|
||||||
|
line_pos_regex.compile("\\{\\\\a([0-9]+)\\}")
|
||||||
|
var matches = line_pos_regex.search_all(content)
|
||||||
|
for m in matches:
|
||||||
|
var count = m.get_string(1).to_int()
|
||||||
|
if count > 0:
|
||||||
|
content = content.replace(m.get_string(), "\n".repeat(count - 1))
|
||||||
|
else:
|
||||||
|
content = content.replace(m.get_string(), "")
|
||||||
|
|
||||||
|
return content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dnjyb6nrg1s02
|
||||||
@@ -3,8 +3,9 @@ extends Node
|
|||||||
|
|
||||||
signal finished
|
signal finished
|
||||||
|
|
||||||
func play(video_file: String):
|
func play(video_path: String):
|
||||||
$VideoStreamPlayer.set_stream(load(video_file))
|
$VideoStreamPlayer.set_stream(load(video_path))
|
||||||
|
$VideoStreamPlayer/SubtitlesLabel.parse_subtitles_file(_get_srt_path(video_path))
|
||||||
$VideoStreamPlayer.play()
|
$VideoStreamPlayer.play()
|
||||||
|
|
||||||
func _on_VideoPlayer_finished():
|
func _on_VideoPlayer_finished():
|
||||||
@@ -17,11 +18,21 @@ func skip():
|
|||||||
self.visible = false
|
self.visible = false
|
||||||
emit_signal("finished")
|
emit_signal("finished")
|
||||||
|
|
||||||
|
|
||||||
func get_player():
|
func get_player():
|
||||||
return $VideoStreamPlayer
|
return $VideoStreamPlayer
|
||||||
|
|
||||||
|
|
||||||
func is_playing() -> bool:
|
func is_playing() -> bool:
|
||||||
var play = $VideoStreamPlayer.is_playing()
|
var play = $VideoStreamPlayer.is_playing()
|
||||||
return play
|
return play
|
||||||
|
|
||||||
|
|
||||||
func _on_Skip_pressed():
|
func _on_Skip_pressed():
|
||||||
skip()
|
skip()
|
||||||
|
|
||||||
|
|
||||||
|
# Get subtitles file path from the video file path.
|
||||||
|
func _get_srt_path(video_path: String):
|
||||||
|
var locale = TranslationServer.get_locale()
|
||||||
|
return video_path.left(-3) + locale + ".srt"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
[gd_scene load_steps=5 format=3 uid="uid://ctg3fukoficqk"]
|
[gd_scene load_steps=7 format=3 uid="uid://ctg3fukoficqk"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://ckl3iy3v3v68s" path="res://addons/escoria-ui-return-monkey-island/video_player/video_player.gd" id="1"]
|
[ext_resource type="Script" uid="uid://ckl3iy3v3v68s" path="res://addons/escoria-ui-return-monkey-island/video_player/video_player.gd" id="1"]
|
||||||
[ext_resource type="Theme" uid="uid://bf2eet52fueam" path="res://addons/escoria-ui-return-monkey-island/theme/ui.tres" id="1_384st"]
|
[ext_resource type="Theme" uid="uid://bf2eet52fueam" path="res://addons/escoria-ui-return-monkey-island/theme/ui.tres" id="1_384st"]
|
||||||
|
[ext_resource type="Script" uid="uid://dnjyb6nrg1s02" path="res://addons/escoria-ui-return-monkey-island/video_player/subtitles/subtitles_label.gd" id="SubtitlesLabel"]
|
||||||
|
|
||||||
[sub_resource type="VideoStreamTheora" id="1"]
|
[sub_resource type="VideoStreamTheora" id="1"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_reqeu"]
|
||||||
|
|
||||||
[sub_resource type="Shortcut" id="3"]
|
[sub_resource type="Shortcut" id="3"]
|
||||||
|
|
||||||
[node name="video_player" type="Control"]
|
[node name="video_player" type="Control"]
|
||||||
@@ -27,6 +30,23 @@ offset_right = 1280.0
|
|||||||
offset_bottom = 720.0
|
offset_bottom = 720.0
|
||||||
stream = SubResource("1")
|
stream = SubResource("1")
|
||||||
|
|
||||||
|
[node name="SubtitlesLabel" type="RichTextLabel" parent="VideoStreamPlayer"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 12
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = -89.0
|
||||||
|
offset_bottom = -2.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
theme_override_font_sizes/normal_font_size = 24
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxEmpty_reqeu")
|
||||||
|
autowrap_mode = 0
|
||||||
|
horizontal_alignment = 1
|
||||||
|
script = ExtResource("SubtitlesLabel")
|
||||||
|
metadata/_custom_type_script = "uid://dnjyb6nrg1s02"
|
||||||
|
|
||||||
[node name="PanelContainer" type="PanelContainer" parent="."]
|
[node name="PanelContainer" type="PanelContainer" parent="."]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
anchors_preset = 3
|
anchors_preset = 3
|
||||||
|
|||||||
8
gymkhana/videos/turno_cocina/intro.en.srt
Normal file
8
gymkhana/videos/turno_cocina/intro.en.srt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
1
|
||||||
|
00:00:00,000 --> 00:00:03,000
|
||||||
|
Test
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:05,000 --> 00:00:36,000
|
||||||
|
2
|
||||||
|
|
||||||
13
gymkhana/videos/turno_cocina/intro.es.srt
Normal file
13
gymkhana/videos/turno_cocina/intro.es.srt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
1
|
||||||
|
00:00:00,000 --> 00:00:03,000
|
||||||
|
Test ES
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:05,000 --> 00:00:07,000
|
||||||
|
2 ES
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:08,000 --> 00:00:36,000
|
||||||
|
Aquí va una línea que es bastante larga, a ver que pasa con ella
|
||||||
|
Y una seguna línea de regalo
|
||||||
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
uid://k2q0o6vc54p7
|
|
||||||
Reference in New Issue
Block a user