diff --git a/src/client.rs b/src/client.rs index a3dcc98..904d03b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,10 +13,10 @@ pub async fn proxy(req: Request, format: &str) -> Result, S url = url.replace(&format!("{{{}}}", name), value); } - stream(&url).await + stream(&url, &req).await } -async fn stream(url: &str) -> Result, String> { +async fn stream(url: &str, req: &Request) -> Result, String> { // First parameter is target URL (mandatory). let url = Uri::from_str(url).map_err(|_| "Couldn't parse URL".to_string())?; @@ -26,8 +26,21 @@ async fn stream(url: &str) -> Result, String> { // Build the hyper client from the HTTPS connector. let client: client::Client<_, hyper::Body> = client::Client::builder().build(https); + let mut builder = Request::get(url); + + // Copy useful headers from original request + let headers = req.headers(); + let headers_keys = vec!["Range", "If-Modified-Since", "Cache-Control"]; + for key in headers_keys { + if let Some(value) = headers.get(key) { + builder = builder.header(key, value); + } + } + + let stream_request = builder.body(Body::default()).expect("stream"); + client - .get(url) + .request(stream_request) .await .map(|mut res| { let mut rm = |key: &str| res.headers_mut().remove(key); @@ -40,6 +53,8 @@ async fn stream(url: &str) -> Result, String> { rm("x-cdn-client-region"); rm("x-cdn-name"); rm("x-cdn-server-region"); + rm("x-reddit-cdn"); + rm("x-reddit-video-features"); res }) diff --git a/src/main.rs b/src/main.rs index 0850008..0e03704 100644 --- a/src/main.rs +++ b/src/main.rs @@ -139,7 +139,7 @@ async fn main() { "Referrer-Policy" => "no-referrer", "X-Content-Type-Options" => "nosniff", "X-Frame-Options" => "DENY", - "Content-Security-Policy" => "default-src 'none'; manifest-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none';" + "Content-Security-Policy" => "default-src 'none'; 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:;" }; if let Some(expire_time) = hsts { @@ -158,15 +158,20 @@ async fn main() { app.at("/logo.png").get(|_| pwa_logo().boxed()); 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()); // Proxy media through Libreddit app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed()); + app.at("/hls/:id/*path").get(|r| proxy(r, "https://v.redd.it/{id}/{path}").boxed()); app.at("/img/:id").get(|r| proxy(r, "https://i.redd.it/{id}").boxed()); app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed()); app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed()); app.at("/preview/:loc/:id/:query").get(|r| proxy(r, "https://{loc}view.redd.it/{id}?{query}").boxed()); app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed()); app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed()); + app.at("/hls.js").get(|r| proxy(r, "https://cdn.jsdelivr.net/npm/hls.js@latest").boxed()); // Browse user profile app diff --git a/src/post.rs b/src/post.rs index 0f06f8d..21005e8 100644 --- a/src/post.rs +++ b/src/post.rs @@ -106,6 +106,7 @@ async fn parse_post(json: &serde_json::Value) -> Post { media, thumbnail: Media { url: format_url(val(post, "thumbnail").as_str()), + alt_url: String::new(), width: post["data"]["thumbnail_width"].as_i64().unwrap_or_default(), height: post["data"]["thumbnail_height"].as_i64().unwrap_or_default(), poster: "".to_string(), diff --git a/src/settings.rs b/src/settings.rs index 5654be0..7c42b61 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -50,7 +50,7 @@ pub async fn set(req: Request) -> Result, String> { let mut res = redirect("/settings".to_string()); - let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw"]; + let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "use_hls"]; for name in names { match form.get(name) { @@ -85,7 +85,17 @@ pub async fn restore(req: Request) -> Result, String> { let form = url::form_urlencoded::parse(query).collect::>(); - let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "post_sort", "show_nsfw", "subscriptions"]; + let names = vec![ + "theme", + "front_page", + "layout", + "wide", + "comment_sort", + "post_sort", + "show_nsfw", + "use_hls", + "subscriptions", + ]; let path = match form.get("redirect") { Some(value) => format!("/{}/", value), diff --git a/src/utils.rs b/src/utils.rs index 412b3bf..5401820 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -75,6 +75,7 @@ pub struct Flags { pub struct Media { pub url: String, + pub alt_url: String, pub width: i64, pub height: i64, pub poster: String, @@ -85,12 +86,28 @@ impl Media { let mut gallery = Vec::new(); // If post is a video, return the video - let (post_type, url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() { + let (post_type, url_val, alt_url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() { // Return reddit video - ("video", &data["preview"]["reddit_video_preview"]["fallback_url"]) + ( + if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) { + "gif" + } else { + "video" + }, + &data["preview"]["reddit_video_preview"]["fallback_url"], + Some(&data["preview"]["reddit_video_preview"]["hls_url"]), + ) } else if data["secure_media"]["reddit_video"]["fallback_url"].is_string() { // Return reddit video - ("video", &data["secure_media"]["reddit_video"]["fallback_url"]) + ( + if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) { + "gif" + } else { + "video" + }, + &data["secure_media"]["reddit_video"]["fallback_url"], + Some(&data["secure_media"]["reddit_video"]["hls_url"]), + ) } else if data["post_hint"].as_str().unwrap_or("") == "image" { // Handle images, whether GIFs or pics let preview = &data["preview"]["images"][0]; @@ -98,26 +115,26 @@ impl Media { if mp4.is_object() { // Return the mp4 if the media is a gif - ("gif", &mp4["source"]["url"]) + ("gif", &mp4["source"]["url"], None) } else { // Return the picture if the media is an image if data["domain"] == "i.redd.it" { - ("image", &data["url"]) + ("image", &data["url"], None) } else { - ("image", &preview["source"]["url"]) + ("image", &preview["source"]["url"], None) } } } else if data["is_self"].as_bool().unwrap_or_default() { // If type is self, return permalink - ("self", &data["permalink"]) + ("self", &data["permalink"], None) } else if data["is_gallery"].as_bool().unwrap_or_default() { // If this post contains a gallery of images gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]); - ("gallery", &data["url"]) + ("gallery", &data["url"], None) } else { // If type can't be determined, return url - ("link", &data["url"]) + ("link", &data["url"], None) }; let source = &data["preview"]["images"][0]["source"]; @@ -128,10 +145,13 @@ impl Media { format_url(url_val.as_str().unwrap_or_default()) }; + let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default())); + ( post_type.to_string(), Self { url, + alt_url, width: source["width"].as_i64().unwrap_or_default(), height: source["height"].as_i64().unwrap_or_default(), poster: format_url(source["url"].as_str().unwrap_or_default()), @@ -259,6 +279,7 @@ impl Post { post_type, thumbnail: Media { url: format_url(val(post, "thumbnail").as_str()), + alt_url: String::new(), width: data["thumbnail_width"].as_i64().unwrap_or_default(), height: data["thumbnail_height"].as_i64().unwrap_or_default(), poster: "".to_string(), @@ -364,6 +385,7 @@ pub struct Preferences { pub layout: String, pub wide: String, pub show_nsfw: String, + pub use_hls: String, pub comment_sort: String, pub post_sort: String, pub subscriptions: Vec, @@ -378,6 +400,7 @@ impl Preferences { layout: cookie(&req, "layout"), wide: cookie(&req, "wide"), show_nsfw: cookie(&req, "show_nsfw"), + use_hls: cookie(&req, "use_hls"), comment_sort: cookie(&req, "comment_sort"), post_sort: cookie(&req, "post_sort"), subscriptions: cookie(&req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), @@ -425,8 +448,32 @@ pub fn format_url(url: &str) -> String { .unwrap_or_default() }; + macro_rules! chain { + () => { + { + String::new() + } + }; + + ( $first_fn:expr, $($other_fns:expr), *) => { + { + let result = $first_fn; + if result.is_empty() { + chain!($($other_fns,)*) + } + else + { + result + } + } + }; + } + match domain { - "v.redd.it" => capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2), + "v.redd.it" => chain!( + capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2), + capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2) + ), "i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1), "a.thumbs.redditmedia.com" => capture(r"https://a\.thumbs\.redditmedia\.com/(.*)", "/thumb/a/", 1), "b.thumbs.redditmedia.com" => capture(r"https://b\.thumbs\.redditmedia\.com/(.*)", "/thumb/b/", 1), diff --git a/static/playHLSVideo.js b/static/playHLSVideo.js new file mode 100644 index 0000000..18fcf49 --- /dev/null +++ b/static/playHLSVideo.js @@ -0,0 +1,75 @@ +(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)) { + 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; + 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); + }); + } + + newVideo.addEventListener('play', initializeHls); + + if (autoplay) { + newVideo.play(); + } + }); + } else { + var videos = document.querySelectorAll("video.hls_autoplay"); + videos.forEach(function (video) { + video.setAttribute("autoplay", ""); + }); + } +})(); diff --git a/templates/post.html b/templates/post.html index 479134e..c3d5ed3 100644 --- a/templates/post.html +++ b/templates/post.html @@ -68,7 +68,16 @@ {% else if post.post_type == "video" || post.post_type == "gif" %} + {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %} + + + + {% else %} + {% endif %} {% else if post.post_type == "gallery" %} +
+ + +
@@ -68,10 +72,10 @@ {% endfor %} {% endif %} - +

Note: settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.


-

You can restore your current settings and subscriptions after clearing your cookies using this link.

+

You can restore your current settings and subscriptions after clearing your cookies using this link.

diff --git a/templates/subreddit.html b/templates/subreddit.html index b00c511..e3095a8 100644 --- a/templates/subreddit.html +++ b/templates/subreddit.html @@ -52,6 +52,10 @@ {% call utils::post_in_list(post) %} {% endif %} {% endfor %} + {% if prefs.use_hls == "on" %} + + + {% endif %}