Merge pull request #134 from ekimekim/mike/thrimbletrimmer/streaming-page

Adds a dedicated streaming page for giffers etc, and more thrimbletrimmer things
pull/136/head
Mike Lang 5 years ago committed by GitHub
commit 1574f319c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -41,9 +41,9 @@
</tr>
<tr>
<td><input id="StreamName" value="desertbus" /></td>
<td><input id="StreamStart" style="display:none;" class="UTCTimeInput" value="" /><input id="BusTimeStart" class="BusTimeInput" value="0:00:00" /></td>
<td><input id="StreamEnd" style="display:none;" class="UTCTimeInput" value="" /><input id="BusTimeEnd" class="BusTimeInput" value="0:00:00" /></td>
<td><input type="button" value="Load Playlist" onclick="loadPlaylist()" /></td>
<td><input id="StreamStart" value="0:10:00" />
<td><input id="StreamEnd" value="" />
<td><input type="button" value="Load Playlist" onclick="loadPlaylist(true)" /></td>
</tr>
<tr>
<td></td>
@ -51,6 +51,7 @@
<td>
<input type="radio" id="BusTimeToggleUTC" name="BusTimeToggle" value="UTC" onclick="toggleTimeInput(this.value)"> UTC
<input type="radio" id="BusTimeToggleBus" name="BusTimeToggle" value="BUSTIME" onclick="toggleTimeInput(this.value)" checked="checked"> Bustime
<input type="radio" id="BusTimeToggleDelay" name="BusTimeToggle" value="AGO" onclick="toggleTimeInput(this.value)" > Time Ago
</td>
<td><a id="AdvancedOptionsButton" href="JavaScript:toggleHiddenPane('wubloaderAdvancedInputTable');">Advanced Submit Options</a></td>
</tr>
@ -93,8 +94,9 @@
</div>
<input type="button" id="SubmitButton" value="Submit" onclick="thrimbletrimmerSubmit('EDITED')"/>
<input type="button" id="DraftButton" value="Save Draft" onclick="thrimbletrimmerSubmit('UNEDITED')"/>
<input type="button" id="DownloadButton" value="Download" onclick="thrimbletrimmerDownload()"/>
<input type="button" id="DownloadButton" value="Download" onclick="thrimbletrimmerDownload(true)"/>
<a href="/thrimbletrimmer/dashboard.html">Go To Dashboard</a> |
<a href="/thrimbletrimmer/stream.html">Go to Re-stream View</a> |
<a id="ManualLinkButton" href="JavaScript:toggleHiddenPane('ManualLinkPane');">Manual Link</a> |
<a id="ResetButton" href="JavaScript:thrimbletrimmerResetLink();">Reset Edits</a>
<a id="HelpButton" style="float:right;" href="JavaScript:toggleHiddenPane('HelpPane');">Help</a>
@ -119,32 +121,7 @@
<script src="/thrimbletrimmer/scripts/keyboardShortcuts.js"></script>
<script src="/thrimbletrimmer/scripts/IO.js"></script>
<script>pageSetup();</script>
<script>
function toggleHiddenPane(paneID) {
var pane = document.getElementById(paneID);
pane.style.display = (pane.style.display === "none") ? "block":"none";
}
function toggleUltrawide() {
var body = document.getElementsByTagName("Body")[0];
body.classList.contains("ultrawide") ? body.classList.remove("ultrawide"):body.classList.add("ultrawide");
}
function toggleTimeInput(toggleInput) {
if(toggleInput == "UTC") {
setStreamRange();
document.getElementById("BusTimeStart").style.display = "none";
document.getElementById("BusTimeEnd").style.display = "none";
document.getElementById("StreamStart").style.display = "";
document.getElementById("StreamEnd").style.display = "";
} else {
setBustimeRange();
document.getElementById("StreamStart").style.display = "none";
document.getElementById("StreamEnd").style.display = "none";
document.getElementById("BusTimeStart").style.display = "";
document.getElementById("BusTimeEnd").style.display = "";
}
}
</script>
<script>pageSetup(true);</script>
<div class="g-signin2" data-onsuccess="onSignIn"></div>
<a href="#" onclick="signOut();">Sign out</a>

