Как обходится ограничение скорости скачивания с YouTube

от автора

Вы когда-нибудь пробовали скачивать видео с YouTube? Я имею в виду ручками, а не через такие софтины, как youtube-dl, yt-dlp или один из «этих» сайтов. Оказывается, это гораздо сложнее, чем можно было бы подумать.

Youtube зарабатывает на показе рекламы пользователям. Поэтому с точки зрения платформы логично внедрить специальные ограничения, которые не позволяли бы скачивать видеоролики или даже просматривать их через неофициальный клиент, например YouTube Vanced. В этой статье будут пояснены технические детали тех механизмов безопасности, что действуют в Youtube, и рассказано, как их обойти.

Гуглим качалку для YouTube

Гуглим качалку для YouTube

Извлечение URL

Первым делом нужно найти тот URL, по которому содержится файл. Для этого можно обратиться к API YouTube. В частности, через конечную точку /youtubei/v1/player можно извлечь всю метаинформацию о видео, а именно: заголовок, описание, эскизы и, что самое важное — форматы. Именно из этих форматов можно выцепить URL файла, отталкиваясь от желаемого качества (SD, HD, UHD, т.д.).

Возьмём, к примеру, видео с ID aqz-KE-bpKQ, где получаем URL для одного из форматов. Обратите внимание: другие объекты, содержащиеся в объекте context – это предусловия, проверяемые API. Принимаемые значения удалось найти, наблюдая, какие именно запросы посылает браузер.

echo -n '{"videoId":"aqz-KE-bpKQ","context":{"client":{"clientName":"WEB","clientVersion":"2.20230810.05.00"}}}' |    http post 'https://www.youtube.com/youtubei/v1/player' |   jq -r '.streamingData.adaptiveFormats[0].url'  https://rr1---sn-8qu-t0aee.googlevideo.com/videoplayback?expire=1691828966&ei=hu7WZOCJHI7T8wTSrr_QBg [TRUNCATED] 

Правда, если попытаться скачать файл по этому URL, загрузка пойдёт очень медленно:

http --print=b --download 'https://rr1---sn-8qu-t0aee.googlevideo.com/videoplayback?expire=1691828966&ei=hu7WZOCJHI7T8wTSrr_QBg [TRUNCATED]'  Downloading to videoplayback.webm [ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ]   0% ( 0.0/1.5 GB ) 6:23:45 66.7 kB/s

Скорость загрузки всегда  ограничена примерно 40-70 кБ/с. К сожалению, на скачивание этого десятиминутного видео уходит примерно шесть с половиной часов. А через браузер, очевидно, получается гораздо быстрее. Так в чём же разница?

Давайте разберём этот URL по частям. Он довольно сложный, но в нём есть конкретный параметр, который нас и интересует.

Protocol: https Hostname: rr1---sn-8qu-t0aee.googlevideo.com Path name: /videoplayback Query Parameters:   expire: 1691829310   ei: 3u_WZJT7Cbag_9EPn7mi0A8   ip: 203.0.113.30   id: o-ABGboQn9qMKsUdClvQHd6cHm6l1dWkRw4WNj3V7wBgY1   itag: 315   aitags: 133,134,135,136,160,242,243,244,247,278,298,299,302,303,308,315,394,395,396,397,398,399,400,401   source: youtube   requiressl: yes   mh: aP   mm: 31,29   mn: sn-8qu-t0aee,sn-t0a7ln7d   ms: au,rdu   mv: m   mvi: 1   pcm2cms: yes   pl: 18   initcwndbps: 1422500   spc: UWF9fzkQbIbHWdKe8-ahg0uWbE_UrbUM0U6LbQfFxg   vprv: 1   svpuc: 1   mime: video/webm   ns: dn5MLRkBtM4BWwzNNOhVxHIP   gir: yes   clen: 1536155487   dur: 634.566   lmt: 1662347928284893   mt: 1691807356   fvip: 3   keepalive: yes   fexp: 24007246,24363392   c: WEB   txp: 553C434   n: mAq3ayrWqdeV_7wbIgP   sparams: expire,ei,ip,id,aitags,source,requiressl,spc,vprv,svpuc,mime,ns,gir,clen,dur,lmt   sig: AOq0QJ8wRgIhAOx29gNeoiOLRe1GhEfE52PAiXW64ZEWX7nNdAiJE6ezAiEA0Plw6Yn0kmSFFZHO2JZPZyMGd0O-gEblUXPRrexQgrY=   lsparams: mh,mm,mn,ms,mv,mvi,pcm2cms,pl,initcwndbps   lsig: AG3C_xAwRQIgZVOkDl4rGPGnlK6IGCAXpzxk-cB5RRFmXDesEqOWTRoCIQCzIdPKE6C6_JQVpH6OKMF3woIJ2yVYaztT9mXIVtE6xw==

