Switch date/time handling from JS Date objects to a datetime library to fix padding bug with DST changeover

pull/239/head
ElementalAlchemist 3 years ago committed by Mike Lang
parent da2ed19b9d
commit d37175f914

@ -1,2 +1,4 @@
videojs/
styles/video-js.min.css
scripts/video.min.js
scripts/luxon.min.js
dashboard.html

@ -8,6 +8,7 @@
<link rel="stylesheet" href="styles/video-js.min.css" />
<script src="scripts/video.min.js"></script>
<script src="scripts/luxon.min.js"></script>
<script src="scripts/common.js"></script>
<script src="scripts/edit.js"></script>
<script src="scripts/keyboard-shortcuts.js"></script>

@ -8,6 +8,7 @@
<link rel="stylesheet" href="styles/video-js.min.css" />
<script src="scripts/video.min.js"></script>
<script src="scripts/luxon.min.js"></script>
<script src="scripts/common.js"></script>
<script src="scripts/stream.js"></script>
<script src="scripts/keyboard-shortcuts.js"></script>

@ -1,4 +1,7 @@
var globalBusStartTime = new Date("1970-01-01T00:00:00Z");
var DateTime = luxon.DateTime;
var Interval = luxon.Interval;
var globalBusStartTime = DateTime.fromISO("1970-01-01T00:00:00", { zone: "utc" });
var globalStreamName = "";
var globalStartTimeString = "";
var globalEndTimeString = "";
@ -87,35 +90,54 @@ function updateVideoPlayer(newPlaylistURL) {
player.src({ src: rangedPlaylistURL });
}
function dateObjFromBusTime(busTime) {
function parseHumanTimeStringAsDateTimeMathObject(inputTime) {
// We need to handle inputs like "-0:10:15" in a way that consistently makes the time negative.
// Since we can't assign the negative sign to any particular part, we'll check for the whole thing here.
let direction = 1;
if (busTime.startsWith("-")) {
busTime = busTime.slice(1);
if (inputTime.startsWith("-")) {
inputTime = inputTime.slice(1);
direction = -1;
}
const parts = busTime.split(":", 3);
const hours = (parts[0] || 0) * direction;
const parts = inputTime.split(":", 3);
const hours = parseInt(parts[0]) * direction;
const minutes = (parts[1] || 0) * direction;
const seconds = (parts[2] || 0) * direction;
const time = new Date(globalBusStartTime);
time.setHours(time.getHours() + hours);
time.setMinutes(time.getMinutes() + minutes);
time.setSeconds(time.getSeconds() + seconds);
return time;
return { hours: hours, minutes: minutes, seconds: seconds };
}
function dateTimeFromBusTime(busTime) {
return globalBusStartTime.plus(parseHumanTimeStringAsDateTimeMathObject(busTime));
}
function busTimeFromDateTime(dateTime) {
const diff = dateTime.diff(globalBusStartTime);
return formatIntervalForDisplay(diff);
}
function dateObjFromWubloaderTime(wubloaderTime) {
return new Date(`${wubloaderTime}Z`);
function formatIntervalForDisplay(interval) {
if (interval.milliseconds < 0) {
const negativeInterval = interval.negate();
return `-${negativeInterval.toFormat("hh:mm:ss.SSS")}`;
}
return interval.toFormat("hh:mm:ss.SSS");
}
function dateTimeFromWubloaderTime(wubloaderTime) {
return DateTime.fromISO(wubloaderTime, { zone: "utc" });
}
function wubloaderTimeFromDateObj(date) {
if (!date) {
function wubloaderTimeFromDateTime(dateTime) {
if (!dateTime) {
return null;
}
return date.toISOString().substring(0, 23); // Trim "Z" marker and smaller than milliseconds
// Not using ISO here because Luxon doesn't give us a quick way to print an ISO8601 string with no offset.
return dateTime.toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
}
function busTimeFromWubloaderTime(wubloaderTime) {
const dt = dateTimeFromWubloaderTime(wubloaderTime);
return busTimeFromDateTime(dt);
}
function assembleVideoPlaylistURL(basePlaylistURL) {
@ -134,10 +156,10 @@ function startAndEndTimeQueryStringParts() {
let queryStringParts = [];
if (startTime) {
queryStringParts.push(`start=${wubloaderTimeFromDateObj(startTime)}`);
queryStringParts.push(`start=${wubloaderTimeFromDateTime(startTime)}`);
}
if (endTime) {
queryStringParts.push(`end=${wubloaderTimeFromDateObj(endTime)}`);
queryStringParts.push(`end=${wubloaderTimeFromDateTime(endTime)}`);
}
return queryStringParts;
}

@ -17,7 +17,7 @@ window.addEventListener("DOMContentLoaded", async (event) => {
}
const newStartField = document.getElementById("stream-time-setting-start");
const newStart = dateObjFromBusTime(newStartField.value);
const newStart = dateTimeFromBusTime(newStartField.value);
if (!newStart) {
addError("Failed to parse start time");
return;
@ -26,7 +26,7 @@ window.addEventListener("DOMContentLoaded", async (event) => {
const newEndField = document.getElementById("stream-time-setting-end");
let newEnd = null;
if (newEndField.value !== "") {
newEnd = dateObjFromBusTime(newEndField.value);
newEnd = dateTimeFromBusTime(newEndField.value);
if (!newEnd) {
addError("Failed to parse end time");
return;
@ -34,8 +34,14 @@ window.addEventListener("DOMContentLoaded", async (event) => {
}
const oldStart = getStartTime();
const startAdjustment = newStart - oldStart;
const newDuration = newEnd === null ? Infinity : newEnd - newStart;
const startAdjustment = newStart.diff(oldStart, "seconds").seconds;
let newDuration = newEnd === null ? Infinity : newEnd.diff(newStart, "seconds").seconds;
// The video duration isn't precisely the video times, but can be padded by up to the
// segment length on either side.
// This makes the assumption that all segments have the same length.
const segmentLength = getPlaylistData().segments[0].duration;
newDuration += segmentLength * 2;
// Abort for ranges that exceed new times
for (const rangeContainer of document.getElementById("range-definitions").children) {
@ -54,8 +60,8 @@ window.addEventListener("DOMContentLoaded", async (event) => {
}
}
globalStartTimeString = wubloaderTimeFromDateObj(newStart);
globalEndTimeString = wubloaderTimeFromDateObj(newEnd);
globalStartTimeString = wubloaderTimeFromDateTime(newStart);
globalEndTimeString = wubloaderTimeFromDateTime(newEnd);
updateSegmentPlaylist();
@ -98,25 +104,17 @@ window.addEventListener("DOMContentLoaded", async (event) => {
document.getElementById("stream-time-setting-start-pad").addEventListener("click", (_event) => {
const startTimeField = document.getElementById("stream-time-setting-start");
let startTime = startTimeField.value;
startTime = dateObjFromBusTime(startTime);
if (isNaN(startTime)) {
addError("Couldn't parse entered start time for padding");
return;
}
startTime.setMinutes(startTime.getMinutes() - 1);
startTimeField.value = busTimeFromDateObj(startTime);
startTime = dateTimeFromBusTime(startTime);
startTime = startTime.minus({ minutes: 1 });
startTimeField.value = busTimeFromDateTime(startTime);
});
document.getElementById("stream-time-setting-end-pad").addEventListener("click", (_event) => {
const endTimeField = document.getElementById("stream-time-setting-end");
let endTime = endTimeField.value;
endTime = dateObjFromBusTime(endTime);
if (isNaN(endTime)) {
addError("Couldn't parse entered end time for padding");
return;
}
endTime.setMinutes(endTime.getMinutes() + 1);
endTimeField.value = busTimeFromDateObj(endTime);
endTime = dateTimeFromBusTime(endTime);
endTime = endTime.plus({ minutes: 1 });
endTimeField.value = busTimeFromDateTime(endTime);
});
const addRangeIcon = document.getElementById("add-range-definition");
@ -225,23 +223,23 @@ async function loadVideoInfo() {
async function initializeVideoInfo() {
globalStreamName = videoInfo.video_channel;
globalBusStartTime = new Date(videoInfo.bustime_start);
globalBusStartTime = DateTime.fromISO(videoInfo.bustime_start, { zone: "utc" });
const eventStartTime = dateObjFromWubloaderTime(videoInfo.event_start);
const eventEndTime = videoInfo.event_end ? dateObjFromWubloaderTime(videoInfo.event_end) : null;
let eventStartTime = dateTimeFromWubloaderTime(videoInfo.event_start);
let eventEndTime = videoInfo.event_end ? dateTimeFromWubloaderTime(videoInfo.event_end) : null;
// To account for various things (stream delay, just slightly off logging, etc.), we pad the start time by one minute
eventStartTime.setMinutes(eventStartTime.getMinutes() - 1);
eventStartTime = eventStartTime.minus({ minutes: 1 });
// To account for various things (stream delay, just slightly off logging, etc.), we pad the end time by one minute.
// To account for the fact that we don't record seconds, but the event could've ended any time in the recorded minute, we pad by an additional minute.
if (eventEndTime) {
eventEndTime.setMinutes(eventEndTime.getMinutes() + 2);
eventEndTime = eventEndTime.plus({ minutes: 2 });
}
globalStartTimeString = wubloaderTimeFromDateObj(eventStartTime);
globalStartTimeString = wubloaderTimeFromDateTime(eventStartTime);
if (eventEndTime) {
globalEndTimeString = wubloaderTimeFromDateObj(eventEndTime);
globalEndTimeString = wubloaderTimeFromDateTime(eventEndTime);
} else {
document.getElementById("waveform").classList.add("hidden");
}
@ -255,42 +253,42 @@ async function initializeVideoInfo() {
let endTime = range[1];
if (startTime) {
startTime = dateObjFromWubloaderTime(startTime);
startTime = dateTimeFromWubloaderTime(startTime);
} else {
startTime = null;
}
if (endTime) {
endTime = dateObjFromWubloaderTime(endTime);
endTime = dateTimeFromWubloaderTime(endTime);
} else {
endTime = null;
}
if (!earliestStartTime || (startTime && startTime < earliestStartTime)) {
if (!earliestStartTime || (startTime && startTime.diff(earliestStartTime).milliseconds < 0)) {
earliestStartTime = startTime;
}
if (!latestEndTime || (endTime && endTime > latestEndTime)) {
if (!latestEndTime || (endTime && endTime.diff(latestEndTime).milliseconds > 0)) {
latestEndTime = endTime;
}
}
if (earliestStartTime && earliestStartTime < eventStartTime) {
earliestStartTime.setMinutes(earliestStartTime.getMinutes() - 1);
globalStartTimeString = wubloaderTimeFromDateObj(earliestStartTime);
if (earliestStartTime && earliestStartTime.diff(eventStartTime).milliseconds < 0) {
earliestStartTime = earliestStartTime.minus({ minutes: 1 });
globalStartTimeString = wubloaderTimeFromDateTime(earliestStartTime);
}
if (latestEndTime && latestEndTime > eventEndTime) {
if (latestEndTime && latestEndTime.diff(eventEndTime).milliseconds > 0) {
// If we're getting the time from a previous draft edit, we have seconds, so one minute is enough
latestEndTime.setMinutes(latestEndTime.getMinutes() + 1);
globalEndTimeString = wubloaderTimeFromDateObj(latestEndTime);
latestEndTime = latestEndTime.plus({ minutes: 1 });
globalEndTimeString = wubloaderTimeFromDateTime(latestEndTime);
}
}
document.getElementById("stream-time-setting-stream").innerText = globalStreamName;
document.getElementById("stream-time-setting-start").value =
getBusTimeFromTimeString(globalStartTimeString);
busTimeFromWubloaderTime(globalStartTimeString);
document.getElementById("stream-time-setting-end").value =
getBusTimeFromTimeString(globalEndTimeString);
busTimeFromWubloaderTime(globalEndTimeString);
updateWaveform();
@ -392,13 +390,11 @@ async function initializeVideoInfo() {
} else {
const rangeStartField =
rangeDefinitionsContainer.getElementsByClassName("range-definition-start")[0];
rangeStartField.value = videoHumanTimeFromVideoPlayerTime(0);
const player = getVideoJS();
const videoDuration = player.duration();
if (isFinite(videoDuration)) {
rangeStartField.value = videoHumanTimeFromWubloaderTime(globalStartTimeString);
if (globalEndTimeString) {
const rangeEndField =
rangeDefinitionsContainer.getElementsByClassName("range-definition-end")[0];
rangeEndField.value = videoHumanTimeFromVideoPlayerTime(videoDuration);
rangeEndField.value = videoHumanTimeFromWubloaderTime(globalEndTimeString);
}
}
rangeDataUpdated();
@ -445,56 +441,14 @@ function getStartTime() {
if (!globalStartTimeString) {
return null;
}
return dateObjFromWubloaderTime(globalStartTimeString);
return dateTimeFromWubloaderTime(globalStartTimeString);
}
function getEndTime() {
if (!globalEndTimeString) {
return null;
}
return dateObjFromWubloaderTime(globalEndTimeString);
}
function getBusTimeFromTimeString(timeString) {
if (timeString === "") {
return "";
}
const time = dateObjFromWubloaderTime(timeString);
return busTimeFromDateObj(time);
}
function busTimeFromDateObj(time) {
const busTimeMilliseconds = time - globalBusStartTime;
let remainingBusTimeSeconds = busTimeMilliseconds / 1000;
let sign = "";
if (remainingBusTimeSeconds < 0) {
sign = "-";
remainingBusTimeSeconds = Math.abs(remainingBusTimeSeconds);
}
const hours = Math.floor(remainingBusTimeSeconds / 3600);
remainingBusTimeSeconds %= 3600;
let minutes = Math.floor(remainingBusTimeSeconds / 60);
let seconds = remainingBusTimeSeconds % 60;
let milliseconds = Math.round((seconds % 1) * 1000);
seconds = Math.trunc(seconds);
while (minutes.toString().length < 2) {
minutes = `0${minutes}`;
}
while (seconds.toString().length < 2) {
seconds = `0${seconds}`;
}
if (milliseconds > 0) {
while (milliseconds.toString().length < 3) {
milliseconds = `0${milliseconds}`;
}
return `${sign}${hours}:${minutes}:${seconds}.${milliseconds}`;
}
return `${sign}${hours}:${minutes}:${seconds}`;
return dateTimeFromWubloaderTime(globalEndTimeString);
}
async function submitVideo() {
@ -632,11 +586,11 @@ function generateDownloadURL(timeRanges, downloadType, allowHoles, quality) {
for (const range of timeRanges) {
let timeRangeString = "";
if (range.hasOwnProperty("start")) {
timeRangeString += wubloaderTimeFromDateObj(range.start);
timeRangeString += range.start;
}
timeRangeString += ",";
if (range.hasOwnProperty("end")) {
timeRangeString += wubloaderTimeFromDateObj(range.end);
timeRangeString += range.end;
}
queryParts.push(`range=${timeRangeString}`);
}
@ -954,14 +908,21 @@ function setCurrentRangeEndToVideoTime() {
function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
const videoPlaylist = getPlaylistData();
const wubloaderDateObj = dateObjFromWubloaderTime(wubloaderTime);
const wubloaderDateTime = dateTimeFromWubloaderTime(wubloaderTime);
let highestDiscontinuitySegmentBefore = 0;
for (start of videoPlaylist.discontinuityStarts) {
const discontinuityStartSegment = videoPlaylist.segments[start];
const discontinuityStartDateTime = DateTime.fromJSDate(
discontinuityStartSegment.dateTimeObject,
{ zone: "utc" }
);
const highestDiscontinuitySegmentDateTime = DateTime.fromJSDate(
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject,
{ zone: "utc" }
);
if (
discontinuityStartSegment.dateTimeObject < wubloaderDateObj &&
discontinuityStartSegment.dateTimeObject >
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject
discontinuityStartDateTime.diff(wubloaderDateTime).milliseconds < 0 && // Discontinuity starts before the provided time
discontinuityStartDateTime.diff(highestDiscontinuitySegmentDateTime).milliseconds > 0 // Discontinuity starts after the latest found discontinuity
) {
highestDiscontinuitySegmentBefore = start;
}
@ -971,16 +932,17 @@ function videoPlayerTimeFromWubloaderTime(wubloaderTime) {
for (let segment = 0; segment < highestDiscontinuitySegmentBefore; segment++) {
highestDiscontinuitySegmentStart += videoPlaylist.segments[segment].duration;
}
const highestDiscontinuitySegmentDateTime = DateTime.fromJSDate(
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject,
{ zone: "utc" }
);
return (
highestDiscontinuitySegmentStart +
secondsDifference(
videoPlaylist.segments[highestDiscontinuitySegmentBefore].dateTimeObject,
wubloaderDateObj
)
wubloaderDateTime.diff(highestDiscontinuitySegmentDateTime, "seconds").seconds
);
}
function wubloaderTimeFromVideoPlayerTime(videoPlayerTime) {
function dateTimeFromVideoPlayerTime(videoPlayerTime) {
const videoPlaylist = getPlaylistData();
let segmentStartTime = 0;
let segmentDateObj;
@ -997,11 +959,14 @@ function wubloaderTimeFromVideoPlayerTime(videoPlayerTime) {
if (segmentDateObj === undefined) {
return null;
}
let wubloaderDateObj = new Date(segmentDateObj);
let wubloaderDateTime = DateTime.fromJSDate(segmentDateObj, { zone: "utc" });
const offset = videoPlayerTime - segmentStartTime;
const offsetMilliseconds = offset * 1000;
wubloaderDateObj.setMilliseconds(wubloaderDateObj.getMilliseconds() + offsetMilliseconds);
return wubloaderDateObj;
return wubloaderDateTime.plus({ seconds: offset });
}
function wubloaderTimeFromVideoPlayerTime(videoPlayerTime) {
const dt = dateTimeFromVideoPlayerTime(videoPlayerTime);
return wubloaderTimeFromDateTime(dt);
}
function videoHumanTimeFromVideoPlayerTime(videoPlayerTime) {
@ -1058,10 +1023,3 @@ function getPlaylistData() {
// etc.), this and all callers will need to be updated.
return player.tech("OK").vhs.playlists.master.playlists[0];
}
function secondsDifference(date1, date2) {
if (date2 > date1) {
return (date2 - date1) / 1000;
}
return (date1 - date2) / 1000;
}

File diff suppressed because one or more lines are too long

@ -29,20 +29,18 @@ async function loadDefaults() {
const streamNameField = document.getElementById("stream-time-setting-stream");
streamNameField.value = defaultData.video_channel;
globalBusStartTime = new Date(defaultData.bustime_start);
globalBusStartTime = DateTime.fromISO(defaultData.bustime_start, { zone: "utc" });
}
// Gets the start time of the video from settings. Returns an invalid date object if the user entered bad data.
function getStartTime() {
switch (globalVideoTimeReference) {
case 1:
return dateObjFromWubloaderTime(globalStartTimeString);
return dateTimeFromWubloaderTime(globalStartTimeString);
case 2:
return dateObjFromBusTime(globalStartTimeString);
return dateTimeFromBusTime(globalStartTimeString);
case 3:
return new Date(
new Date().getTime() - 1000 * parseInputTimeAsNumberOfSeconds(globalStartTimeString)
);
return DateTime.now().minus(parseHumanTimeStringAsDateTimeMathObject(globalStartTimeString));
}
}
@ -53,29 +51,14 @@ function getEndTime() {
}
switch (globalVideoTimeReference) {
case 1:
return dateObjFromWubloaderTime(globalEndTimeString);
return dateTimeFromWubloaderTime(globalEndTimeString);
case 2:
return dateObjFromBusTime(globalEndTimeString);
return dateTimeFromBusTime(globalEndTimeString);
case 3:
return new Date(
new Date().getTime() - 1000 * parseInputTimeAsNumberOfSeconds(globalEndTimeString)
);
return DateTime.now().minus(parseHumanTimeStringAsDateTimeMathObject(globalEndTimeString));
}
}
function parseInputTimeAsNumberOfSeconds(inputTime) {
// We need to handle inputs like "-0:10:15" in a way that consistently makes the time negative.
// Since we can't assign the negative sign to any particular part, we'll check for the whole thing here.
let direction = 1;
if (inputTime.startsWith("-")) {
inputTime = inputTime.slice(1);
direction = -1;
}
const parts = inputTime.split(":", 3);
return (parseInt(parts[0]) + (parts[1] || 0) / 60 + (parts[2] || 0) / 3600) * 60 * 60 * direction;
}
function updateTimeSettings() {
updateStoredTimeSettings();
if (globalLoadedVideoPlayer) {
@ -89,7 +72,7 @@ function updateTimeSettings() {
const startTime = getStartTime();
const endTime = getEndTime();
if (endTime && endTime < startTime) {
if (endTime && endTime.diff(startTime) < 0) {
addError(
"End time is before the start time. This will prevent video loading and cause other problems."
);
@ -97,8 +80,8 @@ function updateTimeSettings() {
}
function generateDownloadURL(startTime, endTime, downloadType, allowHoles, quality) {
const startURLTime = wubloaderTimeFromDateObj(startTime);
const endURLTime = wubloaderTimeFromDateObj(endTime);
const startURLTime = wubloaderTimeFromDateTime(startTime);
const endURLTime = wubloaderTimeFromDateTime(endTime);
const queryParts = [`type=${downloadType}`, `allow_holes=${allowHoles}`];
if (startURLTime) {

Loading…
Cancel
Save