diff --git a/thrimbletrimmer/edit.html b/thrimbletrimmer/edit.html
index f0175a4..dd5ed92 100644
--- a/thrimbletrimmer/edit.html
+++ b/thrimbletrimmer/edit.html
@@ -8,6 +8,7 @@
+
diff --git a/thrimbletrimmer/index.html b/thrimbletrimmer/index.html
index b920527..424be65 100644
--- a/thrimbletrimmer/index.html
+++ b/thrimbletrimmer/index.html
@@ -8,6 +8,7 @@
+
diff --git a/thrimbletrimmer/scripts/chat-load.js b/thrimbletrimmer/scripts/chat-load.js
new file mode 100644
index 0000000..0f6b4ee
--- /dev/null
+++ b/thrimbletrimmer/scripts/chat-load.js
@@ -0,0 +1,49 @@
+self.importScripts("luxon.min.js", "common-worker.js");
+
+var DateTime = luxon.DateTime;
+luxon.Settings.defaultZone = "utc";
+
+self.onmessage = async (event) => {
+ const chatLoadData = event.data;
+
+ const segmentMetadata = chatLoadData.segmentMetadata;
+ for (const segmentData of segmentMetadata) {
+ segmentData.rawStart = DateTime.fromMillis(segmentData.rawStart);
+ segmentData.rawEnd = DateTime.fromMillis(segmentData.rawEnd);
+ }
+
+ const fetchURL = `/${chatLoadData.stream}/chat.json?start=${chatLoadData.start}&end=${chatLoadData.end}`;
+ const chatResponse = await fetch(fetchURL);
+ if (!chatResponse.ok) {
+ return;
+ }
+ const chatRawData = await chatResponse.json();
+
+ const chatData = [];
+ for (const chatLine of chatRawData) {
+ if (
+ chatLine.command !== "PRIVMSG" &&
+ chatLine.command !== "CLEARMSG" &&
+ chatLine.command !== "CLEARCHAT" &&
+ chatLine.command !== "USERNOTICE"
+ ) {
+ continue;
+ }
+ const when = DateTime.fromSeconds(chatLine.time);
+ const displayWhen = videoHumanTimeFromDateTimeWithFragments(segmentMetadata, when);
+ // Here, we just push each line successively into the list. This assumes data is provided to us in chronological order.
+ chatData.push({ message: chatLine, when: when.toMillis(), displayWhen: displayWhen });
+ }
+ self.postMessage(chatData);
+};
+
+function videoHumanTimeFromDateTimeWithFragments(fragmentMetadata, dateTime) {
+ for (const segmentData of fragmentMetadata) {
+ if (dateTime >= segmentData.rawStart && dateTime <= segmentData.rawEnd) {
+ const playerTime =
+ segmentData.playerStart + dateTime.diff(segmentData.rawStart).as("seconds");
+ return videoHumanTimeFromVideoPlayerTime(playerTime);
+ }
+ }
+ return null;
+}
diff --git a/thrimbletrimmer/scripts/common-worker.js b/thrimbletrimmer/scripts/common-worker.js
new file mode 100644
index 0000000..532d556
--- /dev/null
+++ b/thrimbletrimmer/scripts/common-worker.js
@@ -0,0 +1,21 @@
+function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) {
+ const hours = Math.floor(videoPlayerTime / 3600);
+ let minutes = Math.floor((videoPlayerTime % 3600) / 60);
+ let seconds = Math.floor(videoPlayerTime % 60);
+ let milliseconds = Math.floor((videoPlayerTime * 1000) % 1000);
+
+ while (minutes.toString().length < 2) {
+ minutes = `0${minutes}`;
+ }
+ while (seconds.toString().length < 2) {
+ seconds = `0${seconds}`;
+ }
+ while (milliseconds.toString().length < 3) {
+ milliseconds = `0${milliseconds}`;
+ }
+
+ if (hours > 0) {
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`;
+ }
+ return `${minutes}:${seconds}.${milliseconds}`;
+}
diff --git a/thrimbletrimmer/scripts/common.js b/thrimbletrimmer/scripts/common.js
index 84efeba..aff9a22 100644
--- a/thrimbletrimmer/scripts/common.js
+++ b/thrimbletrimmer/scripts/common.js
@@ -12,6 +12,7 @@ var globalSetUpControls = false;
var globalSeekTimer = null;
var globalChatData = [];
+var globalLoadChatWorker = null;
Hls.DefaultConfig.maxBufferHole = 600;
@@ -25,6 +26,8 @@ function commonPageSetup() {
"Your browser doesn't support MediaSource extensions. Video playback and editing won't work."
);
}
+
+ globalLoadChatWorker = new Worker("scripts/chat-load.js");
}
function addError(errorText) {
@@ -50,6 +53,7 @@ async function loadVideoPlayer(playlistURL) {
videoElement.addEventListener("loadedmetadata", (_event) => {
setUpVideoControls();
+ sendChatLogLoadData();
});
videoElement.addEventListener("loadeddata", (_event) => {
@@ -419,12 +423,12 @@ function dateTimeFromVideoPlayerTime(videoPlayerTime) {
}
function videoPlayerTimeFromDateTime(dateTime) {
- const segmentList = getSegmentList();
- for (const segment of segmentList) {
- const segmentStart = DateTime.fromISO(segment.rawProgramDateTime);
- const segmentEnd = segmentStart.plus({ seconds: segment.duration });
+ const segmentTimes = getSegmentTimes();
+ for (const segmentData of segmentTimes) {
+ const segmentStart = segmentData.rawStart;
+ const segmentEnd = segmentData.rawEnd;
if (dateTime >= segmentStart && dateTime <= segmentEnd) {
- return segment.start + dateTime.diff(segmentStart).as("seconds");
+ return segmentData.playerStart + dateTime.diff(segmentStart).as("seconds");
}
}
return null;
@@ -478,6 +482,17 @@ function hasSegmentList() {
return false;
}
+function getSegmentTimes() {
+ const segmentList = getSegmentList();
+ const segmentTimes = [];
+ for (const segment of segmentList) {
+ const segmentStart = DateTime.fromISO(segment.rawProgramDateTime);
+ const segmentEnd = segmentStart.plus({ seconds: segment.duration });
+ segmentTimes.push({ rawStart: segmentStart, rawEnd: segmentEnd, playerStart: segment.start });
+ }
+ return segmentTimes;
+}
+
function downloadFrame() {
const videoElement = document.getElementById("video");
const dateTime = dateTimeFromVideoPlayerTime(videoElement.currentTime);
@@ -500,27 +515,32 @@ function triggerDownload(url, filename) {
document.body.removeChild(link);
}
-async function getChatLog(startWubloaderTime, endWubloaderTime) {
- globalChatData = [];
- const url = `/${globalStreamName}/chat.json?start=${startWubloaderTime}&end=${endWubloaderTime}`;
- const response = await fetch(url);
- if (!response.ok) {
+function sendChatLogLoadData() {
+ const startTime = globalStartTimeString;
+ const endTime = globalEndTimeString;
+ if (!startTime || !endTime) {
return;
}
- const chatLogData = await response.json();
- for (const chatLine of chatLogData) {
- if (
- chatLine.command !== "PRIVMSG" &&
- chatLine.command !== "CLEARMSG" &&
- chatLine.command !== "CLEARCHAT" &&
- chatLine.command !== "USERNOTICE"
- ) {
- continue;
- }
- const when = DateTime.fromSeconds(chatLine.time);
- // Here, we just push each line successively into the list. This assumes data is provided to us in chronological order.
- globalChatData.push({ message: chatLine, when: when });
+ const segmentMetadata = getSegmentTimes();
+ for (const segmentData of segmentMetadata) {
+ segmentData.rawStart = segmentData.rawStart.toMillis();
+ segmentData.rawEnd = segmentData.rawEnd.toMillis();
+ }
+
+ const message = {
+ stream: globalStreamName,
+ start: startTime,
+ end: endTime,
+ segmentMetadata: segmentMetadata,
+ };
+ globalLoadChatWorker.postMessage(message);
+}
+
+function updateChatDataFromWorkerResponse(chatData) {
+ for (const chatLine of chatData) {
+ chatLine.when = DateTime.fromMillis(chatLine.when);
}
+ globalChatData = chatData;
}
function renderChatMessage(chatMessageData) {
@@ -531,7 +551,7 @@ function renderChatMessage(chatMessageData) {
const sendTimeElement = document.createElement("div");
sendTimeElement.classList.add("chat-replay-message-time");
- sendTimeElement.innerText = videoHumanTimeFromDateTime(chatMessageData.when);
+ sendTimeElement.innerText = chatMessageData.displayWhen;
const senderNameElement = createMessageSenderElement(chatMessageData);
@@ -579,7 +599,7 @@ function renderSystemMessages(chatMessageData) {
const sendTimeElement = document.createElement("div");
sendTimeElement.classList.add("chat-replay-message-time");
- sendTimeElement.innerText = videoHumanTimeFromDateTime(chatMessageData.when);
+ sendTimeElement.innerText = chatMessageData.displayWhen;
const systemTextElement = document.createElement("div");
systemTextElement.classList.add("chat-replay-message-text");
diff --git a/thrimbletrimmer/scripts/edit.js b/thrimbletrimmer/scripts/edit.js
index 9c4bdf7..676dcb3 100644
--- a/thrimbletrimmer/scripts/edit.js
+++ b/thrimbletrimmer/scripts/edit.js
@@ -7,6 +7,10 @@ const CHAPTER_MARKER_DELIMITER_PARTIAL = "==========";
window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup();
+ globalLoadChatWorker.onmessage = (event) => {
+ updateChatDataFromWorkerResponse(event.data);
+ renderChatLog();
+ };
const timeUpdateForm = document.getElementById("stream-time-settings");
timeUpdateForm.addEventListener("submit", async (event) => {
@@ -338,20 +342,6 @@ window.addEventListener("DOMContentLoaded", async (event) => {
document.getElementById("google-auth-sign-out").addEventListener("click", (_event) => {
googleSignOut();
});
-
- await loadEditorChatData();
- if (globalChatData) {
- if (hasSegmentList()) {
- renderChatLog();
- } else {
- const videoPlayer = document.getElementById("video");
- const initialChatLogRender = (_event) => {
- renderChatLog();
- videoPlayer.removeEventListener("loadedmetadata", initialChatLogRender);
- };
- videoPlayer.addEventListener("loadedmetadata", initialChatLogRender);
- }
- }
});
async function loadVideoInfo() {
@@ -1540,10 +1530,6 @@ async function rangeDataUpdated() {
}
}
updateDownloadLink();
-
- await getChatLog(globalStartTimeString, globalEndTimeString);
- document.getElementById("chat-replay").innerHTML = "";
- renderChatLog();
}
function setCurrentRangeStartToVideoTime() {
@@ -1591,13 +1577,6 @@ function changeEnableChaptersHandler() {
}
}
-async function loadEditorChatData() {
- if (!globalStartTimeString || !globalEndTimeString) {
- return [];
- }
- return getChatLog(globalStartTimeString, globalEndTimeString);
-}
-
function renderChatLog() {
const chatReplayParent = document.getElementById("chat-replay");
chatReplayParent.innerHTML = "";
diff --git a/thrimbletrimmer/scripts/stream.js b/thrimbletrimmer/scripts/stream.js
index bd01a48..a658c63 100644
--- a/thrimbletrimmer/scripts/stream.js
+++ b/thrimbletrimmer/scripts/stream.js
@@ -8,6 +8,10 @@ var globalChatPreviousRenderTime = null;
window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup();
+ globalLoadChatWorker.onmessage = (event) => {
+ updateChatDataFromWorkerResponse(event.data);
+ initialChatRender();
+ };
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has("start")) {
@@ -124,8 +128,6 @@ async function updateTimeSettings() {
queryParts.push(`end=${wubloaderTimeFromDateTime(endTime)}`);
}
document.getElementById("stream-time-link").href = `?${queryParts.join("&")}`;
-
- await getStreamChatLog();
}
function generateDownloadURL(startTime, endTime, downloadType, allowHoles, quality) {
@@ -230,15 +232,6 @@ function convertEnteredTimes() {
}
}
-async function getStreamChatLog() {
- const startTime = getStartTime();
- const endTime = getEndTime();
- if (!startTime || !endTime) {
- return;
- }
- return getChatLog(wubloaderTimeFromDateTime(startTime), wubloaderTimeFromDateTime(endTime));
-}
-
function initialChatRender() {
if (!globalChatData || globalChatData.length === 0) {
return;
@@ -261,7 +254,7 @@ function initialChatRender() {
}
function updateChatRender() {
- if (!globalChatData || globalChatData === 0) {
+ if (!globalChatData || globalChatData.length === 0) {
return;
}
if (!hasSegmentList()) {