Implement chat transcript for editor and chat replay for restreamer page

pull/305/head
ElementalAlchemist 2 years ago committed by Mike Lang
parent 2939089edd
commit dc4e7f0835

@ -330,6 +330,8 @@
<div id="google-auth-sign-in" class="g-signin2" data-onsuccess="googleOnSignIn"></div>
<a href="#" id="google-auth-sign-out" class="hidden">Sign Out of Google Account</a>
</div>
<div id="chat-replay"></div>
</div>
</body>
</html>

@ -160,6 +160,8 @@
</div>
<button type="submit">Convert Times</button>
</form>
<div id="chat-replay"></div>
</div>
</body>
</html>

@ -11,6 +11,8 @@ var globalPlayer = null;
var globalSetUpControls = false;
var globalSeekTimer = null;
var globalChatData = [];
Hls.DefaultConfig.maxBufferHole = 600;
const VIDEO_FRAMES_PER_SECOND = 30;
@ -348,30 +350,6 @@ function busTimeFromWubloaderTime(wubloaderTime) {
return busTimeFromDateTime(dt);
}
function assembleVideoPlaylistURL(basePlaylistURL) {
let playlistURL = basePlaylistURL;
const queryStringParts = startAndEndTimeQueryStringParts();
if (queryStringParts) {
playlistURL += "?" + queryStringParts.join("&");
}
return playlistURL;
}
function startAndEndTimeQueryStringParts() {
const startTime = getStartTime();
const endTime = getEndTime();
let queryStringParts = [];
if (startTime) {
queryStringParts.push(`start=${wubloaderTimeFromDateTime(startTime)}`);
}
if (endTime) {
queryStringParts.push(`end=${wubloaderTimeFromDateTime(endTime)}`);
}
return queryStringParts;
}
function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) {
const hours = Math.floor(videoPlayerTime / 3600);
let minutes = Math.floor((videoPlayerTime % 3600) / 60);
@ -420,10 +398,6 @@ function videoPlayerTimeFromVideoHumanTime(videoHumanTime) {
return hours * 3600 + minutes * 60 + seconds;
}
function getSegmentList() {
return globalPlayer.latencyController.levelDetails.fragments;
}
function dateTimeFromVideoPlayerTime(videoPlayerTime) {
const segmentList = getSegmentList();
let segmentStartTime;
@ -444,10 +418,72 @@ function dateTimeFromVideoPlayerTime(videoPlayerTime) {
return wubloaderDateTime.plus({ seconds: offset });
}
function videoPlayerTimeFromDateTime(dateTime) {
const segmentList = getSegmentList();
for (const segment of segmentList) {
const segmentStart = DateTime.fromISO(segment.rawProgramDateTime);
const segmentEnd = segmentStart.plus({ seconds: segment.duration });
if (dateTime >= segmentStart && dateTime <= segmentEnd) {
return segment.start + dateTime.diff(segmentStart).as("seconds");
}
}
return null;
}
function videoHumanTimeFromDateTime(dateTime) {
const videoPlayerTime = videoPlayerTimeFromDateTime(dateTime);
if (videoPlayerTime === null) {
return null;
}
return videoHumanTimeFromVideoPlayerTime(videoPlayerTime);
}
function assembleVideoPlaylistURL(basePlaylistURL) {
let playlistURL = basePlaylistURL;
const queryStringParts = startAndEndTimeQueryStringParts();
if (queryStringParts) {
playlistURL += "?" + queryStringParts.join("&");
}
return playlistURL;
}
function startAndEndTimeQueryStringParts() {
const startTime = getStartTime();
const endTime = getEndTime();
let queryStringParts = [];
if (startTime) {
queryStringParts.push(`start=${wubloaderTimeFromDateTime(startTime)}`);
}
if (endTime) {
queryStringParts.push(`end=${wubloaderTimeFromDateTime(endTime)}`);
}
return queryStringParts;
}
function getSegmentList() {
return globalPlayer.latencyController.levelDetails.fragments;
}
function hasSegmentList() {
if (
globalPlayer &&
globalPlayer.latencyController &&
globalPlayer.latencyController.levelDetails &&
globalPlayer.latencyController.levelDetails.fragments
) {
return true;
}
return false;
}
function downloadFrame() {
const videoElement = document.getElementById("video");
const dateTime = dateTimeFromVideoPlayerTime(videoElement.currentTime);
const url = `/frame/${globalStreamName}/source.png?timestamp=${wubloaderTimeFromDateTime(dateTime)}`;
const url = `/frame/${globalStreamName}/source.png?timestamp=${wubloaderTimeFromDateTime(
dateTime
)}`;
// Avoid : as it causes problems on Windows
const filename = `${dateTime.toFormat("yyyy-LL-dd'T'HH-mm-ss.SSS")}.png`;
triggerDownload(url, filename);
@ -455,11 +491,119 @@ function downloadFrame() {
function triggerDownload(url, filename) {
// URL must be same-origin.
const link = document.createElement('a');
link.setAttribute('download', filename);
const link = document.createElement("a");
link.setAttribute("download", filename);
link.href = url;
link.setAttribute('target', '_blank');
link.setAttribute("target", "_blank");
document.body.appendChild(link);
link.click();
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) {
return;
}
const chatLogData = await response.json();
for (const chatLine of chatLogData) {
if (
chatLine.command !== "PRIVMSG" &&
chatLine.command !== "CLEARMSG" &&
chatLine.command !== "CLEARCHAT"
) {
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 });
}
}
function renderChatMessage(chatMessageData) {
const chatMessage = chatMessageData.message;
if (chatMessage.command !== "PRIVMSG" || chatMessage.params[0] !== `#${globalStreamName}`) {
return null;
}
const sendTimeElement = document.createElement("div");
sendTimeElement.classList.add("chat-replay-message-time");
const messageTime = videoHumanTimeFromDateTime(chatMessageData.when);
sendTimeElement.innerText = messageTime;
var displayName = "";
if (chatMessage.tags.hasOwnProperty("display-name")) {
displayName = chatMessage.tags["display-name"];
} else {
displayName = chatMessage.sender;
}
const senderNameElement = document.createElement("div");
senderNameElement.classList.add("chat-replay-message-sender");
if (chatMessage.tags.hasOwnProperty("color")) {
senderNameElement.style.color = chatMessage.tags.color;
}
senderNameElement.innerText = displayName;
const messageTextElement = document.createElement("div");
messageTextElement.classList.add("chat-replay-message-text");
if (chatMessage.tags.emotes) {
const emoteDataStrings = chatMessage.tags.emotes.split("/");
let emotePositions = [];
for (const emoteDataString of emoteDataStrings) {
const emoteData = emoteDataString.split(":", 2);
const emoteID = emoteData[0];
const emotePositionList = emoteData[1].split(",").map((val) => {
const positions = val.split("-");
return { emote: emoteID, start: +positions[0], end: +positions[1] };
});
emotePositions = emotePositions.concat(emotePositionList);
}
emotePositions.sort((a, b) => a.start - b.start);
let messageText = [chatMessage.params[1]];
while (emotePositions.length > 0) {
const emoteData = emotePositions.pop(); // Pop the highest-index element from the array
let text = messageText.shift();
const textAndEmote = [text.substring(0, emoteData.start)];
const emoteImg = document.createElement("img");
emoteImg.src = `https://static-cdn.jtvnw.net/emoticons/v2/${emoteData.emote}/default/dark/1.0`;
const emoteStringLen = emoteData.end - emoteData.start + 1;
const emoteText = text.substring(emoteData.start, emoteStringLen);
emoteImg.alt = emoteText;
emoteImg.title = emoteText;
textAndEmote.push(emoteImg);
const remainingText = text.substring(emoteData.end + 1);
if (remainingText !== "") {
textAndEmote.push(remainingText);
}
messageText = textAndEmote.concat(messageText);
}
for (const messagePart of messageText) {
if (typeof messagePart === "string") {
const node = document.createTextNode(messagePart);
messageTextElement.appendChild(node);
} else {
messageTextElement.appendChild(messagePart);
}
}
} else {
messageTextElement.innerText = chatMessage.params[1];
}
const messageContainer = document.createElement("div");
messageContainer.classList.add("chat-replay-message");
if (chatMessage.tags.hasOwnProperty("id")) {
messageContainer.id = `chat-replay-message-${chatMessage.tags.id}`;
}
messageContainer.dataset.sender = chatMessage.sender;
messageContainer.appendChild(sendTimeElement);
messageContainer.appendChild(senderNameElement);
messageContainer.appendChild(messageTextElement);
return messageContainer;
}

@ -9,7 +9,7 @@ window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup();
const timeUpdateForm = document.getElementById("stream-time-settings");
timeUpdateForm.addEventListener("submit", (event) => {
timeUpdateForm.addEventListener("submit", async (event) => {
event.preventDefault();
if (!videoInfo) {
@ -126,6 +126,10 @@ window.addEventListener("DOMContentLoaded", async (event) => {
updateWaveform();
waveformImage.classList.remove("hidden");
}
await getChatLog(globalStartTimeString, globalEndTimeString);
document.getElementById("chat-replay").innerHTML = "";
renderChatLog();
});
await loadVideoInfo();
@ -338,6 +342,20 @@ 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() {
@ -355,7 +373,7 @@ async function loadVideoInfo() {
return;
}
videoInfo = await dataResponse.json();
initializeVideoInfo();
await initializeVideoInfo();
}
async function initializeVideoInfo() {
@ -1544,6 +1562,39 @@ function changeEnableChaptersHandler() {
}
}
async function loadEditorChatData() {
if (!globalStartTimeString || !globalEndTimeString) {
return [];
}
return getChatLog(globalStartTimeString, globalEndTimeString);
}
function renderChatLog() {
const chatReplayParent = document.getElementById("chat-replay");
chatReplayParent.innerHTML = "";
for (const chatMessage of globalChatData) {
if (chatMessage.message.command === "PRIVMSG") {
const chatDOM = renderChatMessage(chatMessage);
if (chatDOM) {
chatReplayParent.appendChild(chatDOM);
}
} else if (chatMessage.message.command === "CLEARMSG") {
const removedMessageID = chatMessage.message.tags["target-msg-id"];
const removedMessageElem = document.getElementById(`chat-replay-message-${removedMessageID}`);
if (removedMessageElem) {
removedMessageElem.classList.add("chat-replay-message-cleared");
}
} else if (chatMessage.message.command === "CLEARCHAT") {
const removedSender = chatMessage.message.params[1];
for (const childNode of document.getElementById("chat-replay").children) {
if (childNode.dataset.sender === removedSender) {
childNode.classList.add("chat-replay-message-cleared");
}
}
}
}
}
function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
const wubloaderDateTime = dateTimeFromWubloaderTime(wubloaderTime);
const segmentList = getSegmentList();
@ -1571,18 +1622,6 @@ function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
return null;
}
function videoPlayerTimeFromDateTime(dateTime) {
const segmentList = getSegmentList();
for (const segment of segmentList) {
const segmentStart = DateTime.fromISO(segment.rawProgramDateTime);
const segmentEnd = segmentStart.plus({ seconds: segment.duration });
if (dateTime >= segmentStart && dateTime <= segmentEnd) {
return segment.start + dateTime.diff(segmentStart).as("seconds");
}
}
return null;
}
function dateTimeFromVideoHumanTime(videoHumanTime) {
const videoPlayerTime = videoPlayerTimeFromVideoHumanTime(videoHumanTime);
if (videoPlayerTime === null) {
@ -1591,14 +1630,6 @@ function dateTimeFromVideoHumanTime(videoHumanTime) {
return dateTimeFromVideoPlayerTime(videoPlayerTime);
}
function videoHumanTimeFromDateTime(dateTime) {
const videoPlayerTime = videoPlayerTimeFromDateTime(dateTime);
if (videoPlayerTime === null) {
return null;
}
return videoHumanTimeFromVideoPlayerTime(videoPlayerTime);
}
function wubloaderTimeFromVideoPlayerTime(videoPlayerTime) {
const dt = dateTimeFromVideoPlayerTime(videoPlayerTime);
return wubloaderTimeFromDateTime(dt);

@ -4,6 +4,7 @@ const TIME_FRAME_AGO = 3;
var globalLoadedVideoPlayer = false;
var globalVideoTimeReference = TIME_FRAME_AGO;
var globalChatPreviousRenderTime = null;
window.addEventListener("DOMContentLoaded", async (event) => {
commonPageSetup();
@ -55,6 +56,11 @@ window.addEventListener("DOMContentLoaded", async (event) => {
});
updateTimeSettings();
await getStreamChatLog();
const videoPlayer = document.getElementById("video");
videoPlayer.addEventListener("loadedmetadata", (_event) => initialChatRender());
videoPlayer.addEventListener("timeupdate", (_event) => updateChatRender());
});
async function loadDefaults() {
@ -222,3 +228,94 @@ function convertEnteredTimes() {
document.getElementById("time-converter-from-ago").checked = true;
}
}
async function getStreamChatLog() {
const startTime = getStartTime();
const endTime = getEndTime();
if (!startTime || !endTime) {
return;
}
return getChatLog(wubloaderTimeFromDateTime(startTime), wubloaderTimeFromDateTime(endTime));
}
function initialChatRender() {
if (!globalChatData) {
return;
}
const videoPlayer = document.getElementById("video");
const videoTime = videoPlayer.currentTime;
const videoDateTime = dateTimeFromVideoPlayerTime(videoTime);
const chatReplayContainer = document.getElementById("chat-replay");
chatReplayContainer.innerHTML = "";
for (const chatMessage of globalChatData) {
if (chatMessage.when > videoDateTime) {
break;
}
handleChatMessage(chatReplayContainer, chatMessage);
}
globalChatPreviousRenderTime = videoTime;
}
function updateChatRender() {
if (!globalChatData) {
return;
}
const videoPlayer = document.getElementById("video");
const videoTime = videoPlayer.currentTime;
if (videoTime < globalChatPreviousRenderTime) {
initialChatRender();
} else {
const videoDateTime = dateTimeFromVideoPlayerTime(videoTime);
const lastAddedTime = dateTimeFromVideoPlayerTime(globalChatPreviousRenderTime);
const chatReplayContainer = document.getElementById("chat-replay");
let rangeMin = 0;
let rangeMax = globalChatData.length;
let lastChatIndex = Math.floor((rangeMin + rangeMax) / 2);
while (rangeMax - rangeMin > 1) {
if (globalChatData[lastChatIndex].when === lastAddedTime) {
break;
}
if (globalChatData[lastChatIndex].when < lastAddedTime) {
rangeMin = lastChatIndex;
} else {
rangeMax = lastChatIndex;
}
lastChatIndex = Math.floor((rangeMin + rangeMax) / 2);
}
for (let chatIndex = lastChatIndex + 1; chatIndex < globalChatData.length; chatIndex++) {
const chatMessage = globalChatData[chatIndex];
if (chatMessage.when > videoDateTime) {
break;
}
handleChatMessage(chatReplayContainer, chatMessage);
}
}
globalChatPreviousRenderTime = videoTime;
}
function handleChatMessage(chatReplayContainer, chatMessage) {
if (chatMessage.message.command === "PRIVMSG") {
const chatDOM = renderChatMessage(chatMessage);
if (chatDOM) {
chatReplayContainer.appendChild(chatDOM);
}
} else if (chatMessage.message.command === "CLEARMSG") {
const removedID = chatMessage.message.tags["target-msg-id"];
const targetMessageElem = document.getElementById(`chat-replay-message-${removedID}`);
if (targetMessageElem) {
targetMessageElem.classList.add("chat-replay-message-cleared");
}
} else if (chatMessage.message.command === "CLEARCHAT") {
const removedSender = chatMessage.message.params[1];
for (const messageElem of chatReplayContainer.children) {
if (messageElem.dataset.sender === removedSender) {
messageElem.classList.add("chat-replay-message-cleared");
}
}
}
}

@ -353,3 +353,22 @@ a,
display: block;
width: 200px;
}
.chat-replay-message {
display: flex;
gap: 10px;
}
.chat-replay-message-time {
flex-basis: 110px;
color: #ccc;
text-align: right;
}
.chat-replay-message-cleared {
opacity: 0.5;
}
.chat-replay-message-cleared .chat-replay-message-text {
text-decoration: line-through;
}

Loading…
Cancel
Save