Используем именно Google Chrome, так как в Chromium не все работает, например, Shaka player (Google).
После установки Node.js
sudo dnf module list nodejs
sudo dnf module enable nodejs:12
sudo dnf install nodejs
Устанавливаем puppeteer без Chromium (350 МВ)
echo "export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true && npm install i puppeteer" | sudo sh
При запуске puppeteer указываем путь к исполняемому файлу Google Chrome
executablePath: '/opt/google/chrome/google-chrome',
Выключаем CORS
'--disable-web-security',
'--disable-site-isolation-trials',
Из интересного для сохранения «.m3u8» и «.ts» используем page.evaluate, далее fetch в контексте браузера, затем передаем содержимое файла в puppeteer
let dataString = '[' + (new Uint8Array(data)).toString() + ']';
и сохраняем файл определенной с помощью page.exposeFunction функцией writefile.
'use strict';
const puppeteer = require('puppeteer');
const fs = require('fs');
(async function main() {
var browser;
try {
var myArgs = process.argv.slice(2);
if (!myArgs.length) {
throw('need dir');
}
console.log('dir: ', myArgs[0]);
browser = await puppeteer.launch({
headless: true,
slowMo: 250, // slow down by 250ms
// devtools: true,
executablePath: '/opt/google/chrome/google-chrome',
defaultViewport: null, // иначе не работает --window-size=1920,1080
args:[
'--window-size=1920,1080',
'--disable-setuid-sandbox',
'--no-sandbox',
'--disable-infobars',
'--disable-web-security',
'--disable-site-isolation-trials',
],
});
/*
var lessonUrl = myArgs[0]
const crypto = require('crypto');
const hash = crypto.createHash('sha256');
hash.update(lessonUrl);
var dir = 'fdb' + '/' + hash.digest('hex');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
var dirOriginal = dir + '/' + 'original';
if (!fs.existsSync(dirOriginal)) {
fs.mkdirSync(dirOriginal);
}
*/
var lessonUrl = myArgs[1];
var dir = myArgs[0];
var dirOriginal = dir + '/' + 'original';
const regUrl = 'https://zzz.ru/user/registration'
const page = await browser.newPage()
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// await page.setDefaultNavigationTimeout(60000);
await page.goto(regUrl, { waitUntil: 'load' })
await page.waitForTimeout(1000);
await page.screenshot({ path: dir + '/' + 'screenshot01.png' })
let checkUrl = await page.evaluate(() => location.href);
if (regUrl == checkUrl) {
await page.click('[data-qa="__authEmailButton"]')
await page.type('#email', 'username@mail.ru')
await page.type('#password', '12wdnj4ss7ty')
await page.screenshot({ path: dir + '/' + 'screenshot02.png' })
await Promise.all([
page.click('[data-qa="__authEmailSubmitButton"]'),
page.waitForNavigation(), // if "await page.waitForNavigation" then "TimeoutError: Navigation timeout of 30000 ms exceeded", причем на ноде поновее всё работало
]);
}
await page.waitForTimeout(2000);
await page.screenshot({ path: dir + '/' + 'screenshot1.png' })
console.log(lessonUrl);
await page.goto(lessonUrl, { waitUntil: 'load' })
await page.waitForTimeout(2000);
await page.screenshot({ path: dir + '/' + 'screenshot2.png' })
/*
// forEach и async не очень дружат
var videoFrame;
page.frames().forEach(async (frame) => {
let tt = await frame.$('#shaka-video');
if (tt) {
videoFrame = frame;
}
});
*/
var videoFrame;
for (const frame of page.frames()) {
let tt = await frame.$('#shaka-video');
if (tt) {
videoFrame = frame;
}
}
if (!videoFrame) {
throw('#shaka-video not found!');
}
if (videoFrame) {
console.log(videoFrame.url());
await page.goto(videoFrame.url(), { waitUntil: 'load' })
await page.waitForTimeout(5000);
await page.screenshot({ path: dir + '/' + 'screenshot3.png' })
const filesUrls = await page.evaluate(() => {
var urls = [];
var ui0 = document.getElementById('shaka-player').ui;
urls.push(ui0.b.Fa);
// "https://storage.svc.services/api/v2/backends/yandex/sets/hls.webinar::198a2db0-e74b-44c8-ac22-7cec61972e8a/objects/long.v2.yandex.master.m3u8"
// ссылка после редиректа получаем подписанные (pre-signed) URL
// ui0.b.s.K
// "https://storage-lb.services/hls.webinar/198a2db0-e74b-44c8-ac22-7cec61972e8a.long.v2.yandex.master.m3u8?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=DNmtuhzj5w4VFKS-9BUA%2F20201228%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20201228T142459Z&X-Amz-Expires=300&X-Amz-Signature=0a10db545d16abdfd81c988d7f6e488dfc36502aab60456da78afa9f1ed5bac8&X-Amz-SignedHeaders=host"
var a0=v0=undefined;
// select audio and video streams
ui0.b.s.a.forEach((v,k)=>{
if (!a0 && v.stream.type=='audio') {a0=v;}
if (v.stream.type=='video' && (!v0 || v.stream.width > v0.stream.width)) {v0=v;}
});
// load and save selected audio and video segments description files
urls.push(a0.Wc);
urls.push(v0.Wc);
// ["https://storage.svc.services/api/v2/backends/yandex/sets/hls.webinar::198a2db0-e74b-44c8-ac22-7cec61972e8a/objects/long.audio.32kbps.1.1608056896.ts" length: 1 __proto__: Array(0)
a0.stream.segmentIndex.a.forEach((v,k)=>{
urls = urls.concat(v.c());
});
v0.stream.segmentIndex.a.forEach((v,k)=>{
urls = urls.concat(v.c());
});
return urls;
});
console.log(filesUrls[0], filesUrls[1], filesUrls[2], filesUrls[3], filesUrls.length);
var filesUrlsSaved = 0;
await page.exposeFunction('writefile', async (filePath, data) => {
// let dataBuffer = new TextEncoder().encode(data);
filesUrlsSaved++;
let dataBuffer = new Uint8Array(JSON.parse(data));
return new Promise((resolve, reject) => {
fs.writeFile(filePath, dataBuffer, {encoding: null}, (err) => {
if (err)
reject(err);
else
resolve();
});
});
});
if (filesUrls.length) {
fs.writeFileSync(dirOriginal + '/loading', filesUrls[1] + '\n' + filesUrls[2] + '\n', {flag: 'w'});
}
for (var loadFile of filesUrls) {
await page.evaluate(async (dirOriginal, loadFile) => {
let fileName = loadFile.match('[^/]*$')[0].match('^[^#?]*');
await fetch(loadFile, {
method: "GET",
redirect: 'follow',
}).then((response) => {
console.log('url with auth', response.url);
return response.arrayBuffer();
}).then(async function(data) {
// let dataView = new Uint8Array(data);
// let dataString = new TextDecoder().decode(data);
let dataString = '[' + (new Uint8Array(data)).toString() + ']';
await window.writefile(dirOriginal + '/' + fileName, dataString);
});
}, dirOriginal, loadFile);
/*
// error 403
var viewSource = await page.goto(loadFile);
fs.writeFile(dirOriginal + '/' + fileName, await viewSource.buffer(), function (err) {
if (err) {
return console.log(err);
}
});
*/
}
// if (filesUrls.length) {
// fs.writeFileSync(dirOriginal + '/loaded', '', {flag: 'w'});
// }
if (filesUrls.length != filesUrlsSaved) {
console.error('filesUrls:', filesUrls.length, filesUrlsSaved);
throw('Not all files saved!');
}
}
browser.close()
console.log('See screenshot in: ' + dir)
} catch (err) {
console.error(err);
browser.close()
}
})();
Еще чуток бэкенда на php
['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
$proc = proc_open($cmd . "\n", $descriptorspec, $pipes);
foreach ($pipes as $pipe) {
stream_set_blocking($pipe, false);
}
fwrite($pipes[0], $input); fclose($pipes[0]);
// $stdout = stream_get_contents($pipes[1]);
// $stderr = stream_get_contents($pipes[2]);
$stdout = '';
$stderr = '';
while (!feof($pipes[1])) $stdout .= fgets($pipes[1],1024);
while (!feof($pipes[2])) $stderr .= fgets($pipes[2],1024);
fclose($pipes[1]);
fclose($pipes[2]);
$return = proc_close($proc);
return [
'stdout' => $stdout,
'stderr' => $stderr,
'return' => $return
];
}
...
// копируем все ts в один файл output.mp4 без преобразования, очень быстро
// при склейке обычно "убегает" звук, "-itsoffset 0.5", сдвигает видео дорожку на 500 мс
$command = sprintf('/usr/bin/ffmpeg -loglevel repeat+level+error -nostats -y -i "%s" -itsoffset 0.147978 -i "%s" -vcodec copy -c:a copy "output.mp4"', $m3u8files[0], $m3u8files[1]);
$ret = exec_return($command);
file_put_contents("{$forConvert->full}/converted/error", $ret['stderr']);
chmod("{$forConvert->full}/converted/error", 0666);
file_put_contents("{$forConvert->full}/converted/converted", $ret['stdout']);
chmod("{$forConvert->full}/converted/converted", 0666);
...