Compare commits
3 Commits
a12596e1a2
...
59b702aa15
Author | SHA1 | Date | |
---|---|---|---|
59b702aa15 | |||
edf0109095 | |||
ecf0f91464 |
@ -200,7 +200,7 @@ async fn main() {
|
||||
"Referrer-Policy" => "no-referrer",
|
||||
"X-Content-Type-Options" => "nosniff",
|
||||
"X-Frame-Options" => "DENY",
|
||||
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src blob:;"
|
||||
"Content-Security-Policy" => "default-src 'none'; font-src 'self'; script-src 'self' 'wasm-unsafe-eval' blob:; manifest-src 'self'; media-src 'self' data: blob: about:; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; connect-src 'self'; worker-src 'self' blob:;"
|
||||
};
|
||||
|
||||
if let Some(expire_time) = hsts {
|
||||
@ -235,8 +235,8 @@ async fn main() {
|
||||
app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed());
|
||||
app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed());
|
||||
app
|
||||
.at("/playHLSVideo.js")
|
||||
.get(|_| resource(include_str!("../static/playHLSVideo.js"), "text/javascript", false).boxed());
|
||||
.at("/videoUtils.js")
|
||||
.get(|_| resource(include_str!("../static/videoUtils.js"), "text/javascript", false).boxed());
|
||||
app
|
||||
.at("/hls.min.js")
|
||||
.get(|_| resource(include_str!("../static/hls.min.js"), "text/javascript", false).boxed());
|
||||
|
@ -82,7 +82,7 @@ pub async fn set(req: Request<Body>) -> Result<Response<Body>, 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(),
|
||||
),
|
||||
|
1
static/hls.min.js
vendored
1
static/hls.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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
|
@ -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);
|
||||
}
|
228
static/videoUtils.js
Normal file
228
static/videoUtils.js
Normal file
@ -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
|
@ -97,9 +97,13 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if params.typed != "sr_user" %}
|
||||
|
@ -84,12 +84,15 @@
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="use_hls">Use HLS for videos</label>
|
||||
{% if prefs.ffmpeg_video_downloads != "on" %}
|
||||
<details id="feeds">
|
||||
<summary>Why?</summary>
|
||||
<div id="feed_list" class="helper">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.</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}<u>ⓘ HLS is required for downloads</u>{% endif %}
|
||||
<input type="hidden" value="off" name="use_hls">
|
||||
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
|
||||
<input type="checkbox" name="use_hls" id="use_hls" {% if prefs.ffmpeg_video_downloads == "on" %}disabled{% endif %} {% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="prefs-group">
|
||||
<label for="ffmpeg_video_downloads">Use FFmpeg to download videos</label>
|
||||
|
@ -64,9 +64,13 @@
|
||||
{% call utils::post_in_list(post) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if prefs.use_hls == "on" %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -71,9 +71,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if prefs.use_hls == "on" %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" || prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -117,7 +117,11 @@
|
||||
</a>
|
||||
</div>
|
||||
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||
{% if prefs.ffmpeg_video_downloads == "on" %}
|
||||
<script src="/ffmpeg/ffmpeg.js"></script>
|
||||
<script src="/ffmpeg/ffmpeg-util.js"></script>
|
||||
{% endif %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
||||
<script src="/hls.min.js"></script>
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
|
||||
@ -125,7 +129,7 @@
|
||||
<source src="{{ post.media.url }}" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<script src="/playHLSVideo.js"></script>
|
||||
<script src="/videoUtils.js"></script>
|
||||
{% else %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
|
||||
@ -250,7 +254,7 @@
|
||||
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
|
||||
</div>
|
||||
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
|
||||
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() || prefs.ffmpeg_video_downloads == "on" && !post.media.alt_url.is_empty() %}
|
||||
<div class="post_media_content">
|
||||
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
|
||||
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
|
||||
|
Loading…
Reference in New Issue
Block a user