mirror of
https://github.com/LuanRT/googlevideo.git
synced 2026-06-13 08:42:31 +00:00
refactor(ServerAbrStream): Clean up
+ Add an example with ffmpeg.
This commit is contained in:
@@ -22,4 +22,7 @@ deno run --allow-net --allow-read examples/browser/proxy/deno.ts
|
||||
cd examples/downloader
|
||||
npm install
|
||||
npx tsx main.ts
|
||||
|
||||
## Example with ffmpeg
|
||||
npx tsx ffmpeg-example.ts
|
||||
```
|
||||
147
examples/downloader/ffmpeg-example.ts
Normal file
147
examples/downloader/ffmpeg-example.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { WriteStream } from 'node:fs';
|
||||
import { createWriteStream, unlink } from 'node:fs';
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
import GoogleVideo, { concatenateChunks, type Format, MediaType } from '../../dist/src/index.js';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
|
||||
const innertube = await Innertube.create({ cache: new UniversalCache(true) });
|
||||
const info = await innertube.getBasicInfo('wRNnMQEKo7o');
|
||||
|
||||
console.info(`
|
||||
Title: ${info.basic_info.title}
|
||||
Duration: ${info.basic_info.duration}
|
||||
Views: ${info.basic_info.view_count}
|
||||
Author: ${info.basic_info.author}
|
||||
Video ID: ${info.basic_info.id}
|
||||
`);
|
||||
|
||||
const durationMs = info.basic_info?.duration ? info.basic_info.duration * 1000 : 0;
|
||||
const sanitizedTitle = info.basic_info.title?.replace(/[^a-z0-9]/gi, '_');
|
||||
|
||||
let audioOutput: WriteStream | undefined;
|
||||
let videoOutput: WriteStream | undefined;
|
||||
let audioOutputFilename: string | undefined;
|
||||
let videoOutputFilename: string | undefined;
|
||||
|
||||
const audioFormat = info.chooseFormat({ quality: 'best', format: 'webm', type: 'audio' });
|
||||
const videoFormat = info.chooseFormat({ quality: '1080p', format: 'webm', type: 'video' });
|
||||
|
||||
const selectedAudioFormat: Format = {
|
||||
itag: audioFormat.itag,
|
||||
lastModified: audioFormat.last_modified_ms,
|
||||
xtags: audioFormat.xtags
|
||||
};
|
||||
|
||||
const selectedVideoFormat: Format = {
|
||||
itag: videoFormat.itag,
|
||||
lastModified: videoFormat.last_modified_ms,
|
||||
width: videoFormat.width,
|
||||
height: videoFormat.height,
|
||||
xtags: videoFormat.xtags
|
||||
};
|
||||
|
||||
const serverAbrStreamingUrl = innertube.session.player?.decipher(info.page[0].streaming_data?.server_abr_streaming_url);
|
||||
const videoPlaybackUstreamerConfig = info.page[0].player_config?.media_common_config.media_ustreamer_request_config?.video_playback_ustreamer_config;
|
||||
|
||||
if (!videoPlaybackUstreamerConfig)
|
||||
throw new Error('ustreamerConfig not found');
|
||||
|
||||
if (!serverAbrStreamingUrl)
|
||||
throw new Error('serverAbrStreamingUrl not found');
|
||||
|
||||
const serverAbrStream = new GoogleVideo.ServerAbrStream({
|
||||
fetch: innertube.session.http.fetch_function,
|
||||
serverAbrStreamingUrl,
|
||||
videoPlaybackUstreamerConfig: videoPlaybackUstreamerConfig,
|
||||
durationMs
|
||||
});
|
||||
|
||||
serverAbrStream.on('data', (data) => {
|
||||
let progressText = '';
|
||||
|
||||
for (const initializedFormat of data.initializedFormats) {
|
||||
const isVideo = initializedFormat.mimeType?.includes('video');
|
||||
const mediaFormat = info.streaming_data?.adaptive_formats.find((f) => f.itag === initializedFormat.formatId.itag);
|
||||
|
||||
const data = concatenateChunks(initializedFormat.mediaChunks);
|
||||
|
||||
if (isVideo && data.length) {
|
||||
if (!videoOutput) {
|
||||
videoOutputFilename = `${sanitizedTitle}.${initializedFormat.formatId.itag}.webm`;
|
||||
videoOutput = createWriteStream(videoOutputFilename);
|
||||
}
|
||||
videoOutput.write(data);
|
||||
} else if (data.length) {
|
||||
if (!audioOutput) {
|
||||
audioOutputFilename = `${sanitizedTitle}.${initializedFormat.formatId.itag}.webm`;
|
||||
audioOutput = createWriteStream(audioOutputFilename);
|
||||
}
|
||||
audioOutput.write(data);
|
||||
}
|
||||
|
||||
const fmtIdentifier = `${initializedFormat.formatId.itag}_${initializedFormat.mimeType?.split(';')[0]}`;
|
||||
const percentage = Math.round((initializedFormat.sequenceList.at(-1)?.startDataRange ?? 0) / (mediaFormat?.content_length ?? 0) * 100);
|
||||
|
||||
if (percentage)
|
||||
progressText += `${fmtIdentifier}: ${percentage}% | `;
|
||||
}
|
||||
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(progressText);
|
||||
});
|
||||
|
||||
serverAbrStream.on('error', (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
await serverAbrStream.init({
|
||||
audioFormats: [ selectedAudioFormat ],
|
||||
videoFormats: [ selectedVideoFormat ],
|
||||
mediaInfo: {
|
||||
mediaType: MediaType.MEDIA_TYPE_DEFAULT,
|
||||
startTimeMs: 0
|
||||
}
|
||||
});
|
||||
|
||||
if (audioOutput)
|
||||
audioOutput.end();
|
||||
|
||||
if (videoOutput)
|
||||
videoOutput.end();
|
||||
|
||||
const outputFilename = `${sanitizedTitle}_final.webm`;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (!videoOutputFilename || !audioOutputFilename)
|
||||
return reject(new Error('No video or audio output filename'));
|
||||
|
||||
ffmpeg()
|
||||
.input(videoOutputFilename)
|
||||
.input(audioOutputFilename)
|
||||
.videoCodec('copy')
|
||||
.audioCodec('copy')
|
||||
.on('progress', (progress) => {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Processing: ${progress.timemark} (${progress.percent?.toFixed(2)}%)`);
|
||||
})
|
||||
.on('end', () => {
|
||||
if (videoOutputFilename) {
|
||||
unlink(videoOutputFilename, (err) => {
|
||||
if (err) console.error(`Error deleting video temp file: ${err}`);
|
||||
});
|
||||
}
|
||||
if (audioOutputFilename) {
|
||||
unlink(audioOutputFilename, (err) => {
|
||||
if (err) console.error(`Error deleting audio temp file: ${err}`);
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.on('error', (err: Error) => {
|
||||
console.error('Error processing video:', err);
|
||||
reject(err);
|
||||
})
|
||||
.save(outputFilename);
|
||||
});
|
||||
@@ -1,38 +1,27 @@
|
||||
import type { WriteStream } from 'node:fs';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
import GoogleVideo, { type Format, MediaType } from '../../dist/src/index.js';
|
||||
import GoogleVideo, { concatenateChunks, type Format, MediaType } from '../../dist/src/index.js';
|
||||
|
||||
const innertube = await Innertube.create({ cache: new UniversalCache(true) });
|
||||
const info = await innertube.getBasicInfo('el68RBQBlCs');
|
||||
|
||||
const determineFileExtension = (mimeType: string) => {
|
||||
if (mimeType.includes('video'))
|
||||
return mimeType.includes('webm') ? 'webm' : 'mp4';
|
||||
else if (mimeType.includes('audio'))
|
||||
return mimeType.includes('webm') ? 'webm' : 'm4a';
|
||||
};
|
||||
|
||||
const info = await innertube.getBasicInfo('qQ_-toSu29Q');
|
||||
|
||||
console.log('\n');
|
||||
console.info(`Title: ${info.basic_info.title}`);
|
||||
console.info(`Duration: ${info.basic_info.duration}`);
|
||||
console.info(`Views: ${info.basic_info.view_count}`);
|
||||
console.info(`Author: ${info.basic_info.author}`);
|
||||
console.info(`Video ID: ${info.basic_info.id}`);
|
||||
console.info(`
|
||||
Title: ${info.basic_info.title}
|
||||
Duration: ${info.basic_info.duration}
|
||||
Views: ${info.basic_info.view_count}
|
||||
Author: ${info.basic_info.author}
|
||||
Video ID: ${info.basic_info.id}
|
||||
`);
|
||||
|
||||
const durationMs = info.basic_info?.duration ? info.basic_info.duration * 1000 : 0;
|
||||
const sanitizedTitle = info.basic_info.title?.replace(/[^a-z0-9]/gi, '_');
|
||||
|
||||
let wroteAudioInitSegment = false;
|
||||
let wroteVideoInitSegment = false;
|
||||
|
||||
let audioOutput: WriteStream | undefined;
|
||||
let videoOutput: WriteStream | undefined;
|
||||
|
||||
const durationMs = info.basic_info?.duration ? info.basic_info.duration * 1000 : 0;
|
||||
|
||||
const audioFormat = info.chooseFormat({ quality: 'best', format: 'webm', type: 'audio' });
|
||||
const videoFormat = info.chooseFormat({ quality: '720p', format: 'webm', type: 'video' });
|
||||
const videoFormat = info.chooseFormat({ quality: '1080p', format: 'webm', type: 'video' });
|
||||
|
||||
const selectedAudioFormat: Format = {
|
||||
itag: audioFormat.itag,
|
||||
@@ -48,20 +37,22 @@ const selectedVideoFormat: Format = {
|
||||
xtags: videoFormat.xtags
|
||||
};
|
||||
|
||||
console.info(`Selected audio format: ${audioFormat.itag} (${audioFormat.audio_quality})`);
|
||||
console.info(`Selected video format: ${videoFormat.itag} (${videoFormat.quality_label})`);
|
||||
console.log('\n');
|
||||
|
||||
const serverAbrStreamingUrl = innertube.session.player?.decipher(info.page[0].streaming_data?.server_abr_streaming_url);
|
||||
const videoPlaybackUstreamerConfig = info.page[0].player_config?.media_common_config.media_ustreamer_request_config?.video_playback_ustreamer_config;
|
||||
|
||||
if (!videoPlaybackUstreamerConfig)
|
||||
throw new Error('ustreamerConfig not found');
|
||||
|
||||
const serverAbrStreamingUrl = innertube.session.player?.decipher(info.page[0].streaming_data?.server_abr_streaming_url);
|
||||
|
||||
if (!serverAbrStreamingUrl)
|
||||
throw new Error('serverAbrStreamingUrl not found');
|
||||
|
||||
const determineFileExtension = (mimeType: string) => {
|
||||
if (mimeType.includes('video'))
|
||||
return mimeType.includes('webm') ? 'webm' : 'mp4';
|
||||
else if (mimeType.includes('audio'))
|
||||
return mimeType.includes('webm') ? 'webm' : 'm4a';
|
||||
};
|
||||
|
||||
const serverAbrStream = new GoogleVideo.ServerAbrStream({
|
||||
fetch: innertube.session.http.fetch_function,
|
||||
serverAbrStreamingUrl,
|
||||
@@ -76,26 +67,16 @@ serverAbrStream.on('data', (data) => {
|
||||
const isVideo = initializedFormat.mimeType?.includes('video');
|
||||
const mediaFormat = info.streaming_data?.adaptive_formats.find((f) => f.itag === initializedFormat.formatId.itag);
|
||||
|
||||
if (isVideo && initializedFormat.mediaData) {
|
||||
const data = concatenateChunks(initializedFormat.mediaChunks);
|
||||
|
||||
if (isVideo && data.length) {
|
||||
if (!videoOutput)
|
||||
videoOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.formatId.itag}.${determineFileExtension(initializedFormat.mimeType || '')}`);
|
||||
|
||||
if (initializedFormat.initSegment && !wroteVideoInitSegment) {
|
||||
videoOutput.write(initializedFormat.initSegment);
|
||||
wroteVideoInitSegment = true;
|
||||
}
|
||||
|
||||
videoOutput.write(initializedFormat.mediaData);
|
||||
} else if (initializedFormat.mediaData) {
|
||||
videoOutput.write(data);
|
||||
} else if (data.length) {
|
||||
if (!audioOutput)
|
||||
audioOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.formatId.itag}.${determineFileExtension(initializedFormat.mimeType || '')}`);
|
||||
|
||||
if (initializedFormat.initSegment && !wroteAudioInitSegment) {
|
||||
audioOutput.write(initializedFormat.initSegment);
|
||||
wroteAudioInitSegment = true;
|
||||
}
|
||||
|
||||
audioOutput.write(initializedFormat.mediaData);
|
||||
audioOutput.write(data);
|
||||
}
|
||||
|
||||
const fmtIdentifier = `${initializedFormat.formatId.itag}_${initializedFormat.mimeType?.split(';')[0]}`;
|
||||
@@ -115,6 +96,18 @@ serverAbrStream.on('error', (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
serverAbrStream.on('end', () => {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write('Done!');
|
||||
|
||||
if (audioOutput)
|
||||
audioOutput.end();
|
||||
|
||||
if (videoOutput)
|
||||
videoOutput.end();
|
||||
});
|
||||
|
||||
await serverAbrStream.init({
|
||||
audioFormats: [ selectedAudioFormat ],
|
||||
videoFormats: [ selectedVideoFormat ],
|
||||
@@ -127,12 +120,4 @@ await serverAbrStream.init({
|
||||
mediaType: MediaType.MEDIA_TYPE_DEFAULT,
|
||||
startTimeMs: 0
|
||||
}
|
||||
});
|
||||
|
||||
process.stdout.write('Done!');
|
||||
|
||||
if (audioOutput)
|
||||
audioOutput.end();
|
||||
|
||||
if (videoOutput)
|
||||
videoOutput.end();
|
||||
});
|
||||
59
examples/downloader/package-lock.json
generated
59
examples/downloader/package-lock.json
generated
@@ -9,10 +9,12 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"shaka-player": "^4.11.2",
|
||||
"youtubei.js": "github:LuanRT/YouTube.js#refactor/update-protos"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "^2.1.26",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
@@ -29,6 +31,24 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/fluent-ffmpeg": {
|
||||
"version": "2.1.26",
|
||||
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.26.tgz",
|
||||
"integrity": "sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
|
||||
"integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
@@ -40,11 +60,33 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/eme-encryption-scheme-polyfill": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.5.tgz",
|
||||
"integrity": "sha512-z9BKXV4TCYjmar0wiZLObZ0J8HE13VIg7Zq/iyPWdbEfROtxVXEJalknWKtBR5XNezzy15/zWS964TGbcAWlPg=="
|
||||
},
|
||||
"node_modules/fluent-ffmpeg": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
|
||||
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
|
||||
"dependencies": {
|
||||
"async": "^0.2.9",
|
||||
"which": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-2.1.1.tgz",
|
||||
@@ -96,6 +138,23 @@
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/youtubei.js": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "git+ssh://git@github.com/LuanRT/YouTube.js.git#9bcbdb06b834067886689d25a82f890a96fcf0f7",
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"shaka-player": "^4.11.2",
|
||||
"youtubei.js": "github:LuanRT/YouTube.js#refactor/update-protos"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "^2.1.26",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user