) -> Result, String> {
Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone()))
.path("/")
- .http_only(true)
+ .http_only(name != "ffmpeg_video_downloads")
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
),
diff --git a/static/playHLSVideo.js b/static/playHLSVideo.js
deleted file mode 100644
index 4f54e09..0000000
--- a/static/playHLSVideo.js
+++ /dev/null
@@ -1,110 +0,0 @@
-// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
-(function () {
- if (Hls.isSupported()) {
- var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
- videoSources.forEach(function (source) {
- var playlist = source.src;
-
- var oldVideo = source.parentNode;
- var autoplay = oldVideo.classList.contains("hls_autoplay");
-
- // If HLS is supported natively then don't use hls.js
- if (oldVideo.canPlayType(source.type) === "probably") {
- if (autoplay) {
- oldVideo.play();
- }
- return;
- }
-
- // Replace video with copy that will have all "source" elements removed
- var newVideo = oldVideo.cloneNode(true);
- var allSources = newVideo.querySelectorAll("source");
- allSources.forEach(function (source) {
- source.remove();
- });
-
- // Empty source to enable play event
- newVideo.src = "about:blank";
-
- oldVideo.parentNode.replaceChild(newVideo, oldVideo);
-
- function initializeHls() {
- newVideo.removeEventListener('play', initializeHls);
- var hls = new Hls({ autoStartLoad: false });
- hls.loadSource(playlist);
- hls.attachMedia(newVideo);
- hls.on(Hls.Events.MANIFEST_PARSED, function () {
- hls.loadLevel = hls.levels.length - 1;
- var availableLevels = hls.levels.map(function(level) {
- return {
- height: level.height,
- width: level.width,
- bitrate: level.bitrate,
- };
- });
-
- addQualitySelector(newVideo, hls, availableLevels);
-
- hls.startLoad();
- newVideo.play();
- });
-
- hls.on(Hls.Events.ERROR, function (event, data) {
- var errorType = data.type;
- var errorFatal = data.fatal;
- if (errorFatal) {
- switch (errorType) {
- case Hls.ErrorType.NETWORK_ERROR:
- hls.startLoad();
- break;
- case Hls.ErrorType.MEDIA_ERROR:
- hls.recoverMediaError();
- break;
- default:
- hls.destroy();
- break;
- }
- }
-
- console.error("HLS error", data);
- });
- }
-
- function addQualitySelector(videoElement, hlsInstance, availableLevels) {
- var qualitySelector = document.createElement('select');
- qualitySelector.classList.add('quality-selector');
- var last = availableLevels.length - 1;
- availableLevels.forEach(function (level, index) {
- var option = document.createElement('option');
- option.value = index.toString();
- var bitrate = (level.bitrate / 1_000).toFixed(0);
- option.text = level.height + 'p (' + bitrate + ' kbps)';
- if (index === last) {
- option.selected = "selected";
- }
- qualitySelector.appendChild(option);
- });
- qualitySelector.selectedIndex = availableLevels.length - 1;
- qualitySelector.addEventListener('change', function () {
- var selectedIndex = qualitySelector.selectedIndex;
- hlsInstance.nextLevel = selectedIndex;
- hlsInstance.startLoad();
- });
-
- videoElement.parentNode.appendChild(qualitySelector);
- }
-
- newVideo.addEventListener('play', initializeHls);
-
- if (autoplay) {
- newVideo.play();
- }
- });
- } else {
- var videos = document.querySelectorAll("video.hls_autoplay");
- videos.forEach(function (video) {
- video.setAttribute("autoplay", "");
- });
- }
-})();
-// @license-end
diff --git a/static/style.css b/static/style.css
index 87b3c3b..79c122a 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1836,18 +1836,41 @@ td, th {
}
}
-.quality-selector {
+.video-options {
border: 2px var(--outside) solid;
margin-top: 8px;
float: right;
+ border-radius: 5px;
+ height: 35px;
+ height: 35px;
+ margin: 2px;
+ box-sizing: border-box;
}
-.quality-selector option {
+.video-options option {
background-color: var(--background);
color: var(--text);
}
-.quality-selector option:hover {
+.video-options option:hover {
background-color: var(--accent);
color: var(--text);
}
+
+.download {
+ padding-left: 8px;
+ padding-right: 8px;
+ font-size: 20px;
+ font-weight: 900;
+ color: var(--accent);
+ background-color: var(--outside);
+}
+
+.download:hover {
+ background-color: var(--foreground);
+ /*color: var(--);*/
+}
+
+.download:active {
+ background-color: var(--background);
+}
\ No newline at end of file
diff --git a/static/videoUtils.js b/static/videoUtils.js
new file mode 100644
index 0000000..d697e4a
--- /dev/null
+++ b/static/videoUtils.js
@@ -0,0 +1,228 @@
+// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
+let ffmpeg = null;
+(function () {
+ if (Hls.isSupported()) {
+
+ var downloadsEnabled = document.cookie.split("; ").find((row) => row.startsWith("ffmpeg_video_downloads="))?.split("=")[1] == "on";
+
+ var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
+ videoSources.forEach(function (source) {
+ var playlist = source.src;
+
+ var oldVideo = source.parentNode;
+ var autoplay = oldVideo.classList.contains("hls_autoplay");
+
+ // If HLS is supported natively then don't use hls.js
+ if (oldVideo.canPlayType(source.type) === "probably") {
+ if (autoplay) {
+ oldVideo.play();
+ }
+ return;
+ }
+
+ // Replace video with copy that will have all "source" elements removed
+ var newVideo = oldVideo.cloneNode(true);
+ var allSources = newVideo.querySelectorAll("source");
+ allSources.forEach(function (source) {
+ source.remove();
+ });
+
+ // Empty source to enable play event
+ newVideo.src = "about:blank";
+
+ oldVideo.parentNode.replaceChild(newVideo, oldVideo);
+
+ function initializeHls() {
+ newVideo.removeEventListener('play', initializeHls);
+ var hls = new Hls({ autoStartLoad: false });
+ hls.loadSource(playlist);
+ hls.attachMedia(newVideo);
+ hls.on(Hls.Events.MANIFEST_PARSED, function () {
+ hls.loadLevel = hls.levels.length - 1;
+ var availableLevels = hls.levels.map(function(level) {
+ return {
+ height: level.height,
+ width: level.width,
+ bitrate: level.bitrate,
+ };
+ });
+
+ addQualitySelector(newVideo, hls, availableLevels);
+ if (downloadsEnabled){ addVideoDownload(newVideo, hls); }
+ hls.startLoad();
+ newVideo.play();
+ });
+
+ hls.on(Hls.Events.ERROR, function (event, data) {
+ var errorType = data.type;
+ var errorFatal = data.fatal;
+ if (errorFatal) {
+ switch (errorType) {
+ case Hls.ErrorType.NETWORK_ERROR:
+ hls.startLoad();
+ break;
+ case Hls.ErrorType.MEDIA_ERROR:
+ hls.recoverMediaError();
+ break;
+ default:
+ hls.destroy();
+ break;
+ }
+ }
+
+ console.error("HLS error", data);
+ });
+ }
+
+ if (downloadsEnabled){
+ const { fetchFile } = FFmpegUtil;
+ const { FFmpeg } = FFmpegWASM;
+
+ function addVideoDownload(videoElement, hlsInstance) {
+ var mediaStream = [];
+ var downloadButton = document.createElement('button');
+ downloadButton.classList.add('video-options','download');
+ downloadButton.innerText = "⏳"
+ const mergeStreams = async () => {
+ if (ffmpeg === null) {
+ ffmpeg = new FFmpeg();
+ await ffmpeg.load({
+ coreURL: "/ffmpeg/ffmpeg-core.js",
+ });
+ ffmpeg.on("log", ({ message }) => {
+ console.log(message); // This is quite noisy but i will include it
+ })
+ ffmpeg.on("progress", ({ progress, time }) => { // Progress TODO: show progress ring around button not just ⏳
+ // console.log("ffmpeg prog:",progress * 100)
+ });
+ }
+ // Combine Video Audio Streams
+ await ffmpeg.writeFile("video", await fetchFile(concatBlob(mediaStream['video'])));
+ await ffmpeg.writeFile("audio", await fetchFile(concatBlob(mediaStream['audio'])));
+ console.time('ffmpeg-exec');
+ await ffmpeg.exec(['-i', "video", '-i', "audio",'-c:v', "copy", '-c:a', "aac", 'output.mp4']);
+ console.timeEnd('ffmpeg-exec')
+
+ // Save
+ const toSlug = (str) => {
+ return str
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/[\W_]+/g, '-')
+ .toLowerCase()
+ .replace(/^-+|-+$/g, '');
+ }
+
+ var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
+ const data = await ffmpeg.readFile('output.mp4');
+ saveAs(new Blob([data.buffer]),filename);
+ return
+ }
+ function saveAs(blob, filename) { // Yeah ok...
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement("a");
+ document.body.appendChild(a);
+ a.style = "display: none";
+ a.href = url;
+ a.download = filename;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }
+ function concatBlob(inputArray) {
+ var totalLength = inputArray.reduce(function (prev, cur) {
+ return prev + cur.length
+ }, 0);
+ var result = new Uint8Array(totalLength);
+ var offset = 0;
+ inputArray.forEach(function (element) {
+ result.set(element, offset);
+ offset += element.length;
+ });
+ return new Blob([result], {
+ type: 'application/octet-stream'
+ });
+ }
+ function getStreams() {
+ var video = document.createElement('video');
+ video.autoplay = true;
+ var dataStreams = {
+ 'video': [],
+ 'audio': []
+ };
+ mediaStream = dataStreams; // Update stream
+
+ hlsInstance.on(Hls.Events.BUFFER_APPENDING, function (event, data) {
+ dataStreams[data.type].push(data.data);
+ });
+ var isDownloading = false
+ function startDownload() {
+ if (!isDownloading) { isDownloading = true } else { return }
+ downloadButton.innerText = "⏳"
+ mergeStreams()
+ .then(_ => {
+ isDownloading = false
+ downloadButton.innerText = "⭳"
+ });
+ }
+
+ function waitForLoad() {
+ const poll = resolve => {
+ if(hlsInstance._media.buffered.length === 1 &&
+ hlsInstance._media.buffered.start(0) === 0 &&
+ hlsInstance._media.buffered.end(0) === hlsInstance._media.duration)
+ resolve();
+ else setTimeout(_ => poll(resolve), 400);
+ }
+ return new Promise(poll);
+ }
+
+ waitForLoad(_ => flag === true)
+ .then(_ => {
+ downloadButton.innerText = "⭳"
+ downloadButton.addEventListener('click', startDownload);
+ });
+ }
+
+ videoElement.parentNode.appendChild(downloadButton);
+ getStreams()
+ }
+ }
+
+ function addQualitySelector(videoElement, hlsInstance, availableLevels) {
+ var qualitySelector = document.createElement('select');
+ qualitySelector.classList.add('video-options');
+ var last = availableLevels.length - 1;
+ availableLevels.forEach(function (level, index) {
+ var option = document.createElement('option');
+ option.value = index.toString();
+ var bitrate = (level.bitrate / 1_000).toFixed(0);
+ option.text = level.height + 'p (' + bitrate + ' kbps)';
+ if (index === last) {
+ option.selected = "selected";
+ }
+ qualitySelector.appendChild(option);
+ });
+ qualitySelector.selectedIndex = availableLevels.length - 1;
+ qualitySelector.addEventListener('change', function () {
+ var selectedIndex = qualitySelector.selectedIndex;
+ hlsInstance.nextLevel = selectedIndex;
+ hlsInstance.startLoad();
+ });
+
+ videoElement.parentNode.appendChild(qualitySelector);
+ }
+
+ newVideo.addEventListener('play', initializeHls);
+
+ if (autoplay) {
+ newVideo.play();
+ }
+ });
+ } else {
+ var videos = document.querySelectorAll("video.hls_autoplay");
+ videos.forEach(function (video) {
+ video.setAttribute("autoplay", "");
+ });
+ }
+})();
+// @license-end
diff --git a/templates/search.html b/templates/search.html
index 3d91c76..04032a9 100644
--- a/templates/search.html
+++ b/templates/search.html
@@ -97,9 +97,13 @@
{% endif %}
{% endfor %}
{% endif %}
- {% if prefs.use_hls == "on" %}
+ {% if prefs.ffmpeg_video_downloads == "on" %}
+
+
+ {% endif %}
+ {% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
-
+
{% endif %}
{% if params.typed != "sr_user" %}
diff --git a/templates/settings.html b/templates/settings.html
index 69cdd3e..45f355b 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -84,12 +84,15 @@
+ {% if prefs.ffmpeg_video_downloads != "on" %}
Why?
Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.
+ {% endif %}
+ {% if prefs.ffmpeg_video_downloads == "on" %}ⓘ HLS is required for downloads{% endif %}
-
+