@ -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;

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

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>VST Re-stream</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/thrimbletrimmer/plugins/video.js/dist/video-js.min.css" rel="stylesheet">
<link href="/thrimbletrimmer/plugins/videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="/thrimbletrimmer/plugins/video.js/dist/video.min.js"></script>
<script src="/thrimbletrimmer/plugins/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.min.js"></script>
<script src="/thrimbletrimmer/plugins/videojs-hls-quality-selector/dist/videojs-hls-quality-selector.min.js"></script>
<link href="/thrimbletrimmer/styles/style.css" rel="stylesheet">
</head>
<body>
<div style="max-width:1280px;margin:auto;padding-top:25px;">
<table id="wubloaderInputTable">
<tr>
<th>Stream</th>
<th>Start Time</th>
<th>End Time (blank for live)</th>
<th></th>
</tr>
<tr>
<td><input id="StreamName" value="desertbus" /></td>
<td><input id="StreamStart" value="0:10:00" />
<td><input id="StreamEnd" value="" />
<td><input type="button" value="Load Playlist" onclick="loadPlaylist()" /></td>
</tr>
<tr>
<td></td>
<td></td>
<td>
<input type="radio" id="BusTimeToggleUTC" name="BusTimeToggle" value="UTC" onclick="toggleTimeInput(this.value)"> UTC
<input type="radio" id="BusTimeToggleBus" name="BusTimeToggle" value="BUSTIME" onclick="toggleTimeInput(this.value)"> Bustime
<input type="radio" id="BusTimeToggleDelay" name="BusTimeToggle" value="AGO" onclick="toggleTimeInput(this.value)" checked="checked"> Time Ago
</td>
</tr>
</table>
<div id="EditorContainer">
<video id="my-player" class="video-js" controls preload="auto" style="display:none">
<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
</video>
</div>
<div id="InfoPane">
<br/>
When no end time is set, the stream will continue to play as it arrives.</br>
Trying to watch live will result in buffering, as those segments haven't been captured yet.<br/>
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.<br/>
Download Quality: <select id="qualityLevel"></select>
<input type="button" id="DownloadButton" value="Download this time range" onclick="thrimbletrimmerDownload()"/>
<a href="/thrimbletrimmer/dashboard.html">Go To Dashboard</a> |
<a href="/thrimbletrimmer/dashboard.html">Go To Editor</a>
<a id="HelpButton" style="float:right;" href="JavaScript:toggleHiddenPane('HelpPane');">Help</a>
<a id="UltrawideButton" style="float:right;margin-right:10px;" href="JavaScript:toggleUltrawide();">Ultrawide</a>
</div>
<div id="HelpPane" style="display:none;">
<ul>
<li>J/K/L - Back 10 seconds, Play/Pause, Advance 10 seconds</li>
<li>LeftArrow/RightArrow - Back 5 seconds, Advance 5 seconds</li>
<li>,/. - Back 0.1 seconds, Advance 0.1 seconds</li>
<li>0-9 - Jump to 0% - 90% through the video.</li>
</ul>
</div>
</div>
<iframe id="outputFile" style="display:none;"></iframe>
<script src="/thrimbletrimmer/scripts/playerSetup.js"></script>
<script src="/thrimbletrimmer/scripts/keyboardShortcuts.js"></script>
<script src="/thrimbletrimmer/scripts/IO.js"></script>
<script>pageSetup();</script>
</body>
</html>

@ -24,6 +24,10 @@ body.ultrawide .my-player-dimensions { width:100% !important; }
.vjs-menu-button-popup .vjs-menu {
bottom:-3px;
}
.video-js .vjs-picture-in-picture-control {
display: none;
}
#EditorDetailsPane {
margin-top:100px;

Loading…
Cancel
Save