Skip to content

Commit

Permalink
[proxy] do not use adaptiveFormats
Browse files Browse the repository at this point in the history
This is a workaround to videos not playing on the client, but causes us to only be able to play low quality streams.

Additional changes: Cleaned up the horrible mess.
  • Loading branch information
drunkwinter committed Oct 8, 2024
1 parent 7d2d12a commit c64fa7e
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 111 deletions.
78 changes: 33 additions & 45 deletions account-proxy/controllers/proxyController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import process from 'node:process';

import dotenv from 'dotenv';
import { ProxyAgent } from 'undici';

Expand All @@ -14,14 +12,6 @@ const credentials = new YouTubeCredentials();
const proxy = process.env.PROXY;
const proxyAgent = proxy ? new ProxyAgent(proxy) : undefined;

const relevantAttributes = [
'playabilityStatus',
'videoDetails',
'streamingData',
'contents',
'engagementPanels'
]

function getPlayer(req, res) {
handleProxyRequest(req, res, 'player');
}
Expand Down Expand Up @@ -53,51 +43,49 @@ async function handleProxyRequest(req, res, endpoint) {
pRequests.push(innertubeApi.sendApiRequest('next', clientParams, credentials, proxyAgent));
}

const youtubeResponses = await Promise.all(pRequests);
let result = handleResponses(youtubeResponses, clientParams, endpoint);
const [playerResponse, nextResponse] = await Promise.all(pRequests);

res.code(200).send(result);
} catch (err) {
console.error(endpoint, err.message);
res.code(500).send({ errorMessage: err.message });
stats.countResponse(endpoint, 'EXCEPTION');
stats.countException(endpoint, err.message);
} finally {
let latencyMs = new Date().getTime() - tsStart;
stats.countLatency(latencyMs);
}
}
if (!playerResponse || typeof playerResponse !== 'object') {
throw new Error(`Invalid YouTube response received for endpoint /player`);
}

function handleResponses(youtubeResponses, clientParams, endpoint) {
let responseData = {};
if (clientParams.includeNext && (!nextResponse || typeof nextResponse !== 'object')) {
throw new Error(`Invalid YouTube response received for endpoint /next`);
}

youtubeResponses.forEach((response, index) => {
const currentEndpoint = ((endpoint === 'next' && index === 0) || (endpoint === 'player' && index === 1)) ? 'next' : 'player';
const youtubeStatus = getYoutubeResponseStatus(playerResponse);
const youtubeGcrFlagSet = checkForGcrFlag(playerResponse);

if (response === null || typeof response !== 'object') {
throw new Error(`Invalid YouTube response received for endpoint /${currentEndpoint}`);
}
stats.countResponse('player', youtubeStatus, youtubeGcrFlagSet);

const responseData = extractAttributes(playerResponse, ['playabilityStatus', 'videoDetails', 'streamingData']);

const youtubeStatus = getYoutubeResponseStatus(response);
const youtubeGcrFlagSet = checkForGcrFlag(response);
const relevantData = extractAttributes(response, relevantAttributes);
responseData.proxy = { clientParams, youtubeGcrFlagSet, youtubeStatus };

stats.countResponse(currentEndpoint, youtubeStatus, youtubeGcrFlagSet);
/**
* Workaround: when we provide `adaptiveFormats` the client cannot playback the video.
*
* It seems the URLs we get here or the one the client constructs from these URLs are tied to the requesting account.
* The low quality `formats` URLs seem fine.
*/
delete responseData.streamingData.adaptiveFormats;

if (index === 0) {
responseData = relevantData;
if (nextResponse) {
stats.countResponse('next', getYoutubeResponseStatus(nextResponse), null);
responseData.nextResponse = extractAttributes(nextResponse, ['contents', 'engagementPanels']);
}

responseData.proxy = {
clientParams,
youtubeGcrFlagSet,
youtubeStatus
};
} else {
responseData[currentEndpoint + 'Response'] = relevantData;
res.code(200).send(responseData);
} catch (err) {
if (err instanceof Error) {
console.error(endpoint, err.message);
res.code(500).send({ errorMessage: err.message });
stats.countResponse(endpoint, 'EXCEPTION');
stats.countException(endpoint, err.message);
}
});
}

return responseData;
stats.countLatency(new Date().getTime() - tsStart);
}