С середины 2021 года YouTube включает в большинство URL параметр запроса n. Этот параметр требуется преобразовывать при помощи алгоритма JavaScript, расположенного в файле base.js, который распространяется вместе с веб-страницей. YouTube использует этот параметр как клеймо, удостоверяющее, что загрузка выполнена через «официальный» клиент. Если клеймо не подтверждено, и n преобразовано неправильно, YouTube тихонько ограничит скорость при скачивании этого видео.

Алгоритм JavaScript обфусцирован и часто меняется, поэтому пытаться взломать его через реверс-инжиниринг — дохлый номер. Лучше просто скачать файл JavaScript, извлечь из него код алгоритма и выполнить, передав в этот код параметр n. (Кстати, вот урезанная версия интерпретатора JavaScript для работы с youtube-dl — прим. пер).

Именно это и сделано в следующем листинге.

import axios from 'axios'; import vm from 'vm'  const videoId = 'aqz-KE-bpKQ';  /**  * Извлекаем через API Youtube метаданные о видео (заголовок, формат видео и формат аудио)  */ async function retrieveMetadata(videoId) {     const response = await axios.post('https://www.youtube.com/youtubei/v1/player', {         "videoId": videoId,         "context": {             "client": { "clientName": "WEB", "clientVersion": "2.20230810.05.00" }         }     });      const formats = response.data.streamingData.adaptiveFormats;      return [         response.data.videoDetails.title,         formats.filter(w => w.mimeType.startsWith("video/webm"))[0],          formats.filter(w => w.mimeType.startsWith("audio/webm"))[0],     ]; }  /**  * С веб-страницы Youtube извлекаем алгоритм, позволяющий проверить параметр n данного запроса  */ async function retrieveChallenge(video_id){      /**      * Находим URL файла javascript для актуальной версии плеера      */     async function retrieve_player_url(video_id) {         let response = await axios.get('https://www.youtube.com/embed/' + video_id);         let player_hash = /\/s\/player\/(\w+)\/player_ias.vflset\/\w+\/base.js/.exec(response.data)[1]         return `https://www.youtube.com/s/player/${player_hash}/player_ias.vflset/en_US/base.js`     }      const player_url = await retrieve_player_url(video_id);      const response = await axios.get(player_url);     let challenge_name = /\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\([a-zA-Z0-9]\)/.exec(response.data)[1];     challenge_name = new RegExp(`var ${challenge_name}\\s*=\\s*\\[(.+?)\\]\\s*[,;]`).exec(response.data)[1];      const challenge = new RegExp(`${challenge_name}\\s*=\\s*function\\s*\\(([\\w$]+)\\)\\s*{(.+?}\\s*return\\ [\\w$]+.join\\(""\\))};`, "s").exec(response.data)[2];      return challenge; }  /**  * Проходим проверку и меняем параметр запроса n из url  */ function solveChallenge(challenge, formatUrl) {     const url = new URL(formatUrl);      const n = url.searchParams.get("n");     const n_transformed = vm.runInNewContext(`((a) => {${challenge}})('${n}')`);      url.searchParams.set("n", n_transformed);     return url.toString(); }   const [title, video, audio] = await retrieveMetadata(videoId); const challenge = await retrieveChallenge(videoId);  video.url = solveChallenge(challenge, video.url); audio.url = solveChallenge(challenge, audio.url);  console.log(video.url); 

Скачиваем медиа-файлы

Теперь у нас есть новый URL с корректно преобразованным параметром n. Дальше нужно скачать видео. Правда, YouTube всё равно обязывает ограничивать скорость загрузки. Речь идёт о лимите на скорость закачки, который варьируется в зависимости от размера и длительности видео. Цель — добиться, чтобы длительность скачивания была примерно вдвое меньше длительности самого видео. Это логично с точки зрения потоковой природы видеороликов. Для YouTube было бы крайне расточительно расходовать полосу передачи данных так, чтобы файл всегда скачивался в кратчайший срок.

