2024-07-08 05:30:51 +00:00
|
|
|
<!--
|
|
|
|
HTML page that views subtitles.
|
|
|
|
Copyright (C) 2024 Stephen Seo
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
Contact info: stephen@seodisparate.com
|
2024-07-10 05:43:29 +00:00
|
|
|
Repo that has this code:
|
|
|
|
https://git.seodisparate.com/stephenseo/subtitle_displayer
|
2024-07-08 05:30:51 +00:00
|
|
|
-->
|
|
|
|
|
|
|
|
<!doctype html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8" />
|
2024-07-08 05:57:00 +00:00
|
|
|
<title>Subtitle Displayer</title>
|
2024-07-08 05:30:51 +00:00
|
|
|
<style>
|
|
|
|
body {
|
|
|
|
color: #FFF;
|
|
|
|
background: #333;
|
|
|
|
}
|
|
|
|
.large_text {
|
|
|
|
font-size: 11vh;
|
|
|
|
}
|
|
|
|
.small_text {
|
|
|
|
font-size: 3vh;
|
|
|
|
}
|
|
|
|
input#s_idx {
|
|
|
|
position: fixed;
|
|
|
|
bottom: 0;
|
|
|
|
left: 30vw;
|
2024-07-08 08:10:43 +00:00
|
|
|
width: 39vw;
|
|
|
|
height: 4vh;
|
|
|
|
z-index: 2;
|
2024-07-08 05:30:51 +00:00
|
|
|
}
|
|
|
|
button.transport_button {
|
|
|
|
background-color: transparent;
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
font-size: 29vw;
|
|
|
|
outline: none;
|
|
|
|
color: rgba(0, 0, 0, 0.4);
|
2024-07-08 08:10:43 +00:00
|
|
|
z-index: 1;
|
2024-07-08 05:30:51 +00:00
|
|
|
}
|
|
|
|
button#left_button {
|
|
|
|
position: fixed;
|
|
|
|
top: 20vh;
|
|
|
|
left: 0;
|
2024-07-08 08:10:43 +00:00
|
|
|
width: 30vw;
|
|
|
|
height: 70vh;
|
2024-07-08 05:30:51 +00:00
|
|
|
}
|
|
|
|
button#right_button {
|
|
|
|
position: fixed;
|
|
|
|
top: 20vh;
|
|
|
|
right: 0;
|
2024-07-08 08:10:43 +00:00
|
|
|
width: 30vw;
|
|
|
|
height: 70vh;
|
|
|
|
}
|
|
|
|
button#playpause_button {
|
|
|
|
position: fixed;
|
|
|
|
bottom: 10vh;
|
|
|
|
left: 35vw;
|
|
|
|
width: 30vw;
|
|
|
|
height: 20vh;
|
|
|
|
font-size: 18vh;
|
|
|
|
/* animation */
|
|
|
|
background: linear-gradient(to left, green 50%, white 50%) right;
|
|
|
|
background-size: 200% 100%;
|
|
|
|
background-position: left;
|
2024-07-08 05:30:51 +00:00
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<script>
|
|
|
|
class SubtitlePart {
|
|
|
|
constructor(idx, time_start, time_end, timestamp_start, timestamp_end, text) {
|
|
|
|
this.idx = idx;
|
|
|
|
this.time_start = time_start;
|
|
|
|
this.time_end = time_end;
|
|
|
|
this.timestamp_start = timestamp_start;
|
|
|
|
this.timestamp_end = timestamp_end;
|
|
|
|
this.text = text;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-10 05:33:57 +00:00
|
|
|
class TimeInfo {
|
|
|
|
constructor(timestamp) {
|
|
|
|
this.timestamp = timestamp;
|
|
|
|
}
|
|
|
|
|
|
|
|
set_time_offset(offset_millis) {
|
|
|
|
this.timestamp = document.timeline.currentTime - offset_millis;
|
|
|
|
}
|
|
|
|
|
|
|
|
is_after_ts_end(ts_end_millis) {
|
|
|
|
return (document.timeline.currentTime - this.timestamp) > ts_end_millis;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-08 05:30:51 +00:00
|
|
|
const subtitles_array = [];
|
|
|
|
var current_subtitle = 0;
|
2024-07-10 05:33:57 +00:00
|
|
|
var time_info = new TimeInfo(document.timeline.currentTime);
|
2024-07-09 10:07:54 +00:00
|
|
|
var current_end = 1.0;
|
2024-07-10 05:33:57 +00:00
|
|
|
var current_duration = 1.0;
|
2024-07-08 08:10:43 +00:00
|
|
|
var is_playing = false;
|
2024-07-08 05:30:51 +00:00
|
|
|
|
|
|
|
function display_subtitle() {
|
|
|
|
if (current_subtitle < subtitles_array.length) {
|
|
|
|
document.getElementById("subtitle_text").textContent = subtitles_array[current_subtitle].text;
|
|
|
|
document.getElementById("subtitle_time").textContent = subtitles_array[current_subtitle].timestamp_start + " --> " + subtitles_array[current_subtitle].timestamp_end;
|
2024-07-08 08:10:43 +00:00
|
|
|
document.getElementById("s_idx").value = current_subtitle;
|
2024-07-10 05:33:57 +00:00
|
|
|
var current_start = subtitles_array[current_subtitle].time_start;
|
|
|
|
current_end = current_start + 1.0;
|
2024-07-09 10:07:54 +00:00
|
|
|
if (current_subtitle + 1 < subtitles_array.length
|
|
|
|
&& subtitles_array[current_subtitle + 1].time_start > subtitles_array[current_subtitle].time_end) {
|
|
|
|
current_end = subtitles_array[current_subtitle + 1].time_start;
|
|
|
|
} else {
|
|
|
|
current_end = subtitles_array[current_subtitle].time_end;
|
|
|
|
}
|
2024-07-10 05:33:57 +00:00
|
|
|
current_duration = current_end - current_start;
|
2024-07-08 08:10:43 +00:00
|
|
|
} else if (current_subtitle === subtitles_array.length) {
|
|
|
|
document.getElementById("subtitle_text").textContent = "End of subtitles.";
|
2024-07-08 05:30:51 +00:00
|
|
|
} else {
|
|
|
|
document.getElementById("subtitle_text").textContent = "Internal Error";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function parse_timestamp(timestamp) {
|
|
|
|
const timestamp_re = /(?:(?:(?:([0-9]+:)?)([0-9]+:)?)([0-9]+,[0-9]+)?)/
|
|
|
|
const seconds_re = /([0-9][0-9]),([0-9]+)/
|
|
|
|
const re_match_ts = timestamp_re.exec(timestamp.trim());
|
|
|
|
var counter = 0;
|
|
|
|
var temp_time = 0.0;
|
|
|
|
for (var idx = re_match_ts.length - 1; idx > 0; --idx) {
|
|
|
|
if (re_match_ts[idx] === undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (counter === 0) {
|
|
|
|
const re_match_ts_seconds = seconds_re.exec(re_match_ts[idx]);
|
|
|
|
if (re_match_ts_seconds[1] !== undefined) {
|
|
|
|
temp_time += parseFloat(re_match_ts_seconds[1]);
|
|
|
|
if (re_match_ts_seconds[2] !== undefined) {
|
|
|
|
temp_time += parseFloat("0." + re_match_ts_seconds[2]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
++counter;
|
|
|
|
} else if (counter === 1) {
|
|
|
|
temp_time += parseFloat(re_match_ts[idx]) * 60.0;
|
|
|
|
++counter;
|
|
|
|
} else if (counter === 2) {
|
|
|
|
temp_time += parseFloat(re_match_ts[idx]) * 60.0 * 60.0;
|
|
|
|
++counter;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return temp_time;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parse_subtitles(subtitles) {
|
|
|
|
subtitles_array.length = 0;
|
|
|
|
// Split on lines.
|
|
|
|
const lines = subtitles.trim().split("\n");
|
|
|
|
const number_re = /^[0-9]+$/;
|
|
|
|
var subtitle_number = 0;
|
|
|
|
var newline_reached = true;
|
|
|
|
var number_reached = false;
|
|
|
|
var timestamp_reached = false;
|
|
|
|
var ts_first = 0.0;
|
|
|
|
var ts_second = 0.0;
|
|
|
|
var ts_first_ts = undefined;
|
|
|
|
var ts_second_ts = undefined;
|
|
|
|
var part_text = undefined;
|
|
|
|
for (var lidx = 0; lidx < lines.length; ++lidx) {
|
|
|
|
var line = lines[lidx].trim();
|
|
|
|
if (line.length === 0) {
|
|
|
|
newline_reached = true;
|
|
|
|
number_reached = false;
|
|
|
|
timestamp_reached = false;
|
|
|
|
if (ts_second !== 0.0 && part_text !== undefined) {
|
|
|
|
subtitles_array.push(new SubtitlePart(subtitle_number, ts_first, ts_second, ts_first_ts, ts_second_ts, part_text));
|
|
|
|
}
|
|
|
|
ts_first = 0.0;
|
|
|
|
ts_second = 0.0;
|
|
|
|
ts_first_ts = undefined;
|
|
|
|
ts_second_ts = undefined;
|
|
|
|
part_text = undefined;
|
|
|
|
} else if (newline_reached && number_re.test(line)) {
|
|
|
|
subtitle_number = parseInt(line);
|
|
|
|
newline_reached = false;
|
|
|
|
number_reached = true;
|
|
|
|
} else if (number_reached) {
|
|
|
|
var times = line.split(/-->/);
|
|
|
|
if (times.length !== 2) {
|
|
|
|
console.log("ERROR: Failed to parse timestamp at " + subtitle_number + ".");
|
|
|
|
} else {
|
|
|
|
ts_first_ts = times[0];
|
|
|
|
ts_second_ts = times[1];
|
|
|
|
ts_first = parse_timestamp(times[0]);
|
|
|
|
ts_second = parse_timestamp(times[1]);
|
|
|
|
}
|
|
|
|
number_reached = false;
|
|
|
|
timestamp_reached = true;
|
|
|
|
} else if (timestamp_reached) {
|
|
|
|
if (part_text === undefined) {
|
|
|
|
part_text = line;
|
|
|
|
} else {
|
|
|
|
part_text += " " + line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (ts_second !== 0.0 && part_text !== undefined) {
|
|
|
|
subtitles_array.push(new SubtitlePart(subtitle_number, ts_first, ts_second, ts_first_ts, ts_second_ts, part_text));
|
|
|
|
}
|
|
|
|
current_subtitle = 0;
|
|
|
|
if (subtitles_array.length > 1) {
|
|
|
|
document.getElementById("s_idx").max = subtitles_array.length - 1;
|
|
|
|
} else {
|
|
|
|
document.getElementById("s_idx").max = 1;
|
|
|
|
}
|
|
|
|
display_subtitle();
|
|
|
|
}
|
|
|
|
|
2024-07-08 08:10:43 +00:00
|
|
|
function set_up_play_anim() {
|
|
|
|
if (is_playing) {
|
|
|
|
requestAnimationFrame((time) => {
|
|
|
|
playpause_button.style["transition"] = "all 0s linear";
|
|
|
|
playpause_button.style["background-position"] = "left";
|
|
|
|
requestAnimationFrame((time) => {
|
|
|
|
let playpause_button = document.getElementById("playpause_button");
|
2024-07-10 05:33:57 +00:00
|
|
|
playpause_button.style["transition"] = "all " + current_duration + "s linear";
|
2024-07-08 08:10:43 +00:00
|
|
|
playpause_button.style["background-position"] = "right";
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function on_animation_frame(anim_timestamp) {
|
|
|
|
if (is_playing && current_subtitle >= 0 && current_subtitle < subtitles_array.length) {
|
2024-07-10 05:33:57 +00:00
|
|
|
if (time_info.is_after_ts_end(current_end * 1000.0)) {
|
2024-07-08 08:10:43 +00:00
|
|
|
++current_subtitle;
|
|
|
|
display_subtitle();
|
|
|
|
set_up_play_anim();
|
|
|
|
}
|
|
|
|
requestAnimationFrame(on_animation_frame);
|
|
|
|
} else {
|
2024-07-08 09:17:20 +00:00
|
|
|
document.getElementById("playpause_button").textContent = "▶";
|
2024-07-08 08:10:43 +00:00
|
|
|
is_playing = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-08 05:30:51 +00:00
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
var uploadInput = document.getElementById("uploadInput");
|
|
|
|
const subtitle_slider = document.getElementById("s_idx");
|
|
|
|
|
|
|
|
uploadInput.addEventListener(
|
|
|
|
"change",
|
|
|
|
() => {
|
|
|
|
if (uploadInput.files.length > 0) {
|
|
|
|
let reader = new FileReader();
|
|
|
|
reader.addEventListener(
|
|
|
|
"load",
|
|
|
|
() => {
|
|
|
|
parse_subtitles(reader.result);
|
|
|
|
document.getElementById("upload_div").remove();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
reader.readAsText(uploadInput.files[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
subtitle_slider.addEventListener("input", (event) => {
|
|
|
|
var value = Math.round(event.target.value);
|
|
|
|
if (value < 0) {
|
|
|
|
value = 0;
|
|
|
|
} else if (value >= subtitles_array.length) {
|
|
|
|
value = subtitles_array.length - 1;
|
|
|
|
}
|
|
|
|
current_subtitle = value;
|
2024-07-10 05:33:57 +00:00
|
|
|
time_info.set_time_offset(
|
|
|
|
subtitles_array[current_subtitle].time_start * 1000.0
|
|
|
|
);
|
2024-07-08 05:30:51 +00:00
|
|
|
display_subtitle();
|
2024-07-08 08:21:13 +00:00
|
|
|
set_up_play_anim();
|
2024-07-08 05:30:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
document.getElementById("left_button").addEventListener(
|
|
|
|
"click",
|
|
|
|
() => {
|
|
|
|
if (current_subtitle > 0) {
|
|
|
|
current_subtitle -= 1;
|
2024-07-10 05:33:57 +00:00
|
|
|
time_info.set_time_offset(
|
|
|
|
subtitles_array[current_subtitle].time_start * 1000.0
|
|
|
|
);
|
2024-07-08 05:30:51 +00:00
|
|
|
display_subtitle();
|
2024-07-08 08:10:43 +00:00
|
|
|
set_up_play_anim();
|
2024-07-08 05:30:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
document.getElementById("right_button").addEventListener(
|
|
|
|
"click",
|
|
|
|
() => {
|
|
|
|
if (current_subtitle + 1 < subtitles_array.length) {
|
|
|
|
current_subtitle += 1;
|
2024-07-10 05:33:57 +00:00
|
|
|
time_info.set_time_offset(
|
|
|
|
subtitles_array[current_subtitle].time_start * 1000.0
|
|
|
|
);
|
2024-07-08 05:30:51 +00:00
|
|
|
display_subtitle();
|
2024-07-08 08:10:43 +00:00
|
|
|
set_up_play_anim();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
document.getElementById("playpause_button").addEventListener(
|
|
|
|
"click",
|
|
|
|
() => {
|
|
|
|
is_playing = !is_playing;
|
|
|
|
if (is_playing && current_subtitle >= 0 && current_subtitle < subtitles_array.length) {
|
2024-07-10 05:33:57 +00:00
|
|
|
time_info.set_time_offset(
|
|
|
|
subtitles_array[current_subtitle].time_start * 1000.0
|
|
|
|
);
|
2024-07-08 08:10:43 +00:00
|
|
|
requestAnimationFrame(on_animation_frame);
|
|
|
|
document.getElementById("playpause_button").textContent = "⏸";
|
|
|
|
set_up_play_anim();
|
|
|
|
} else {
|
|
|
|
is_playing = false;
|
|
|
|
let playpause_button = document.getElementById("playpause_button");
|
2024-07-08 09:17:20 +00:00
|
|
|
playpause_button.textContent = "▶";
|
2024-07-08 08:10:43 +00:00
|
|
|
playpause_button.style["transition"] = "all 0s linear";
|
|
|
|
playpause_button.style["background-position"] = "left";
|
2024-07-08 05:30:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="upload_div">
|
|
|
|
<form name="uploadForm">
|
|
|
|
<div>
|
2024-07-08 05:57:00 +00:00
|
|
|
<input id="uploadInput" type="file" accept=".srt" />
|
2024-07-08 05:30:51 +00:00
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
<div id="subtitle_text" class="large_text">
|
|
|
|
Pick a subtitle ".srt" file. Subtitles will appear here.
|
|
|
|
</div>
|
|
|
|
<div id="subtitle_time" class="small_text">
|
|
|
|
</div>
|
|
|
|
<div id="slider">
|
|
|
|
<input type="range" id="s_idx" name="subtitle index" min="0" max="1" step="any" />
|
|
|
|
</div>
|
|
|
|
<button id="left_button" class="transport_button" type="button">⬅️</button>
|
|
|
|
<button id="right_button" class="transport_button" type="button">➡️</button>
|
2024-07-08 09:17:20 +00:00
|
|
|
<button id="playpause_button" class="transport_button" type="button">▶</button>
|
2024-07-08 05:30:51 +00:00
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
|
|
|
|
<!--
|
|
|
|
vim: ts=2 sts=2 sw=2
|
|
|
|
-->
|