chore(examples): clean up

This commit is contained in:
Luan
2024-09-15 15:30:38 -03:00
parent f273a416c7
commit a1e573831b
6 changed files with 189 additions and 74 deletions

View File

@@ -1,8 +1,16 @@
import ffmpeg from 'fluent-ffmpeg';
import cliProgress from 'cli-progress';
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';
import GoogleVideo, { type Format, MediaType } from '../../dist/src/index.js';
const progressBars = new cliProgress.MultiBar({
clearOnComplete: false,
hideCursor: true
}, cliProgress.Presets.rect);
const formatProgressBars = new Map<string, cliProgress.SingleBar>();
const innertube = await Innertube.create({ cache: new UniversalCache(true) });
const info = await innertube.getBasicInfo('wRNnMQEKo7o');
@@ -15,7 +23,7 @@ console.info(`
Video ID: ${info.basic_info.id}
`);
const durationMs = info.basic_info?.duration ? info.basic_info.duration * 1000 : 0;
const durationMs = (info.basic_info?.duration ?? 0) * 1000;
const sanitizedTitle = info.basic_info.title?.replace(/[^a-z0-9]/gi, '_');
let audioOutput: WriteStream | undefined;
@@ -56,39 +64,53 @@ const serverAbrStream = new GoogleVideo.ServerAbrStream({
durationMs
});
serverAbrStream.on('data', (data) => {
let progressText = '';
let downloadedBytesAudio = 0;
let downloadedBytesVideo = 0;
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);
serverAbrStream.on('data', (streamData) => {
for (const formatData of streamData.initializedFormats) {
const isVideo = formatData.mimeType?.includes('video');
const mediaFormat = info.streaming_data?.adaptive_formats.find((f) => f.itag === formatData.formatId.itag);
const formatKey = formatData.formatKey;
const data = concatenateChunks(initializedFormat.mediaChunks);
let bar = formatProgressBars.get(formatKey);
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);
if (!bar) {
bar = progressBars.create(100, 0, undefined, { format: `${isVideo ? 'video' : 'audio'} (${formatData.formatId.itag}) [{bar}] {percentage}% | ETA: {eta}s` });
formatProgressBars.set(formatKey, bar);
}
const fmtIdentifier = `${initializedFormat.formatId.itag}_${initializedFormat.mimeType?.split(';')[0]}`;
const percentage = Math.round((initializedFormat.sequenceList.at(-1)?.startDataRange ?? 0) / (mediaFormat?.content_length ?? 0) * 100);
const mediaChunks = formatData.mediaChunks;
if (percentage)
progressText += `${fmtIdentifier}: ${percentage}% | `;
if (isVideo && mediaChunks.length) {
if (!videoOutput) {
videoOutputFilename = `${sanitizedTitle}.${formatData.formatId.itag}.webm`;
videoOutput = createWriteStream(videoOutputFilename);
}
for (const chunk of mediaChunks) {
downloadedBytesVideo += chunk.length;
videoOutput.write(chunk);
}
} else if (mediaChunks.length) {
if (!audioOutput) {
audioOutputFilename = `${sanitizedTitle}.${formatData.formatId.itag}.webm`;
audioOutput = createWriteStream(audioOutputFilename);
}
for (const chunk of mediaChunks) {
downloadedBytesAudio += chunk.length;
audioOutput.write(chunk);
}
}
const contentLength = mediaFormat?.content_length ?? 0;
const downloadedBytes = isVideo ? downloadedBytesVideo : downloadedBytesAudio;
if (contentLength > 0) {
const percentage = (downloadedBytes / contentLength) * 100;
bar.update(percentage);
}
}
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(progressText);
});
serverAbrStream.on('error', (error) => {
@@ -121,11 +143,6 @@ await new Promise<void>((resolve, reject) => {
.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) => {

View File

@@ -1,10 +1,18 @@
import cliProgress from 'cli-progress';
import type { WriteStream } from 'node:fs';
import { createWriteStream } from 'node:fs';
import { Innertube, UniversalCache } from 'youtubei.js';
import GoogleVideo, { concatenateChunks, type Format, MediaType } from '../../dist/src/index.js';
import GoogleVideo, { type Format, MediaType } from '../../dist/src/index.js';
const progressBars = new cliProgress.MultiBar({
clearOnComplete: false,
hideCursor: true
}, cliProgress.Presets.rect);
const formatProgressBars = new Map<string, cliProgress.SingleBar>();
const innertube = await Innertube.create({ cache: new UniversalCache(true) });
const info = await innertube.getBasicInfo('el68RBQBlCs');
const info = await innertube.getBasicInfo('mzqO7oKTJKI');
console.info(`
Title: ${info.basic_info.title}
@@ -14,14 +22,14 @@ console.info(`
Video ID: ${info.basic_info.id}
`);
const durationMs = info.basic_info?.duration ? info.basic_info.duration * 1000 : 0;
const durationMs = (info.basic_info?.duration ?? 0) * 1000;
const sanitizedTitle = info.basic_info.title?.replace(/[^a-z0-9]/gi, '_');
let audioOutput: WriteStream | undefined;
let videoOutput: WriteStream | undefined;
const audioFormat = info.chooseFormat({ quality: 'best', format: 'webm', type: 'audio' });
const videoFormat = info.chooseFormat({ quality: '1080p', format: 'webm', type: 'video' });
const videoFormat = info.chooseFormat({ quality: '720p', format: 'webm', type: 'video' });
const selectedAudioFormat: Format = {
itag: audioFormat.itag,
@@ -47,10 +55,19 @@ if (!serverAbrStreamingUrl)
throw new Error('serverAbrStreamingUrl not found');
const determineFileExtension = (mimeType: string) => {
if (mimeType.includes('video'))
if (mimeType.includes('video')) {
return mimeType.includes('webm') ? 'webm' : 'mp4';
else if (mimeType.includes('audio'))
} else if (mimeType.includes('audio')) {
return mimeType.includes('webm') ? 'webm' : 'm4a';
}
return 'bin';
};
const getOutputStream = (isVideo: boolean, mimeType: string, formatId?: number) => {
const type = isVideo ? 'video' : 'audio';
const extension = determineFileExtension(mimeType);
const stream = createWriteStream(`${sanitizedTitle}.${formatId}.${type}.${extension}`);
return stream;
};
const serverAbrStream = new GoogleVideo.ServerAbrStream({
@@ -60,46 +77,57 @@ const serverAbrStream = new GoogleVideo.ServerAbrStream({
durationMs
});
serverAbrStream.on('data', (data) => {
let progressText = '';
let downloadedBytesAudio = 0;
let downloadedBytesVideo = 0;
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);
serverAbrStream.on('data', (streamData) => {
for (const formatData of streamData.initializedFormats) {
const isVideo = formatData.mimeType?.includes('video');
const mediaFormat = info.streaming_data?.adaptive_formats.find((f) => f.itag === formatData.formatId.itag);
const formatKey = formatData.formatKey;
const data = concatenateChunks(initializedFormat.mediaChunks);
let bar = formatProgressBars.get(formatKey);
if (isVideo && data.length) {
if (!videoOutput)
videoOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.formatId.itag}.${determineFileExtension(initializedFormat.mimeType || '')}`);
videoOutput.write(data);
} else if (data.length) {
if (!audioOutput)
audioOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.formatId.itag}.${determineFileExtension(initializedFormat.mimeType || '')}`);
audioOutput.write(data);
if (!bar) {
bar = progressBars.create(100, 0, undefined, { format: `${isVideo ? 'video' : 'audio'} (${formatData.formatId.itag}) [{bar}] {percentage}% | ETA: {eta}s` });
formatProgressBars.set(formatKey, bar);
}
const fmtIdentifier = `${initializedFormat.formatId.itag}_${initializedFormat.mimeType?.split(';')[0]}`;
const mediaChunks = formatData.mediaChunks;
const percentage = Math.round((initializedFormat.sequenceList.at(-1)?.startDataRange ?? 0) / (mediaFormat?.content_length ?? 0) * 100);
if (isVideo && mediaChunks.length) {
if (!videoOutput)
videoOutput = getOutputStream(true, formatData.mimeType || '', formatData.formatId?.itag);
for (const chunk of mediaChunks) {
downloadedBytesVideo += chunk.length;
videoOutput.write(chunk);
}
} else if (mediaChunks.length) {
if (!audioOutput)
audioOutput = getOutputStream(false, formatData.mimeType || '', formatData.formatId?.itag);
for (const chunk of mediaChunks) {
downloadedBytesAudio += chunk.length;
audioOutput.write(chunk);
}
}
if (percentage)
progressText += `${fmtIdentifier}: ${percentage}% | `;
const contentLength = mediaFormat?.content_length ?? 0;
const downloadedBytes = isVideo ? downloadedBytesVideo : downloadedBytesAudio;
if (contentLength > 0) {
const percentage = (downloadedBytes / contentLength) * 100;
bar.update(percentage);
}
}
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(progressText);
});
serverAbrStream.on('error', (error) => {
progressBars.stop();
console.error(error);
});
serverAbrStream.on('end', () => {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write('Done!');
progressBars.stop();
if (audioOutput)
audioOutput.end();

View File

@@ -9,11 +9,13 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cli-progress": "^3.12.0",
"fluent-ffmpeg": "^2.1.3",
"shaka-player": "^4.11.2",
"youtubei.js": "github:LuanRT/YouTube.js#refactor/update-protos"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",
"@types/fluent-ffmpeg": "^2.1.26",
"typescript": "^5.6.2"
}
@@ -31,6 +33,15 @@
"node": ">=14"
}
},
"node_modules/@types/cli-progress": {
"version": "3.11.6",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
"integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/fluent-ffmpeg": {
"version": "2.1.26",
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.26.tgz",
@@ -60,16 +71,40 @@
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"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/cli-progress": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
"dependencies": {
"string-width": "^4.2.3"
},
"engines": {
"node": ">=4"
}
},
"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/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/fluent-ffmpeg": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
@@ -82,6 +117,14 @@
"node": ">=18"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -109,6 +152,30 @@
"node": ">=14"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",

View File

@@ -12,11 +12,13 @@
"author": "",
"license": "ISC",
"dependencies": {
"cli-progress": "^3.12.0",
"fluent-ffmpeg": "^2.1.3",
"shaka-player": "^4.11.2",
"youtubei.js": "github:LuanRT/YouTube.js#refactor/update-protos"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",
"@types/fluent-ffmpeg": "^2.1.26",
"typescript": "^5.6.2"
}

View File

@@ -39,15 +39,15 @@ export class ServerAbrStream extends EventEmitterLike {
this.totalDurationMs = args.durationMs;
}
public on(event: 'end', listener: (data: ServerAbrResponse) => void): void;
public on(event: 'data', listener: (data: ServerAbrResponse) => void): void;
public on(event: 'end', listener: (streamData: ServerAbrResponse) => void): void;
public on(event: 'data', listener: (streamData: ServerAbrResponse) => void): void;
public on(event: 'error', listener: (error: Error) => void): void;
public on(event: string, listener: (...data: any[]) => void): void {
super.on(event, listener);
}
public once(event: 'end', listener: (data: ServerAbrResponse) => void): void;
public once(event: 'data', listener: (data: ServerAbrResponse) => void): void;
public once(event: 'end', listener: (streamData: ServerAbrResponse) => void): void;
public once(event: 'data', listener: (streamData: ServerAbrResponse) => void): void;
public once(event: 'error', listener: (error: Error) => void): void;
public once(event: string, listener: (...args: any[]) => void): void {
super.once(event, listener);
@@ -88,8 +88,8 @@ export class ServerAbrStream extends EventEmitterLike {
if (typeof mediaInfo.startTimeMs !== 'number')
throw new Error('Invalid media start time');
while (mediaInfo.startTimeMs < this.totalDurationMs) {
try {
try {
while (mediaInfo.startTimeMs < this.totalDurationMs) {
const data = await this.fetchMedia({ mediaInfo, audioFormatIds, videoFormatIds });
this.emit('data', data);
@@ -115,10 +115,9 @@ export class ServerAbrStream extends EventEmitterLike {
}
mediaInfo.startTimeMs += mainFormat.sequenceList.reduce((acc, seq) => acc + (seq.durationMs || 0), 0);
} catch (error) {
this.emit('error', error);
break;
}
} catch (error) {
this.emit('error', error);
}
}
@@ -283,6 +282,7 @@ export class ServerAbrStream extends EventEmitterLike {
if (!this.formatsByKey.has(formatKey)) {
this.initializedFormats.push({
formatId: formatInitializationMetadata.formatId,
formatKey: getFormatKey(formatInitializationMetadata.formatId),
durationMs: formatInitializationMetadata.durationMs,
mimeType: formatInitializationMetadata.mimeType,
sequenceCount: formatInitializationMetadata.field4,

View File

@@ -41,6 +41,7 @@ export type Sequence = {
export type InitializedFormat = {
formatId: FormatId;
formatKey: string;
durationMs?: number;
mimeType?: string;
sequenceCount?: number;