diff --git a/restreamer/restreamer/main.py b/restreamer/restreamer/main.py
index c6859ed..78fdce9 100644
--- a/restreamer/restreamer/main.py
+++ b/restreamer/restreamer/main.py
@@ -239,8 +239,16 @@ def cut(channel, quality):
A fast cut is much faster but minor artifacting may be present near the start and end.
A fast cut is encoded as MPEG-TS, a full as mp4.
"""
- start = dateutil.parse_utc_only(request.args['start'])
- end = dateutil.parse_utc_only(request.args['end'])
+ start = dateutil.parse_utc_only(request.args['start']) if 'start' in request.args else None
+ end = dateutil.parse_utc_only(request.args['end']) if 'end' in request.args else None
+ if start is None or end is None:
+ # If start or end are not given, use the earliest/latest time available
+ first, last = time_range_for_quality(channel, quality)
+ if start is None:
+ start = first
+ if end is None:
+ end = last
+
if end <= start:
return "End must be after start", 400
diff --git a/thrimbletrimmer/index.html b/thrimbletrimmer/index.html
index eceb298..159db15 100644
--- a/thrimbletrimmer/index.html
+++ b/thrimbletrimmer/index.html
@@ -41,9 +41,9 @@
@@ -93,8 +94,9 @@
-
+
Go To Dashboard |
+ Go to Re-stream View |
Manual Link |
Reset EditsHelp
@@ -119,32 +121,7 @@
-
-
+
Sign out
diff --git a/thrimbletrimmer/scripts/IO.js b/thrimbletrimmer/scripts/IO.js
index 16225e9..6427c72 100644
--- a/thrimbletrimmer/scripts/IO.js
+++ b/thrimbletrimmer/scripts/IO.js
@@ -1,8 +1,10 @@
var desertBusStart = new Date("1970-01-01T00:00:00Z");
+var timeFormat = 'AGO';
+
+pageSetup = function(isEditor) {
-pageSetup = function() {
//Get values from ThrimShim
- if(/id=/.test(document.location.search)) {
+ if(isEditor && /id=/.test(document.location.search)) {
var rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
fetch("/thrimshim/"+rowId).then(data => data.json()).then(function (data) {
if (!data) {
@@ -15,10 +17,14 @@ pageSetup = function() {
document.getElementById("StreamName").value = data.video_channel;
document.getElementById("hiddenSubmissionID").value = data.id;
- // set stream start/end, then copy to bustime inputs
- document.getElementById("StreamStart").value = data.event_start;
- document.getElementById("StreamEnd").value = data.event_end;
- setBustimeRange();
+ // for editor, switch to bustime since that's the default
+ timeFormat = 'BUSTIME';
+ // Apply padding - start 1min early, finish 2min late because these times are generally
+ // rounded down to the minute, so if something ends at "00:10" it might actually end
+ // at 00:10:59 so we should pad to 00:12:00.
+ var start = (data.event_start) ? new Date(fromTimestamp(data.event_start).getTime() - 60*1000) : null;
+ var end = (data.event_end) ? new Date(fromTimestamp(data.event_end).getTime() + 2*60*1000) : null;
+ setTimeRange(start, end);
// title and description both default to row description
document.getElementById("VideoTitle").value = data.video_title ? data.video_title : data.description;
document.getElementById("VideoDescription").value = data.video_description ? data.video_description : data.description;
@@ -41,53 +47,127 @@ pageSetup = function() {
document.getElementById('wubloaderAdvancedInputTable').style.display = "block";
}
- loadPlaylist(data.video_start, data.video_end, data.video_quality);
+ loadPlaylist(isEditor, data.video_start, data.video_end, data.video_quality);
});
}
else {
- document.getElementById('SubmitButton').disabled = true;
+ if (isEditor) { document.getElementById('SubmitButton').disabled = true; }
fetch("/thrimshim/defaults").then(data => data.json()).then(function (data) {
desertBusStart = new Date(data.bustime_start);
- document.getElementById("VideoTitlePrefix").value = data.title_prefix;
- document.getElementById("VideoTitle").setAttribute("maxlength", data.title_max_length);
document.getElementById("StreamName").value = data.video_channel;
- setOptions('uploadLocation', data.upload_locations);
+ if (isEditor) {
+ document.getElementById("VideoTitlePrefix").value = data.title_prefix;
+ document.getElementById("VideoTitle").setAttribute("maxlength", data.title_max_length);
+ setOptions('uploadLocation', data.upload_locations);
+ }
+
+ // Default time format changes depending on mode.
+ // But in both cases the default input value is 10min ago / "",
+ // it's just for editor we convert it before the user sees.
+ if (isEditor) {
+ toggleTimeInput('BUSTIME');
+ }
- // Default time range to the last 10min. This is useful for giffers, immediate replay, etc.
- document.getElementById("StreamStart").value = new Date(new Date().getTime() - 1000*60*10).toISOString().substring(0,19);
- document.getElementById("StreamEnd").value = new Date().toISOString().substring(0,19);
- setBustimeRange();
+ loadPlaylist(isEditor);
});
- loadPlaylist();
}
};
-timestampToBustime = function(ts) {
- date = new Date(ts + "Z");
- return (date < desertBusStart ? "-":"") + videojs.formatTime(Math.abs((date - desertBusStart)/1000), 600.01).padStart(7, "0:");
-};
+// Time-formatting functions
-bustimeToTimestamp = function(bustime) {
- direction = 1;
- if(bustime.startsWith("-")) {
- bustime = bustime.slice(1);
+parseDuration = function(duration) {
+ var direction = 1;
+ if(duration.startsWith("-")) {
+ duration = duration.slice(1);
direction = -1;
}
- parts = bustime.split(':')
- bustime_ms = (parseInt(parts[0]) + parts[1]/60 + parts[2]/3600) * 1000 * 60 * 60;
- return new Date(desertBusStart.getTime() + direction * bustime_ms).toISOString().substring(0, 19);
+ var parts = duration.split(':');
+ return (parseInt(parts[0]) + (parts[1] || "0")/60 + (parts[2] || "0")/3600) * 60 * 60 * direction;
+}
+
+toBustime = function(date) {
+ return (date < desertBusStart ? "-":"") + videojs.formatTime(Math.abs((date - desertBusStart)/1000), 600.01).padStart(7, "0:");
};
-setBustimeRange = function() {
- document.getElementById("BusTimeStart").value = timestampToBustime(document.getElementById("StreamStart").value);
- document.getElementById("BusTimeEnd").value = timestampToBustime(document.getElementById("StreamEnd").value);
+fromBustime = function(bustime) {
+ return new Date(desertBusStart.getTime() + 1000 * parseDuration(bustime));
};
-setStreamRange = function() {
- document.getElementById("StreamStart").value = bustimeToTimestamp(document.getElementById("BusTimeStart").value);
- document.getElementById("StreamEnd").value = bustimeToTimestamp(document.getElementById("BusTimeEnd").value);
+toTimestamp = function(date) {
+ return date.toISOString().substring(0, 19);
+}
+
+fromTimestamp = function(ts) {
+ return new Date(ts + "Z");
+}
+
+toAgo = function(date) {
+ now = new Date()
+ return (date < now ? "":"-") + videojs.formatTime(Math.abs((date - now)/1000), 600.01).padStart(7, "0:");
+}
+
+fromAgo = function(ago) {
+ return new Date(new Date().getTime() - 1000 * parseDuration(ago));
+}
+
+// Set the stream start/end range from a pair of Dates using the current format
+// If given null, sets to blank.
+setTimeRange = function(start, end) {
+ var toFunc = {
+ UTC: toTimestamp,
+ BUSTIME: toBustime,
+ AGO: toAgo,
+ }[timeFormat];
+ document.getElementById("StreamStart").value = (start) ? toFunc(start) : "";
+ document.getElementById("StreamEnd").value = (end) ? toFunc(end) : "";
+}
+
+// Get the current start/end range as Dates using the current format
+// Returns an object containing 'start' and 'end' fields.
+// If either is empty / invalid, returns null.
+getTimeRange = function() {
+ var fromFunc = {
+ UTC: fromTimestamp,
+ BUSTIME: fromBustime,
+ AGO: fromAgo,
+ }[timeFormat];
+ var convert = function(value) {
+ if (!value) { return null; }
+ var date = fromFunc(value);
+ return (isNaN(date)) ? null : date;
+ };
+ return {
+ start: convert(document.getElementById("StreamStart").value),
+ end: convert(document.getElementById("StreamEnd").value),
+ };
+}
+
+getTimeRangeAsTimestamp = function() {
+ var range = getTimeRange();
+ return {
+ // if not null, format as timestamp
+ start: range.start && toTimestamp(range.start),
+ end: range.end && toTimestamp(range.end),
+ };
+}
+
+toggleHiddenPane = function(paneID) {
+ var pane = document.getElementById(paneID);
+ pane.style.display = (pane.style.display === "none") ? "block":"none";
+}
+
+toggleUltrawide = function() {
+ var body = document.getElementsByTagName("Body")[0];
+ body.classList.contains("ultrawide") ? body.classList.remove("ultrawide"):body.classList.add("ultrawide");
+}
+
+toggleTimeInput = function(toggleInput) {
+ // Get times using current format, then change format, then write them back
+ var range = getTimeRange();
+ timeFormat = toggleInput;
+ setTimeRange(range.start, range.end);
}
// For a given select input element id, add the given list of options.
@@ -102,21 +182,21 @@ setOptions = function(element, options, selected) {
});
}
-loadPlaylist = function(startTrim, endTrim, defaultQuality) {
- var playlist = "/playlist/" + document.getElementById("StreamName").value + ".m3u8";
+buildQuery = function(params) {
+ return Object.keys(params).filter(key => params[key] !== null).map(key =>
+ encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
+ ).join('&');
+}
- // If we're using bustime, update stream start/end from it first
- if(document.getElementById("BusTimeToggleBus").checked) {
- setStreamRange();
- }
+loadPlaylist = function(isEditor, startTrim, endTrim, defaultQuality) {
+ var playlist = "/playlist/" + document.getElementById("StreamName").value + ".m3u8";
- var streamStart = document.getElementById("StreamStart").value ? "start="+document.getElementById("StreamStart").value:null;
- var streamEnd = document.getElementById("StreamEnd").value ? "end="+document.getElementById("StreamEnd").value:null;
- var queryString = (streamStart || streamEnd) ? "?" + [streamStart, streamEnd].filter((a) => !!a).join("&"):"";
+ var range = getTimeRangeAsTimestamp();
+ var queryString = buildQuery(range);
- setupPlayer(playlist + queryString, startTrim, endTrim);
+ setupPlayer(isEditor, playlist + '?' + queryString, startTrim, endTrim);
- //Get quality levels for advanced properties.
+ //Get quality levels for advanced properties / download
document.getElementById('qualityLevel').innerHTML = "";
fetch('/files/' + document.getElementById('StreamName').value).then(data => data.json()).then(function (data) {
if (!data.length) {
@@ -165,7 +245,7 @@ thrimbletrimmerSubmit = function(state) {
})
.then(response => response.text().then(text => {
if (!response.ok) {
- error = response.statusText + ": " + text;
+ var error = response.statusText + ": " + text;
console.log(error);
alert(error);
} else if (state == 'EDITED') {
@@ -177,29 +257,34 @@ thrimbletrimmerSubmit = function(state) {
}));
};
-thrimbletrimmerDownload = function() {
- if(player.trimmingControls().options.startTrim >= player.trimmingControls().options.endTrim) {
- alert("End Time must be greater than Start Time");
- } else {
+thrimbletrimmerDownload = function(isEditor) {
+ var range = getTimeRangeAsTimestamp();
+ if (isEditor) {
+ if(player.trimmingControls().options.startTrim >= player.trimmingControls().options.endTrim) {
+ alert("End Time must be greater than Start Time");
+ return;
+ }
var discontinuities = mapDiscontinuities();
-
- var downloadStart = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.startTrim);
- var downloadEnd = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.endTrim);
-
- var targetURL = "/cut/" + document.getElementById("StreamName").value +
- "/"+document.getElementById('qualityLevel').options[document.getElementById('qualityLevel').options.selectedIndex].value+".ts" +
- "?start=" + downloadStart +
- "&end=" + downloadEnd +
- "&allow_holes=" + String(document.getElementById('AllowHoles').checked);
- console.log(targetURL);
- document.getElementById('outputFile').src = targetURL;
+ range.start = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.startTrim);
+ range.end = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.endTrim);
}
+
+ var targetURL = "/cut/" + document.getElementById("StreamName").value +
+ "/"+document.getElementById('qualityLevel').options[document.getElementById('qualityLevel').options.selectedIndex].value+".ts" +
+ "?" + buildQuery({
+ start: range.start,
+ end: range.end,
+ // Always allow holes in non-editor, accidentially including holes isn't important
+ allow_holes: (isEditor) ? String(document.getElementById('AllowHoles').checked) : "true",
+ });
+ console.log(targetURL);
+ document.getElementById('outputFile').src = targetURL;
};
thrimbletrimmerManualLink = function() {
document.getElementById("ManualButton").disabled = true;
var rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
- body = {link: document.getElementById("ManualLink").value};
+ var body = {link: document.getElementById("ManualLink").value};
if (!!user) {
body.token = user.getAuthResponse().id_token;
}
@@ -213,7 +298,7 @@ thrimbletrimmerManualLink = function() {
})
.then(response => response.text().then(text => {
if (!response.ok) {
- error = response.statusText + ": " + text;
+ var error = response.statusText + ": " + text;
console.log(error);
alert(error);
document.getElementById("ManualButton").disabled = false;
@@ -236,7 +321,7 @@ thrimbletrimmerResetLink = function() {
return;
}
document.getElementById("ResetButton").disabled = true;
- body = {}
+ var body = {}
if (!!user) {
body.token = user.getAuthResponse().id_token;
}
@@ -250,7 +335,7 @@ thrimbletrimmerResetLink = function() {
})
.then(response => response.text().then(text => {
if (!response.ok) {
- error = response.statusText + ": " + text;
+ var error = response.statusText + ": " + text;
console.log(error);
alert(error);
document.getElementById("ResetButton").disabled = false;
diff --git a/thrimbletrimmer/scripts/playerSetup.js b/thrimbletrimmer/scripts/playerSetup.js
index 77f2c86..35776f2 100644
--- a/thrimbletrimmer/scripts/playerSetup.js
+++ b/thrimbletrimmer/scripts/playerSetup.js
@@ -1,6 +1,6 @@
var player = null;
-function setupPlayer(source, startTrim, endTrim) {
+function setupPlayer(isEditor, source, startTrim, endTrim) {
document.getElementById("my-player").style.display = "";
//Make poster of DB logo in correct aspect ratio, to control initial size of fluid container.
var options = {
@@ -14,7 +14,7 @@ function setupPlayer(source, startTrim, endTrim) {
playbackRates: [0.5, 1, 1.25, 1.5, 2],
inactivityTimeout: 0,
controlBar: {
- fullscreenToggle: false,
+ fullscreenToggle: true,
volumePanel: {
inline: false
}
@@ -39,10 +39,12 @@ function setupPlayer(source, startTrim, endTrim) {
this.vhs.playlists.on('loadedmetadata', function() {
// setTimeout(function() { player.play(); }, 1000);
player.hasStarted(true); //So it displays all the controls.
- stream_start = player.vhs.playlists.master.playlists.filter(playlist => typeof playlist.discontinuityStarts !== "undefined")[0].dateTimeObject;
- startTrim = startTrim ? (new Date(startTrim+"Z")-stream_start)/1000:0;
- endTrim = endTrim ? (new Date(endTrim+"Z")-stream_start)/1000:player.duration();
- var trimmingControls = player.trimmingControls({ startTrim:startTrim, endTrim:endTrim });
+ if (isEditor) {
+ var stream_start = player.vhs.playlists.master.playlists.filter(playlist => typeof playlist.discontinuityStarts !== "undefined")[0].dateTimeObject;
+ startTrim = startTrim ? (new Date(startTrim+"Z")-stream_start)/1000:0;
+ endTrim = endTrim ? (new Date(endTrim+"Z")-stream_start)/1000:player.duration();
+ var trimmingControls = player.trimmingControls({ startTrim:startTrim, endTrim:endTrim });
+ }
});
// How about an event listener?
@@ -56,7 +58,6 @@ function setupPlayer(source, startTrim, endTrim) {
})
});
var hlsQS = player.hlsQualitySelector();
- //var trimmingControls = player.trimmingControls({ startTrim:(startTrim ? startTrim:0), endTrim:(endTrim ? endTrim:player.duration()) });
}
mapDiscontinuities = function() {
diff --git a/thrimbletrimmer/stream.html b/thrimbletrimmer/stream.html
new file mode 100644
index 0000000..152490a
--- /dev/null
+++ b/thrimbletrimmer/stream.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ VST Re-stream
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Stream
+
Start Time
+
End Time (blank for live)
+
+
+
+
+
+
+
+
+
+
+
+
+ UTC
+ Bustime
+ Time Ago
+
+
+
+
+
+
+
+
+
+
+ When no end time is set, the stream will continue to play as it arrives.
+ Trying to watch live will result in buffering, as those segments haven't been captured yet.
+ If you watch for a long time, it may become difficult to navigate on the video bar because there's too long a time loaded.
+ To fix this, re-load the video in the desired time range (default: the last 10 minutes) by clicking Load Playlist.
+ Download Quality:
+
+ Go To Dashboard |
+ Go To Editor
+ Help
+ Ultrawide
+
+
+
+
J/K/L - Back 10 seconds, Play/Pause, Advance 10 seconds
+
LeftArrow/RightArrow - Back 5 seconds, Advance 5 seconds