export default {
Expand Down
79 changes: 13 additions & 66 deletions account-proxy/lib/innertubeApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,48 @@ import crypto from 'node:crypto';

import { fetch } from 'undici';

const generateSidBasedAuth = function (sapisid) {
const timestamp = Math.floor(new Date().getTime() / 1000);
const hashInput = timestamp + " " + sapisid + " " + "https://www.youtube.com";
const hashDigest = crypto.createHash("sha1").update(hashInput).digest("hex");
return `SAPISIDHASH ${timestamp}_${hashDigest}`;
}

const generateApiRequestData = function (clientParams) {
return {
"videoId": clientParams.videoId,
"context": {
"client": {
"hl": clientParams.hl,
"gl": "US",
"deviceMake": "",
"deviceModel": "",
"visitorData": "CgtIZDE3aXJqLXFwNCiZpPaHBg%3D%3D",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36,gzip(gfe)",
"clientName": clientParams.clientName,
"clientVersion": clientParams.clientVersion,
"osName": "Windows",
"osVersion": "10.0",
"originalUrl": "https://www.youtube.com/watch?v=" + clientParams.videoId,
"screenPixelDensity": 1,
"platform": "DESKTOP",
"clientFormFactor": "UNKNOWN_FORM_FACTOR",
"screenDensityFloat": 1.25,
"timeZone": "Europe/Berlin",
"browserName": "Chrome",
"browserVersion": "91.0.4472.164",
"screenWidthPoints": 834,
"screenHeightPoints": 1051,
"utcOffsetMinutes": 120,
"userInterfaceTheme": clientParams.userInterfaceTheme,
"connectionType": "CONN_CELLULAR_4G",
"mainAppWebInfo": {
"graftUrl": "https://www.youtube.com/watch?v=" + clientParams.videoId,
"webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
"isWebNativeShareAvailable": true
},
"playerType": "UNIPLAYER",
"tvAppInfo": {
"livingRoomAppMode": "LIVING_ROOM_APP_MODE_UNSPECIFIED"
},
"clientScreen": "WATCH_FULL_SCREEN"
},
"user": {
"lockedSafetyMode": false
},
"request": {
"useSsl": true,
"internalExperimentFlags": [],
"consistencyTokenJars": []
},
},
"playbackContext": {
"contentPlaybackContext": {
"html5Preference": "HTML5_PREF_WANTS",
"lactMilliseconds": "2132",
"referer": "https://www.youtube.com/watch?v=" + clientParams.videoId,
"signatureTimestamp": clientParams.signatureTimestamp,
"autoCaptionsDefaultOn": false,
"autoplay": true,
"mdxContext": {},
"playerWidthPixels": 770,
"playerHeightPixels": 433
}
},
"captionParams": {},
"racyCheckOk": true,
"contentCheckOk": true,
"startTimeSecs": clientParams.startTimeSecs,
}
}

const generateSidBasedAuth = function (sapisid, origin) {
const timestamp = Math.floor(new Date().getTime() / 1000);
const hashInput = timestamp + " " + sapisid + " " + origin;
const hashDigest = crypto.createHash("sha1").update(hashInput).digest("hex");
return `SAPISIDHASH ${timestamp}_${hashDigest}`;
}

const generateApiRequestHeaders = function (credentials) {
const origin = "https://www.youtube.com";

return {
"Cookie": `SID=${credentials.SID}; HSID=${credentials.HSID}; SSID=${credentials.SSID}; APISID=${credentials.APISID}; SAPISID=${credentials.SAPISID}; __Secure-1PSIDTS=${credentials.PSIDTS};`,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36",
"Content-Type": "application/json",
"Authorization": generateSidBasedAuth(credentials.SAPISID, origin),
"X-Origin": origin,
"X-Youtube-Client-Name": "1",
"X-Youtube-Client-Version": "2.20210721.00.00",
"Accept-Language": "en-US;q=0.8,en;q=0.7",
"Origin": origin,
"Referer": "https://www.youtube.com/watch?v=ENdgyD7Uar4"
"cookie": `SID=${credentials.SID}; HSID=${credentials.HSID}; SSID=${credentials.SSID}; APISID=${credentials.APISID}; SAPISID=${credentials.SAPISID}; __Secure-1PSIDTS=${credentials.PSIDTS};`,
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"content-type": "application/json",
"authorization": generateSidBasedAuth(credentials.SAPISID),
"origin": "https://www.youtube.com",
}
}

const sendApiRequest = async function (endpoint, clientParams, credentials, proxyAgent) {

const url = `https://www.youtube.com/youtubei/v1/${endpoint}?key=${credentials.API_KEY}&prettyPrint=false`;
const url = `https://www.youtube.com/youtubei/v1/${endpoint}?prettyPrint=false`;
const headers = generateApiRequestHeaders(credentials);
const data = generateApiRequestData(clientParams);

Expand Down

0 comments on commit c64fa7e

Please sign in to comment.