diff --git a/restreamer/restreamer/main.py b/restreamer/restreamer/main.py index a198831..04c77df 100644 --- a/restreamer/restreamer/main.py +++ b/restreamer/restreamer/main.py @@ -411,6 +411,11 @@ def cut(channel, quality): stream, muxer, mimetype = (True, 'mpegts', 'video/MP2T') if type == 'mpegts' else (False, 'mp4', 'video/mp4') encoding_args = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '0', '-f', muxer] return Response(full_cut_segments(segment_ranges, ranges, transitions, encoding_args, stream=stream), mimetype=mimetype) + elif type == 'webm': + # Basic webm settings to work in Firefox and Chrome. + stream, muxer, mimetype = (True, 'webm', 'video/webm') + encoding_args = ['-c:v', 'libvpx-vp9', '-c:a', 'libopus', '-f', muxer] + return Response(full_cut_segments(segment_ranges, ranges, transitions, encoding_args, stream=stream), mimetype=mimetype) else: return "Unknown type {!r}".format(type), 400 diff --git a/thrimbletrimmer/scripts/edit.js b/thrimbletrimmer/scripts/edit.js index 5e976cf..99944db 100644 --- a/thrimbletrimmer/scripts/edit.js +++ b/thrimbletrimmer/scripts/edit.js @@ -1655,6 +1655,43 @@ function updateDownloadLink() { videoInfo.video_quality, ); document.getElementById("download-link").href = downloadURL; + + // Create preview links for each transition + const transitionPreviewPaddingSeconds = 5; + const transitionLinks = document.getElementsByClassName("range-transition-preview-button"); + for (let i=0; i<transitionLinks.length; i++) { + const transitionLinkTarget = transitionLinks[i]; + const previewTimeRanges = timeRanges.slice(i,i+2); // Current and next range + const previewTransition = transitions[i]; // Transition after current range + // Calculate the duration of the transition. Cut transitions have + // previewTransition == "" and are instantaneous. + const thisTransitionDuration = previewTransition ? parseFloat(previewTransition.split(",")[1]) : 0; + // Each side of the transition needs the configured padding length plus + // the duration of the transition. + const thisPaddingDurationSeconds = thisTransitionDuration + transitionPreviewPaddingSeconds; + if (previewTimeRanges[0].start && previewTimeRanges[0].end && previewTimeRanges[1].start && previewTimeRanges[1].end) { + // Adjust segment start and end times based on calculated padding. + previewTimeRanges[0].start = wubloaderTimeFromDateTime(luxon.DateTime.max( + dateTimeFromWubloaderTime(previewTimeRanges[0].start), + dateTimeFromWubloaderTime(previewTimeRanges[0].end).minus(luxon.Duration.fromMillis(thisPaddingDurationSeconds*1000)) + )); + previewTimeRanges[1].end = wubloaderTimeFromDateTime(luxon.DateTime.min( + dateTimeFromWubloaderTime(previewTimeRanges[1].end), + dateTimeFromWubloaderTime(previewTimeRanges[1].start).plus(luxon.Duration.fromMillis(thisPaddingDurationSeconds*1000)) + )); + // Create a video URL, forcing webm and 480p + const previewURL = generateDownloadURL( + previewTimeRanges, + [previewTransition], + "webm", + allowHoles, + "480p", + ); + transitionLinkTarget.dataset.videoSource = previewURL; + } else { + transitionLinkTarget.dataset.videoSource = null; + } + } } async function setManualVideoLink() { @@ -1819,7 +1856,39 @@ function rangeDefinitionDOM() { handleFieldChange(); }); transitionDurationSection.append(" over ", transitionDuration, " seconds"); - transitionContainer.append("Transition: ", transitionType, transitionDurationSection); + + // Add a link to preview this transition + const transitionPreviewSection = makeElement("div", [ + "range-transition-preview-section", + ]); + const transitionPreviewButton = makeElement("button", ["range-transition-preview-button"]); + transitionPreviewButton.append("Preview"); + transitionPreviewButton.addEventListener("click", (event) => { + const videoUrl = event.target.dataset.videoSource; + + // Pop up a new window to contain the video preview. + // These dimensions aren't quite right because if I make them exact for 480p, + // firefox creates an unnecessary scroll bar. + const newWindow = window.open("", "_blank", "width=845,height=480"); + // Very basic video player wrapper + newWindow.document.write(` + <!DOCTYPE html> + <html> + <head> + <title>Thrimbletrimmer Transition Preview</title> + </head> + <body style="margin:0; padding:0;"> + <video width="100%" height="100%" controls autoplay> + <source src="${videoUrl}" type="video/webm" /> + Your browser does not support the video tag. + </video> + </body> + </html> + `); + newWindow.document.close(); + }); + transitionPreviewSection.append(transitionPreviewButton); + transitionContainer.append("Transition: ", transitionType, transitionDurationSection, transitionPreviewSection); const rangeTimesContainer = makeElement("div", ["range-definition-times"]); const rangeStart = makeElement("input", ["range-definition-start"], { type: "text" }); diff --git a/thrimbletrimmer/styles/thrimbletrimmer.css b/thrimbletrimmer/styles/thrimbletrimmer.css index 005381a..f3e0c1b 100644 --- a/thrimbletrimmer/styles/thrimbletrimmer.css +++ b/thrimbletrimmer/styles/thrimbletrimmer.css @@ -248,12 +248,18 @@ a.click { .range-transition-duration-section { display: inline-block; + padding-left: 0.5em; } .range-transition-duration { width: 50px; } +.range-transition-preview-section { + display: inline-block; + padding-left: 0.5em; +} + .range-definition-times { display: flex; align-items: center;