Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QUESTION: How does Invidious handle video streams? #1325

Closed
pluja opened this issue Aug 5, 2020 · 1 comment
Closed

QUESTION: How does Invidious handle video streams? #1325

pluja opened this issue Aug 5, 2020 · 1 comment
Labels
question Further information is requested

Comments

@pluja
Copy link

pluja commented Aug 5, 2020

I'm working on a project and I want to know how does Invidious handle the video streaming without making any connection to googlevideo from the client. Is it streaming the video through the invidious server to the client? If so, how is it done?

I have taken a look to the code, but I don't really undestand the structure and I've never worked with Crystal nor Ruby on rails so it's a bit slow for me. I can read and investigate the part of the code that is doing that if you can tell me where it is.

Thanks!

@isaackd
Copy link
Contributor

isaackd commented Aug 5, 2020

I may be mistaken but it seems like these would be the relevant lines:

invidious/src/invidious.cr

Lines 5376 to 5597 in 62f015f

get "/videoplayback/*" do |env|
path = env.request.path
path = path.lchop("/videoplayback/")
path = path.rchop("/")
path = path.gsub(/mime\/\w+\/\w+/) do |mimetype|
mimetype = mimetype.split("/")
mimetype[0] + "/" + mimetype[1] + "%2F" + mimetype[2]
end
path = path.split("/")
raw_params = {} of String => Array(String)
path.each_slice(2) do |pair|
key, value = pair
value = URI.decode_www_form(value)
if raw_params[key]?
raw_params[key] << value
else
raw_params[key] = [value]
end
end
query_params = HTTP::Params.new(raw_params)
env.response.headers["Access-Control-Allow-Origin"] = "*"
env.redirect "/videoplayback?#{query_params}"
end
get "/videoplayback" do |env|
query_params = env.params.query
fvip = query_params["fvip"]? || "3"
mns = query_params["mn"]?.try &.split(",")
mns ||= [] of String
if query_params["region"]?
region = query_params["region"]
query_params.delete("region")
end
if query_params["host"]? && !query_params["host"].empty?
host = "https://#{query_params["host"]}"
query_params.delete("host")
else
host = "https://r#{fvip}---#{mns.pop}.googlevideo.com"
end
url = "/videoplayback?#{query_params.to_s}"
headers = HTTP::Headers.new
REQUEST_HEADERS_WHITELIST.each do |header|
if env.request.headers[header]?
headers[header] = env.request.headers[header]
end
end
client = make_client(URI.parse(host), region)
response = HTTP::Client::Response.new(500)
error = ""
5.times do
begin
response = client.head(url, headers)
if response.headers["Location"]?
location = URI.parse(response.headers["Location"])
env.response.headers["Access-Control-Allow-Origin"] = "*"
host = "#{location.scheme}://#{location.host}"
client = make_client(URI.parse(host), region)
url = "#{location.full_path}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
else
break
end
rescue Socket::Addrinfo::Error
if !mns.empty?
mn = mns.pop
end
fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region)
rescue ex
error = ex.message
end
end
if response.status_code >= 400
env.response.status_code = response.status_code
env.response.content_type = "text/plain"
next error
end
if url.includes? "&file=seg.ts"
if CONFIG.disabled?("livestreams")
env.response.status_code = 403
error_message = "Administrator has disabled this endpoint."
next templated "error"
end
begin
client = make_client(URI.parse(host), region)
client.get(url, headers) do |response|
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = response.headers["Location"]?
location = URI.parse(location)
location = "#{location.full_path}&host=#{location.host}"
if region
location += "&region=#{region}"
end
next env.redirect location
end
IO.copy(response.body_io, env.response)
end
rescue ex
end
else
if query_params["title"]? && CONFIG.disabled?("downloads") ||
CONFIG.disabled?("dash")
env.response.status_code = 403
error_message = "Administrator has disabled this endpoint."
next templated "error"
end
content_length = nil
first_chunk = true
range_start, range_end = parse_range(env.request.headers["Range"]?)
chunk_start = range_start
chunk_end = range_end
if !chunk_end || chunk_end - chunk_start > HTTP_CHUNK_SIZE
chunk_end = chunk_start + HTTP_CHUNK_SIZE - 1
end
client = make_client(URI.parse(host), region)
# TODO: Record bytes written so we can restart after a chunk fails
while true
if !range_end && content_length
range_end = content_length
end
if range_end && chunk_start > range_end
break
end
if range_end && chunk_end > range_end
chunk_end = range_end
end
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
begin
client.get(url, headers) do |response|
if first_chunk
if !env.request.headers["Range"]? && response.status_code == 206
env.response.status_code = 200
else
env.response.status_code = response.status_code
end
response.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) && key.downcase != "content-range"
env.response.headers[key] = value
end
end
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = response.headers["Location"]?
location = URI.parse(location)
location = "#{location.full_path}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
env.redirect location
break
end
if title = query_params["title"]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{URI.encode_www_form(title)}\"; filename*=UTF-8''#{URI.encode_www_form(title)}"
end
if !response.headers.includes_word?("Transfer-Encoding", "chunked")
content_length = response.headers["Content-Range"].split("/")[-1].to_i64
if env.request.headers["Range"]?
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
env.response.content_length = content_length
end
end
end
proxy_file(response, env)
end
rescue ex
if ex.message != "Error reading socket: Connection reset by peer"
break
else
client = make_client(URI.parse(host), region)
end
end
chunk_start = chunk_end + 1
chunk_end += HTTP_CHUNK_SIZE
first_chunk = false
end
end
end

This comment might also be helpful: #453 (comment)

@TheFrenchGhosty TheFrenchGhosty added the question Further information is requested label Aug 5, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants