Compare commits

...

3 Commits

Author SHA1 Message Date
59b702aa15 kinda horrific download implementation 2024-06-23 19:57:23 +12:00
edf0109095 allow wasm to run 2024-06-23 19:31:30 +12:00
ecf0f91464 remove failed hls.js source map request 2024-06-23 19:30:44 +12:00
11 changed files with 287 additions and 128 deletions

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -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" %}

View File

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

View File

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

View File

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

View File

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