http --print=b --download 'https://rr1---sn-8qu-t0aee.googlevideo.com/videoplayback?expire=1691888702&ei=3tfXZIXSI72c_9EP1NGHqA8 [TRUNCATED]'  Downloading to videoplayback.webm [ ━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ]   4% ( 0.1/1.5 GB ) 0:06:07 4.0 MB/s

Чтобы обойти это ограничение, можно разбить загрузку на несколько более мелких фрагментов; это делается при помощи HTTP-заголовка Range. В этом заголовке можно указать, какую часть файла вы хотите скачать при каждом из запросов (например: Range bytes=2000-3000). Эта логика реализована в следующем коде.

/**  * Скачать медиа-файл, разбив его на несколько сегментов по 10 МБ   */ async function download(url, length, file){     const MEGABYTE = 1024 * 1024;      await fs.promises.rm(file, { force: true });      let downloadedBytes = 0;      while (downloadedBytes < length) {         let nextSegment = downloadedBytes +  10 * MEGABYTE;         if (nextSegment > length) nextSegment = length;          // Скачать сегмент         const start = Date.now();         let response = await axios.get(url, { headers: { "Range": `bytes=${downloadedBytes}-${nextSegment}` }, responseType: 'stream' });                  // Записать сегмент         await fs.promises.writeFile(file, response.data, {flag: 'a'});         const end = Date.now();                  // Вывести статистику загрузки         const progress = (nextSegment / length * 100).toFixed(2);         const total = (length / MEGABYTE).toFixed(2);         const speed = ((nextSegment - downloadedBytes) / (end - start) * 1000 / MEGABYTE).toFixed(2);         console.log(`${progress}% of ${total}MB at ${speed}MB/s`);                  downloadedBytes = nextSegment + 1;     } }

Такой подход работает, поскольку на применение ограничения уходит некоторое время, а мелкие сегменты загружаются очень быстро, и для каждого сегмента открывается новое соединение.

node index.js  0.68% of 1464.99MB at 46.73MB/s 1.37% of 1464.99MB at 60.98MB/s 2.05% of 1464.99MB at 71.94MB/s 2.73% of 1464.99MB at 70.42MB/s 3.41% of 1464.99MB at 68.49MB/s 4.10% of 1464.99MB at 68.97MB/s 4.78% of 1464.99MB at 74.07MB/s 5.46% of 1464.99MB at 81.97MB/s 6.14% of 1464.99MB at 104.17MB/s

Теперь мы можем скачивать видео гораздо быстрее. Когда я тестировал код, при некоторых закачках соединение в 1 Гб/с удавалось использовать почти целиком. Но средняя скорость обычно варьируется в пределах 50-70 МБ/с или 400-560 Mб/с, что всё равно достаточно быстро.

Постобработка

YouTube раздаёт каналы видео и аудио в двух отдельных файлах. Так экономится место, поскольку видео в качестве HD и UHD могут использовать один и тот же аудио-файл. Кроме того, в некоторых видео теперь предлагаются и другие аудиоканалы (с привязкой к языку текста). Следовательно, нам остаётся только лишь совместить два этих канала в один файл, и для этого можно воспользоваться ffmpeg.

/**  * Использование ffmpeg, комбинируем воедино аудио и видео   */ async function combineChannels(destinationFile, videoFile, audioFile) {     await fs.promises.rm(destinationFile, { force: true });     child_process.spawnSync('ffmpeg', [         "-y",         "-i", videoFile,         "-i", audioFile,         "-c", "copy",         "-map", "0:v:0",         "-map", "1:a:0",         destinationFile     ]);      await fs.promises.rm(videoFile, { force: true });     await fs.promises.rm(audioFile, { force: true }); }

Наконец, если кому-то интересно, полный код к этому посту можно скачать здесь.

Заключение

Такие приёмы помогают многим проектам обходить ограничения YouTube по скачиванию видео. Самые популярные из них — yt-dlp (форк youtube-dl), написанный на Python, но в нём встроен собственный интерпретатор JavaScript, при помощи которого преобразуется параметр n.

yt-dlp

https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/youtube.py

Медиаплеер VLC

https://github.com/videolan/vlc/blob/master/share/lua/playlist/youtube.lua

NewPipe

https://github.com/Theta-Dev/NewPipeExtractor/blob/dev/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeJavaScriptExtractor.java

node-ytdl-core

https://github.com/fent/node-ytdl-core/blob/master/lib/sig.js


ссылка на оригинал статьи https://habr.com/ru/articles/754654/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *