Skip to content

Instantly share code, notes, and snippets.

@szkrd
Created March 7, 2021 19:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save szkrd/29c842228078c5019e18d3ad18b741d9 to your computer and use it in GitHub Desktop.
Save szkrd/29c842228078c5019e18d3ad18b741d9 to your computer and use it in GitHub Desktop.
Listen to a youtube stream in puppeteer (headless chrome/chromium). By default plays Chillcow's channel, works on linux and windows, clicks on all the popup crap.
// first: `npm init -- yes && npm i chalk puppeteer-core`
// then: `node . --help`
const puppeteer = require('puppeteer-core');
const chalk = require('chalk');
// ---
const ifHasParam = (s = '') => process.argv.includes(s);
const paramValueOf = (s = '') => (process.argv.find(x => x.startsWith(`${s}=`)) || '').split('=')[1];
const DEFAULT_VIDEO_ID = '5qap5aO4i9A'; // ChillCow; his other channel is "DWcJFNfaw9c"
const DEBUG = ifHasParam('--debug');
const SHOW_HEAD = ifHasParam('--head');
const MAX_TRIES = parseInt(paramValueOf('--iteration'), 10) || 20;
const VIDEO_ID = paramValueOf('--vid');
const URL = `https://www.youtube.com/watch?v=${VIDEO_ID || DEFAULT_VIDEO_ID}`;
const ALSA_DEVICE = paramValueOf('--alsa');
let BROWSER_PATH = paramValueOf('--browser');;
if (!BROWSER_PATH && process.platform === 'linux') BROWSER_PATH = '/usr/bin/chromium';
if (!BROWSER_PATH && process.platform === 'win32') BROWSER_PATH = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
// ---
if (ifHasParam('--help')) {
console.info('Params (all optional):\n--help\n--debug\n--head\n--vid=YOUTUBE_vID' +
'\n--iteration=N\n--browser=/usr/bin/chromium\n--alsa=ALSA_DEVICE_ID');
process.exit();
}
let pup = { browser: null, page: null };
const selectors = {
pageBody: 'body',
gdprButton: 'button[aria-label^="Agree"]',
introAgreeButton: '#introAgreeButton',
playButton: 'button[aria-label="Play"]',
noThanksButton: '#button[aria-label="No thanks"]',
dismissBullshitButton: '#dismiss-button paper-button',
consentIframe: 'iframe[src*="consent"]',
consentForm: 'form[action*="consent"]',
skipAdButton: 'button[class*="skip-button"]'
};
const print = (...args) => console.info(chalk.cyan(...args));
const sleep = (t = 0) => new Promise((resolve) => { setTimeout(resolve, t); });
async function waitForSelector(sel = '', timeout = 2000) {
if (DEBUG) print(`Waiting for ${chalk.blue(sel)}`);
if (selectors[sel]) sel = selectors[sel];
let success = true;
try { success = await pup.page.waitForSelector(sel, { timeout }); } catch (err) { success = false; }
return success;
}
const _clicked = {};
async function click(sel = '', times = 1) {
if (DEBUG) print(`Clicking "${chalk.blue(sel)}"`);
if (selectors[sel]) sel = selectors[sel];
if (_clicked[sel] >= times) return;
let success = true;
try { await pup.page.click(sel); } catch (err) { success = false; }
if (success) _clicked[sel] = (_clicked[sel] || 0) + 1;
return success;
}
async function acceptConsent() {
let hasConsentFrame = await waitForSelector('consentIframe');
if (!hasConsentFrame) return;
const frame = pup.page.frames().find((frame, i) => frame.url().includes('consent'));
if (frame) {
let success = true;
try { await frame.waitForSelector(selectors.consentForm, { timeout: 2000 }); } catch (err) { success = false; }
if (success) {
if (DEBUG) print('Submitting consent in iframe')
await frame.$eval(selectors.consentForm, form => form.submit());
}
}
}
async function quit(code = 0, message = '') {
if (message) print(message);
await pup.browser.close();
process.exit(code);
}
async function handleExit() {
await quit(0, 'Closing chrome, good bye!');
}
// ---
(async () => {
process.on('SIGINT', handleExit);
print(`Let's try to play ${chalk.white(URL)}`);
const headless = !SHOW_HEAD;
const args = [
'--autoplay-policy=no-user-gesture-required',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-crash-reporter',
// youtube will block playback for headless browsers
// for headless detection see: https://intoli.com/blog/making-chrome-headless-undetectable/
'--user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"',
'--use-gl=egl', // linux headless
// linux sound: use `aplay -L` then `speaker-test -Dplug:front -c2` to determine the proper device
// and also, please fuck pulseaudio and all the devices it rode in on, yes, even in 2021
ALSA_DEVICE ? `--alsa-output-device=${ALSA_DEVICE}` : '',
headless ? '--headless' : '' // headless by itself in pup params is NOT enough of course
].filter(x => x);
if (DEBUG) print(`Args:\n${args.join('\n')}`);
const ignoreDefaultArgs = '--mute-audio';
const browser = pup.browser = await puppeteer.launch({ headless, executablePath: BROWSER_PATH, ignoreDefaultArgs, args });
const page = pup.page = await browser.newPage();
await page.goto(URL);
for (let i = 0; i < MAX_TRIES; i++) { // when in doubt, use brute force
print(`Iteration ${chalk.white(i + 1)}/${chalk.white(MAX_TRIES)}...`);
if (DEBUG) page.screenshot({ path: `screen-${i}.png` });
await sleep(3000);
const hasBody = await waitForSelector('pageBody');
if (!hasBody) { print('No body?!'); continue; }
await acceptConsent();
await click('playButton');
await click('gdprButton');
await click('dismissBullshitButton');
await click('noThanksButton');
await click('skipAdButton');
await click('introAgreeButton');
}
print('Done. Press ctrl+c to exit.');
})();
@szkrd
Copy link
Author

szkrd commented Apr 7, 2021

Google broke the sign in modal (the video doesn't start after clicking on the "No thanks" button, not even in a "real" browser) and reviving the player in a sane way after that is kinda complicated.

If the stream allows embedding, then here's the simplifed version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment