Вы когда-нибудь пробовали скачивать видео с 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

Комментарии (22)


  1. vindy123
    15.08.2023 08:38
    +3

    А кто то может посоветовать рабочий альтернативный клиент под Андроид? Revanced у меня почему то не собирается, new pipe - уж очень убого. Что ещё бывает?


    1. crawlingroof
      15.08.2023 08:38
      +2

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


      1. Tarakanator
        15.08.2023 08:38
        +2

        revanced банят. видео дальше 1:09 не грузится. Проблема решается, но только пляской с бубном, или рут версией. Так что я присоединяюсь к вопросу о альтернативном клиенте.


        1. nixtonixto
          15.08.2023 08:38
          +1

          Обновите с 4пда. Там на следующий же день выкатили исправленную версию. Или, как подсказали здесь — в разделе Дополнительные, найти пункт Протокол буфера и включить его.


          1. Tarakanator
            15.08.2023 08:38

            Там же надпись что это может не помочь и 100% решения нет. И чтоб не писали те, у кого не работает, т.к. решения нет, а тему захламлять не стоит.
            Мне пришлось ещё создать новый канал и с бубном поплясать. заработало. Пишут статистически работать будет от нескольких дней, до нескольких недель.


          1. Tarakanator
            15.08.2023 08:38

            UPD. Прошли сутки, канал опять в чёрный список пихнули. Опять на 1:09 виснет. Поменял канал, работает... но сколько? опять сутки?


            1. nixtonixto
              15.08.2023 08:38

              Вчера весь вечер смотрел, сегодня проверил — показывает дальше 1:09 и перематывает даже на второй час. Версия 18.30.37 NonRoot. Попробуйте переустановить по инструкции с упомянутого выше сайта.


              1. Tarakanator
                15.08.2023 08:38

                18:31:40
                А где там инструкция? Там только упоминание о том, что microg надо обязательно заблаговременно ставить... ну а единой инструкции по обходу блокировок вообще нет. В шапке написано что если вам не помогло, сидите и не рыпайтесь, 100% решения нет. Ну а по постам советы противоречат друг другу.


    1. hssergey
      15.08.2023 08:38

      Вот же в статье упомянут NewPipe: https://newpipe.net/


      1. vindy123
        15.08.2023 08:38
        +1

        Newpipe:

        Живые стримы не играет

        Не показывает твои подписки

        В последние пару недель тупо крэшится через минуту максимум на любом видео


        1. Doman
          15.08.2023 08:38
          +1

          Уже вышел фикс крэша.


        1. lrrr11
          15.08.2023 08:38

          у разработчиков newpipe есть официальная репа для fdroid, https://archive.newpipe.net/fdroid/repo/?fingerprint=E2402C78F9B97C6C89E97DB914A2751FDA1D02FE2039CC0897A462BDB57E7501 - там оперативно появляются свежие версии


    1. theurus
      15.08.2023 08:38

      libretube


    1. Duke_nukeum
      15.08.2023 08:38

      Revanced Extended


  1. novoselov
    15.08.2023 08:38

    А есть клиент под Android TV который позволяет загружать разрешение выше чем 1080p?


    1. safari2012
      15.08.2023 08:38

      у меня свисток умеет 4k/HDR через родного клиента youtube. правда от версии к версии иногда подлагивает на таком разрешении.


    1. YouROK
      15.08.2023 08:38

      SmartTubeNext вроде как может, но у меня тормозят некоторые ролики


  1. alexbsd
    15.08.2023 08:38

    Спасибо, сделал docker container, подправил немного скрипт что бы он принимал аргументы и все работает!


  1. anzay911
    15.08.2023 08:38

    А что, base.js никогда не повторяется после смены? Может, их всего несколько разных.


  1. dimka11
    15.08.2023 08:38

    Youtube-dl в последние годы тоже скачивает очень медленно, отказался от его использования.


    1. serejk
      15.08.2023 08:38
      +1

      Да, они где то даже писали про это. Я перешел на yt-dlp.


  1. VaalKIA
    15.08.2023 08:38

    Раньше, смотрел видео с Ютьюба, через VLC на 3x+ скорости, сейчас заменил lua на указанный вами, но ничего не изменилось: как затыкается каждую секунду, так и осталось.

    Так что, приходится. как последнему тормозу, смотреть на 2x. Да и сам lua от февраля, когда статью-то готовили?