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()) {