subtitle_displayer/index.html
Stephen Seo 846416b9f7
All checks were successful
Publish to burnedkirby.com/subtitle / publish-site (push) Successful in 0s
Fix time duration when "playing"
Previous implementation changed to next subtitle when current subtitle
ended. This means that times between subtitles were ignored.

This implementation now checks if the next subtitle's start is later
than the current subtitle's end, and will wait for the duration that is
longer.
2024-07-09 19:07:54 +09:00

344 lines
11 KiB
HTML

<!--
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
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Subtitle Displayer</title>
<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;
width: 39vw;
height: 4vh;
z-index: 2;
}
button.transport_button {
background-color: transparent;
background-repeat: no-repeat;
font-size: 29vw;
outline: none;
color: rgba(0, 0, 0, 0.4);
z-index: 1;
}
button#left_button {
position: fixed;
top: 20vh;
left: 0;
width: 30vw;
height: 70vh;
}
button#right_button {
position: fixed;
top: 20vh;
right: 0;
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;
}
</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;
}
}
const subtitles_array = [];
var current_subtitle = 0;
var current_time = 0.0;
var current_end = 1.0;
var prev_timestamp = 0.0;
var is_playing = false;
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;
document.getElementById("s_idx").value = current_subtitle;
current_time = subtitles_array[current_subtitle].time_start;
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;
}
} else if (current_subtitle === subtitles_array.length) {
document.getElementById("subtitle_text").textContent = "End of subtitles.";
} 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();
}
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");
let duration = current_end - current_time;
playpause_button.style["transition"] = "all " + duration + "s linear";
playpause_button.style["background-position"] = "right";
});
});
}
}
function on_animation_frame(anim_timestamp) {
if (is_playing && current_subtitle >= 0 && current_subtitle < subtitles_array.length) {
current_time += (anim_timestamp - prev_timestamp) / 1000.0;
prev_timestamp = anim_timestamp;
if (current_time > current_end) {
++current_subtitle;
display_subtitle();
set_up_play_anim();
}
requestAnimationFrame(on_animation_frame);
} else {
document.getElementById("playpause_button").textContent = "▶";
is_playing = false;
}
}
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;
display_subtitle();
set_up_play_anim();
});
document.getElementById("left_button").addEventListener(
"click",
() => {
if (current_subtitle > 0) {
current_subtitle -= 1;
display_subtitle();
set_up_play_anim();
}
}
);
document.getElementById("right_button").addEventListener(
"click",
() => {
if (current_subtitle + 1 < subtitles_array.length) {
current_subtitle += 1;
display_subtitle();
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) {
current_time = subtitles_array[current_subtitle].time_start;
requestAnimationFrame(on_animation_frame);
prev_timestamp = document.timeline.currentTime;
document.getElementById("playpause_button").textContent = "⏸";
set_up_play_anim();
} else {
is_playing = false;
let playpause_button = document.getElementById("playpause_button");
playpause_button.textContent = "▶";
playpause_button.style["transition"] = "all 0s linear";
playpause_button.style["background-position"] = "left";
}
}
);
});
</script>
</head>
<body>
<div id="upload_div">
<form name="uploadForm">
<div>
<input id="uploadInput" type="file" accept=".srt" />
</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>
<button id="playpause_button" class="transport_button" type="button"></button>
</body>
</html>
<!--
vim: ts=2 sts=2 sw=2
-->