diff --git a/assets/js/player.js b/assets/js/player.js index ee678663..5bc371dc 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -43,6 +43,14 @@ embed_url = location.origin + '/embed/' + video_data.id + embed_url.search; var save_player_pos_key = 'save_player_pos'; videojs.Vhs.xhr.beforeRequest = function(options) { + // Pass range as an URL parameter, not as a header + if (options.headers){ + if (options.headers.Range) { + options.uri = options.uri + '&range=' + options.headers.Range.split("=")[1]; + delete options.headers.Range; + } + } + // set local if requested not videoplayback if (!options.uri.includes('videoplayback')) { if (!options.uri.includes('local=true')) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 63559ab5..d27b214d 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -8,13 +8,17 @@ module Invidious::Routes::VideoPlayback mns = query_params["mn"]?.try &.split(",") mns ||= [] of String - if query_params["region"]? - region = query_params["region"] + # Extract some invidious-specific parameters + + if region = query_params["region"]? query_params.delete("region") end - if query_params["host"]? && !query_params["host"].empty? - host = query_params["host"] + if title = query_params["title"]? + query_params.delete("title") + end + + if host = query_params["host"]? query_params.delete("host") else host = "r#{fvip}---#{mns.pop}.googlevideo.com" @@ -25,17 +29,34 @@ module Invidious::Routes::VideoPlayback return error_template(400, "Invalid \"host\" parameter.") end + # Range manipulation + + has_range_param = false + has_range_header = false + + if range = query_params["range"]? + query_params.delete("range") + has_range_param = true + end + + if range_header = env.request.headers["Range"]? + env.request.headers.delete("Range") + range ||= range_header.split('=')[1] + has_range_header = true if !has_range_param + end + + # Skip redirections + host = "https://#{host}" url = "/videoplayback?#{query_params}" headers = HTTP::Headers.new MediaProxy.copy_request_headers(from: env.request.headers, to: headers) - # See: https://github.com/iv-org/invidious/issues/3302 - range_header = env.request.headers["Range"]? - if range_header.nil? - range_for_head = query_params["range"]? || "0-640" - headers["Range"] = "bytes=#{range_for_head}" + if has_range_param + url += "&range=#{range}" + else + headers["Range"] = "bytes=#{range || "0-"}" end client = make_client(URI.parse(host), region) @@ -74,7 +95,7 @@ module Invidious::Routes::VideoPlayback end # Remove the Range header added previously. - headers.delete("Range") if range_header.nil? + headers.delete("Range") if response.status_code >= 400 env.response.content_type = "text/plain" @@ -86,29 +107,21 @@ module Invidious::Routes::VideoPlayback return error_template(403, "Administrator has disabled this endpoint.") end - begin - client.get(url, headers) do |resp| - MediaProxy.copy_response_headers(from: resp.headers, to: env.response.headers) - env.response.headers["Access-Control-Allow-Origin"] = "*" - - if location = resp.headers["Location"]? - url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region) - return env.redirect url - end - - IO.copy(resp.body_io, env.response) - end - rescue ex + MediaProxy.proxy_dash_chunk(env, client, url, region) + elsif has_range_param + if CONFIG.disabled?("dash") + return error_template(403, "Administrator has disabled this endpoint.") end + + MediaProxy.proxy_dash_chunk(env, client, url, region) else - if query_params["title"]? && CONFIG.disabled?("downloads") || - CONFIG.disabled?("dash") + if (title && CONFIG.disabled?("downloads")) || (title.nil? && CONFIG.disabled?("local")) return error_template(403, "Administrator has disabled this endpoint.") end content_length = nil first_chunk = true - range_start, range_end = parse_range(env.request.headers["Range"]?) + range_start, range_end = parse_range(range) chunk_start = range_start chunk_end = range_end @@ -135,16 +148,12 @@ module Invidious::Routes::VideoPlayback begin client.get(url, headers) do |resp| if first_chunk - if !env.request.headers["Range"]? && resp.status_code == 206 + if !has_range_header && resp.status_code == 206 env.response.status_code = 200 else env.response.status_code = resp.status_code end - MediaProxy.copy_response_headers(from: resp.headers, to: env.response.headers) - env.response.headers.delete("Content-Range") # Important! - env.response.headers["Access-Control-Allow-Origin"] = "*" - if location = resp.headers["Location"]? location = URI.parse(location) location = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}" @@ -153,7 +162,11 @@ module Invidious::Routes::VideoPlayback break end - if title = query_params["title"]? + MediaProxy.copy_response_headers(from: resp.headers, to: env.response.headers) + env.response.headers.delete("Content-Range") # Important! + env.response.headers["Access-Control-Allow-Origin"] = "*" + + if title # https://blog.fastmail.com/2011/06/24/download-non-english-filenames/ filename = URI.encode_www_form(title, space_to_plus: false) header = "attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}" @@ -162,7 +175,7 @@ module Invidious::Routes::VideoPlayback if !resp.headers.includes_word?("Transfer-Encoding", "chunked") content_length = resp.headers["Content-Range"].split("/")[-1].to_i64 - if env.request.headers["Range"]? + if has_range_header env.response.headers["Content-Range"] = "bytes #{range_start}-#{range_end || (content_length - 1)}/#{content_length}" env.response.content_length = ((range_end.try &.+ 1) || content_length) - range_start else diff --git a/src/invidious/yt_backend/media_proxy.cr b/src/invidious/yt_backend/media_proxy.cr index 3eebb3ad..1b246a7e 100644 --- a/src/invidious/yt_backend/media_proxy.cr +++ b/src/invidious/yt_backend/media_proxy.cr @@ -35,4 +35,36 @@ module Invidious::MediaProxy end end end + + # ------------------- + # Proxy functions + # ------------------- + + def proxy_dash_chunk( + env : HTTP::Server::Context, + client : HTTP::Client, + url : URI | String, + region : String? + ) + headers = HTTP::Headers.new + self.copy_request_headers(from: env.request.headers, to: headers) + + # Make sure to remove a potential range header, to avoid throttling + headers.delete("Range") + + client.get(url, headers) do |resp| + env.response.status_code = resp.status_code + + self.copy_response_headers(from: resp.headers, to: env.response.headers) + env.response.headers["Access-Control-Allow-Origin"] = "*" + + if location = resp.headers["Location"]? + url = HttpServer::Utils.proxy_video_url(location, region: region) + env.redirect url + else + IO.copy(resp.body_io, env.response) + end + end + rescue ex + end end