432 lines
17 KiB
JavaScript
432 lines
17 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
|
|
app.registerExtension({
|
|
name: "vrch.TextSrtPlayerNode",
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass === "VrchTextSrtPlayerNode") {
|
|
// Obtain required widgets
|
|
const srtTextWidget = node.widgets.find(w => w.name === "srt_text");
|
|
const loopWidget = node.widgets.find(w => w.name === "loop");
|
|
const currentSelectionWidget = node.widgets.find(w => w.name === "current_selection");
|
|
const debugWidget = node.widgets.find(w => w.name === "debug");
|
|
|
|
// Player state variables
|
|
let isPlaying = false;
|
|
let timerId = null;
|
|
let playbackTime = 0; // Current playback time in seconds
|
|
let lastResumeTimestamp = 0; // Timestamp when playback last resumed
|
|
let srtEntries = []; // Parsed SRT entries array
|
|
let shouldLoop = loopWidget ? loopWidget.value : false;
|
|
|
|
// Debug logging function: logs messages when debug is enabled
|
|
const debugLog = (msg) => {
|
|
if (debugWidget && debugWidget.value) {
|
|
console.log("[VrchTextSrtPlayerNode]", msg);
|
|
}
|
|
};
|
|
|
|
// Convert time string "HH:MM:SS,mmm" to seconds (as a float)
|
|
function timeToSeconds(timeStr) {
|
|
const parts = timeStr.split(/[:,]/);
|
|
const hours = parseInt(parts[0], 10);
|
|
const minutes = parseInt(parts[1], 10);
|
|
const seconds = parseInt(parts[2], 10);
|
|
const milliseconds = parseInt(parts[3], 10);
|
|
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
|
|
}
|
|
|
|
// Format seconds as "M:SS" (e.g. 65 -> "1:05")
|
|
function formatTime(sec) {
|
|
const m = Math.floor(sec / 60);
|
|
const s = Math.floor(sec % 60);
|
|
return m + ":" + (s < 10 ? "0" + s : s);
|
|
}
|
|
|
|
// Parse SRT text, return an array of objects containing index, start (in seconds), end (in seconds), and content
|
|
function parseSRTText(srtText) {
|
|
const entries = [];
|
|
const blocks = srtText.split(/\r?\n\r?\n/);
|
|
blocks.forEach(block => {
|
|
block = block.trim();
|
|
if (!block) return;
|
|
const lines = block.split(/\r?\n/);
|
|
if (lines.length < 2) return; // At least index and time line are required
|
|
const index = parseInt(lines[0], 10);
|
|
const timeLine = lines[1];
|
|
const timeMatch = timeLine.match(/(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})/);
|
|
if (!timeMatch) return;
|
|
const start = timeToSeconds(timeMatch[1]);
|
|
const end = timeToSeconds(timeMatch[2]);
|
|
const content = lines.slice(2).join("\n");
|
|
entries.push({ index, start, end, content });
|
|
});
|
|
debugLog("Parsed " + entries.length + " SRT entries.");
|
|
return entries;
|
|
}
|
|
|
|
// Update current subtitle selection based on playback time (1-based index, set to -1 if not within any subtitle interval)
|
|
function updateCurrentSelection() {
|
|
if (!srtEntries || srtEntries.length === 0) return;
|
|
let selectedIndex = -1;
|
|
for (let i = 0; i < srtEntries.length; i++) {
|
|
const entry = srtEntries[i];
|
|
if (playbackTime >= entry.start && playbackTime < entry.end) {
|
|
selectedIndex = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (currentSelectionWidget) {
|
|
currentSelectionWidget.value = selectedIndex;
|
|
debugLog("Updated current selection to: " + selectedIndex);
|
|
}
|
|
}
|
|
|
|
// Update the text in the time display area
|
|
function updateTimeDisplay() {
|
|
timeDisplay.textContent = "Current time: " + playbackTime.toFixed(2) + " sec";
|
|
}
|
|
|
|
// Create or update tick labels at 0%, 25%, 50%, 75%, and 100% of the total duration,
|
|
// each with a small vertical marker above the label.
|
|
function createTickLabels(duration) {
|
|
tickLabelsContainer.innerHTML = "";
|
|
if (duration <= 0) return;
|
|
|
|
// We want 5 tick positions: 0%, 25%, 50%, 75%, 100%
|
|
const fractions = [0, 0.25, 0.5, 0.75, 1.0];
|
|
fractions.forEach(frac => {
|
|
const labelTime = frac * duration;
|
|
|
|
// Container for marker + label
|
|
const labelDiv = document.createElement("div");
|
|
labelDiv.style.display = "flex";
|
|
labelDiv.style.flexDirection = "column";
|
|
labelDiv.style.alignItems = "center";
|
|
|
|
// The small vertical bar (marker) above the label
|
|
const marker = document.createElement("div");
|
|
marker.classList.add("vrch-srt-tick-marker");
|
|
// For example, you could also use marker.textContent = "|" if you want a direct symbol
|
|
|
|
// The time label below the marker
|
|
const label = document.createElement("div");
|
|
label.classList.add("vrch-srt-tick-label");
|
|
label.textContent = formatTime(labelTime);
|
|
|
|
labelDiv.appendChild(marker);
|
|
labelDiv.appendChild(label);
|
|
tickLabelsContainer.appendChild(labelDiv);
|
|
});
|
|
}
|
|
|
|
// Timer tick function: update playback time, subtitle selection, time display, and slider
|
|
function tick() {
|
|
if (!isPlaying) return;
|
|
const now = Date.now();
|
|
const delta = (now - lastResumeTimestamp) / 1000;
|
|
playbackTime += delta;
|
|
lastResumeTimestamp = now;
|
|
debugLog("Playback time: " + playbackTime.toFixed(2) + " sec");
|
|
updateCurrentSelection();
|
|
updateTimeDisplay();
|
|
// Update the slider value
|
|
slider.value = playbackTime;
|
|
|
|
// Check if playback has reached the end time of the last subtitle
|
|
if (srtEntries.length > 0 && playbackTime >= srtEntries[srtEntries.length - 1].end) {
|
|
if (shouldLoop) {
|
|
debugLog("Loop mode active, restarting playback.");
|
|
playbackTime = 0;
|
|
lastResumeTimestamp = Date.now();
|
|
pausePlayback();
|
|
startPlayback();
|
|
} else {
|
|
debugLog("Playback finished, stopping.");
|
|
pausePlayback();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start or resume playback: parse SRT text and start the timer, also update slider max value and tick labels
|
|
function startPlayback() {
|
|
if (isPlaying) return;
|
|
if (srtTextWidget) {
|
|
srtEntries = parseSRTText(srtTextWidget.value);
|
|
if (srtEntries.length > 0) {
|
|
slider.max = srtEntries[srtEntries.length - 1].end;
|
|
createTickLabels(srtEntries[srtEntries.length - 1].end);
|
|
} else {
|
|
slider.max = 0;
|
|
createTickLabels(0);
|
|
}
|
|
}
|
|
isPlaying = true;
|
|
lastResumeTimestamp = Date.now();
|
|
timerId = setInterval(tick, 100);
|
|
playPauseButton.textContent = "Pause";
|
|
debugLog("Playback started.");
|
|
}
|
|
|
|
// Pause playback: stop the timer but keep current playback time
|
|
function pausePlayback() {
|
|
if (!isPlaying) return;
|
|
isPlaying = false;
|
|
if (timerId) {
|
|
clearInterval(timerId);
|
|
timerId = null;
|
|
}
|
|
playPauseButton.textContent = "Play SRT File";
|
|
debugLog("Playback paused at " + playbackTime.toFixed(2) + " sec.");
|
|
}
|
|
|
|
// Reset playback: pause and reset time to start, also update slider display
|
|
function resetPlayback() {
|
|
pausePlayback();
|
|
playbackTime = 0;
|
|
updateCurrentSelection();
|
|
updateTimeDisplay();
|
|
slider.value = playbackTime;
|
|
debugLog("Playback reset.");
|
|
}
|
|
|
|
// Create a container for widgets, with a fixed height
|
|
const widgetContainer = document.createElement("div");
|
|
|
|
// Create a container for buttons, arranged vertically and centered
|
|
const buttonsContainer = document.createElement("div");
|
|
buttonsContainer.style.display = "flex";
|
|
buttonsContainer.style.flexDirection = "column";
|
|
buttonsContainer.style.alignItems = "center";
|
|
buttonsContainer.style.gap = "10px";
|
|
|
|
// Create the Play/Pause button, initial state "Play SRT File"
|
|
const playPauseButton = document.createElement("button");
|
|
playPauseButton.textContent = "Play SRT File";
|
|
playPauseButton.classList.add("vrch-srt-big-button");
|
|
playPauseButton.onclick = () => {
|
|
if (isPlaying) {
|
|
pausePlayback();
|
|
} else {
|
|
startPlayback();
|
|
}
|
|
};
|
|
|
|
// Create the Reset button
|
|
const resetButton = document.createElement("button");
|
|
resetButton.textContent = "Reset";
|
|
resetButton.classList.add("vrch-srt-big-button", "vrch-srt-reset-button");
|
|
resetButton.onclick = () => {
|
|
resetPlayback();
|
|
};
|
|
|
|
// Append buttons to the buttons container
|
|
buttonsContainer.appendChild(playPauseButton);
|
|
buttonsContainer.appendChild(resetButton);
|
|
|
|
// Create the progress slider
|
|
const slider = document.createElement("input");
|
|
slider.type = "range";
|
|
slider.min = 0;
|
|
slider.step = 0.01; // Allow adjustment in fractions of a second
|
|
slider.value = playbackTime;
|
|
slider.classList.add("vrch-srt-slider");
|
|
|
|
// When the user drags the slider, update playback time and related displays
|
|
slider.addEventListener("input", (e) => {
|
|
playbackTime = parseFloat(slider.value);
|
|
lastResumeTimestamp = Date.now();
|
|
updateCurrentSelection();
|
|
updateTimeDisplay();
|
|
debugLog("Slider adjusted to: " + playbackTime.toFixed(2) + " sec.");
|
|
});
|
|
|
|
// Create a container for the tick labels below the slider
|
|
const tickLabelsContainer = document.createElement("div");
|
|
tickLabelsContainer.style.display = "flex";
|
|
tickLabelsContainer.style.justifyContent = "space-between";
|
|
tickLabelsContainer.style.width = "100%";
|
|
tickLabelsContainer.style.marginTop = "5px";
|
|
|
|
// Create a small display area to show playback time
|
|
const timeDisplay = document.createElement("div");
|
|
timeDisplay.classList.add("vrch-srt-small-display");
|
|
timeDisplay.textContent = "Current time: 0.00 sec";
|
|
|
|
// Append the buttons container, slider, tick labels, and time display to the main container in order
|
|
widgetContainer.appendChild(buttonsContainer);
|
|
widgetContainer.appendChild(slider);
|
|
widgetContainer.appendChild(tickLabelsContainer);
|
|
widgetContainer.appendChild(timeDisplay);
|
|
|
|
// Add the main container as a component to the node
|
|
const widget = node.addDOMWidget("srt_control_widget", "SRT Control", widgetContainer);
|
|
|
|
// Override the computeSize method to force correct sizing
|
|
widget.computeSize = function(width) {
|
|
return [width, 240];
|
|
};
|
|
|
|
// Listen for changes to the loop widget
|
|
if (loopWidget) {
|
|
loopWidget.callback = (value) => {
|
|
shouldLoop = value;
|
|
debugLog("Loop setting changed to: " + value);
|
|
};
|
|
}
|
|
|
|
// If the srt_text widget value changes, re-parse and update the slider's max value and tick labels
|
|
if (srtTextWidget) {
|
|
srtTextWidget.callback = (value) => {
|
|
srtEntries = parseSRTText(value);
|
|
if (srtEntries.length > 0) {
|
|
slider.max = srtEntries[srtEntries.length - 1].end;
|
|
createTickLabels(srtEntries[srtEntries.length - 1].end);
|
|
} else {
|
|
slider.max = 0;
|
|
createTickLabels(0);
|
|
}
|
|
debugLog("srt_text updated, slider max set to: " + slider.max);
|
|
};
|
|
}
|
|
|
|
// Clear the timer when the node is removed
|
|
const onRemoved = this.onRemoved;
|
|
this.onRemoved = function () {
|
|
if (timerId) {
|
|
clearInterval(timerId);
|
|
timerId = null;
|
|
}
|
|
return onRemoved?.();
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
// Insert custom CSS styles
|
|
const srtButtonStyle = document.createElement("style");
|
|
srtButtonStyle.textContent = `
|
|
.vrch-srt-big-button {
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
border: none;
|
|
border-radius: 8px;
|
|
width: 100%;
|
|
height: 60px;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
transition: background-color 0.3s, transform 0.2s;
|
|
margin-top: 20px;
|
|
padding: 8px 16px;
|
|
}
|
|
|
|
.vrch-srt-big-button:hover {
|
|
background-color: #45a049;
|
|
}
|
|
|
|
.vrch-srt-big-button:active {
|
|
background-color: #3e8e41;
|
|
}
|
|
|
|
/* Grey style for the Reset button */
|
|
.vrch-srt-reset-button {
|
|
height: 40px;
|
|
margin-top: 10px;
|
|
background-color: #757575;
|
|
}
|
|
|
|
.vrch-srt-reset-button:hover {
|
|
background-color: #616161;
|
|
}
|
|
|
|
.vrch-srt-reset-button:active {
|
|
background-color: #424242;
|
|
}
|
|
|
|
/* Style for the slider */
|
|
.vrch-srt-slider {
|
|
width: 100%;
|
|
margin-top: 30px; /* Additional space from the buttons */
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
background: transparent;
|
|
}
|
|
|
|
/* Slider track (default gray) */
|
|
.vrch-srt-slider::-webkit-slider-runnable-track {
|
|
height: 6px;
|
|
background: #ddd;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
.vrch-srt-slider::-moz-range-track {
|
|
height: 6px;
|
|
background: #ddd;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Slider thumb (solid theme color), with basic hover/active animation */
|
|
.vrch-srt-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 24px;
|
|
height: 24px;
|
|
background: #4CAF50;
|
|
border: none;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
margin-top: -9px; /* center on the track (half thumb size + half track height) */
|
|
transition: transform 0.2s, background 0.2s;
|
|
}
|
|
.vrch-srt-slider::-webkit-slider-thumb:hover {
|
|
transform: scale(1.1);
|
|
background: #45a049;
|
|
}
|
|
.vrch-srt-slider::-webkit-slider-thumb:active {
|
|
transform: scale(1.2);
|
|
background: #3e8e41;
|
|
}
|
|
|
|
.vrch-srt-slider::-moz-range-thumb {
|
|
width: 24px;
|
|
height: 24px;
|
|
background: #4CAF50;
|
|
border: none;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, background 0.2s;
|
|
}
|
|
.vrch-srt-slider::-moz-range-thumb:hover {
|
|
transform: scale(1.1);
|
|
background: #45a049;
|
|
}
|
|
.vrch-srt-slider::-moz-range-thumb:active {
|
|
transform: scale(1.2);
|
|
background: #3e8e41;
|
|
}
|
|
|
|
/* Small display area for the current time */
|
|
.vrch-srt-small-display {
|
|
font-size: 14px;
|
|
color: #666;
|
|
margin-top: 20px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Tick marker and label styles */
|
|
.vrch-srt-tick-marker {
|
|
width: 2px;
|
|
height: 8px;
|
|
background-color: #333;
|
|
margin-bottom: 2px; /* small gap between marker and label */
|
|
}
|
|
.vrch-srt-tick-label {
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
`;
|
|
document.head.appendChild(srtButtonStyle); |