Skip to content

Commit

Permalink
feat(info): use iOS client to fetch streaming data
Browse files Browse the repository at this point in the history
  • Loading branch information
skick1234 committed Aug 4, 2024
1 parent 0ccc87d commit 6ddf70d
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 24 deletions.
File renamed without changes.
6 changes: 3 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const sig = require('./sig');
const miniget = require('miniget');
const m3u8stream = require('m3u8stream');
const { parseTimestamp } = require('m3u8stream');
const cookie = require('./cookie');
const agent = require('./agent');


/**
Expand All @@ -32,8 +32,8 @@ ytdl.validateID = urlUtils.validateID;
ytdl.validateURL = urlUtils.validateURL;
ytdl.getURLVideoID = urlUtils.getURLVideoID;
ytdl.getVideoID = urlUtils.getVideoID;
ytdl.createAgent = cookie.createAgent;
ytdl.createProxyAgent = cookie.createProxyAgent;
ytdl.createAgent = agent.createAgent;
ytdl.createProxyAgent = agent.createProxyAgent;
ytdl.cache = {
sig: sig.cache,
info: getInfo.cache,
Expand Down
103 changes: 87 additions & 16 deletions lib/info.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
const sax = require('sax');
const utils = require('./utils');
// Forces Node JS version of setTimeout for Electron based applications
Expand Down Expand Up @@ -56,7 +57,8 @@ exports.getBasicInfo = async(id, options) => {


Object.assign(info, {
formats: parseFormats(info.player_response),
// Replace with formats from iosPlayerResponse
// formats: parseFormats(info.player_response),
related_videos: extras.getRelatedVideos(info),
});

Expand Down Expand Up @@ -239,40 +241,109 @@ const parseFormats = player_response => {
* @returns {Promise<Object>}
*/
exports.getInfo = async(id, options) => {
let info = await exports.getBasicInfo(id, options);
const info = await exports.getBasicInfo(id, options);
const iosPlayerResponse = await fetchIosJsonPlayer(id, options);
info.formats = parseFormats(iosPlayerResponse);
const hasManifest =
info.player_response && info.player_response.streamingData && (
info.player_response.streamingData.dashManifestUrl ||
info.player_response.streamingData.hlsManifestUrl
iosPlayerResponse && iosPlayerResponse.streamingData && (
iosPlayerResponse.streamingData.dashManifestUrl ||
iosPlayerResponse.streamingData.hlsManifestUrl
);
let funcs = [];
if (info.formats.length) {
info.html5player = info.html5player ||
getHTML5player(await getWatchHTMLPageBody(id, options)) || getHTML5player(await getEmbedPageBody(id, options));
if (!info.html5player) {
throw Error('Unable to find html5player file');
}
const html5player = new URL(info.html5player, BASE_URL).toString();
funcs.push(sig.decipherFormats(info.formats, html5player, options));
// Stream from ios player doesn't need to be deciphered.
// info.html5player = info.html5player ||
// getHTML5player(await getWatchHTMLPageBody(id, options)) || getHTML5player(await getEmbedPageBody(id, options));
// if (!info.html5player) {
// throw Error('Unable to find html5player file');
// }
// const html5player = new URL(info.html5player, BASE_URL).toString();
// funcs.push(sig.decipherFormats(info.formats, html5player, options));
funcs.push(info.formats);
}
if (hasManifest && info.player_response.streamingData.dashManifestUrl) {
let url = info.player_response.streamingData.dashManifestUrl;
if (hasManifest && iosPlayerResponse.streamingData.dashManifestUrl) {
let url = iosPlayerResponse.streamingData.dashManifestUrl;
funcs.push(getDashManifest(url, options));
}
if (hasManifest && info.player_response.streamingData.hlsManifestUrl) {
let url = info.player_response.streamingData.hlsManifestUrl;
if (hasManifest && iosPlayerResponse.streamingData.hlsManifestUrl) {
let url = iosPlayerResponse.streamingData.hlsManifestUrl;
funcs.push(getM3U8(url, options));
}

let results = await Promise.all(funcs);
info.formats = Object.values(Object.assign({}, ...results));
info.formats = info.formats.map(formatUtils.addFormatMeta);
info.formats.sort(formatUtils.sortFormats);


info.full = true;
return info;
};


// TODO: Clean up this code to impliment Android player.
const IOS_CLIENT_VERSION = '19.28.1',
IOS_DEVICE_MODEL = 'iPhone16,2',
IOS_USER_AGENT_VERSION = '17_5_1',
IOS_OS_VERSION = '17.5.1.21F90';

const fetchIosJsonPlayer = async(videoId, options) => {
const cpn = utils.generateClientPlaybackNonce(16);
const payload = {
videoId,
cpn,
contentCheckOk: true,
racyCheckOk: true,
context: {
client: {
clientName: 'IOS',
clientVersion: IOS_CLIENT_VERSION,
deviceMake: 'Apple',
deviceModel: IOS_DEVICE_MODEL,
platform: 'MOBILE',
osName: 'iOS',
osVersion: IOS_OS_VERSION,
hl: 'en',
gl: 'US',
utcOffsetMinutes: -240,
},
request: {
internalExperimentFlags: [],
useSsl: true,
},
user: {
lockedSafetyMode: false,
},
},
};

const { jar, dispatcher } = options.agent;
const opts = {
requestOptions: {
method: 'POST',
dispatcher,
query: {
prettyPrint: false,
t: utils.generateClientPlaybackNonce(12),
id: videoId,
},
headers: {
'Content-Type': 'application/json',
cookie: jar.getCookieStringSync('https://www.youtube.com'),
'User-Agent': `com.google.ios.youtube/${IOS_CLIENT_VERSION}(${
IOS_DEVICE_MODEL
}; U; CPU iOS ${IOS_USER_AGENT_VERSION} like Mac OS X; en_US)`,
'X-Goog-Api-Format-Version': '2',
},
body: JSON.stringify(payload),
},
};
const response = await utils.request('https://youtubei.googleapis.com/youtubei/v1/player', opts);
if (videoId !== response.videoDetails.videoId) throw Error('Video ID mismatch');
return response;
};


/**
* Gets additional DASH formats.
*
Expand Down
15 changes: 10 additions & 5 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { request } = require('undici');
const { writeFileSync } = require('fs');
const cookie = require('./cookie');
const AGENT = require('./agent');


/**
Expand Down Expand Up @@ -320,11 +320,11 @@ let oldCookieWarning = true;
let oldDispatcherWarning = true;
exports.applyDefaultAgent = options => {
if (!options.agent) {
const { jar } = cookie.defaultAgent;
const { jar } = AGENT.defaultAgent;
const c = exports.getPropInsensitive(options.requestOptions.headers, 'cookie');
if (c) {
jar.removeAllCookiesSync();
cookie.addCookiesFromString(jar, c);
AGENT.addCookiesFromString(jar, c);
if (oldCookieWarning) {
oldCookieWarning = false;
console.warn(
Expand All @@ -341,7 +341,7 @@ exports.applyDefaultAgent = options => {
'(https://github.com/distubejs/ytdl-core#how-to-implement-ytdlagent-with-your-own-dispatcher)',
);
}
options.agent = cookie.defaultAgent;
options.agent = AGENT.defaultAgent;
}
};

Expand All @@ -352,7 +352,7 @@ exports.applyOldLocalAddress = options => {
!options.requestOptions.localAddress ||
options.requestOptions.localAddress === options.agent.localAddress
) return;
options.agent = cookie.createAgent(undefined, { localAddress: options.requestOptions.localAddress });
options.agent = AGENT.createAgent(undefined, { localAddress: options.requestOptions.localAddress });
if (oldLocalAddressWarning) {
oldLocalAddressWarning = false;
console.warn(
Expand Down Expand Up @@ -386,3 +386,8 @@ exports.applyDefaultHeaders = options => {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36',
}, options.requestOptions.headers);
};

exports.generateClientPlaybackNonce = length => {
const CPN_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
return Array.from({ length }, () => CPN_CHARS[Math.floor(Math.random() * CPN_CHARS.length)]).join('');
};

0 comments on commit 6ddf70d

Please sign in to comment.