Created
December 31, 2024 16:31
-
-
Save erdesigns-eu/1811c9a4038b28cd0d39b6207c329384 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fetch from 'node-fetch'; | |
/** | |
* Youtube Resolver Parameters | |
* @interface YoutubeResolverParams | |
* @property {string} url - Youtube URL (https://www.youtube.com/watch?v=VIDEO_ID) | |
* @property {string} videoId - Youtube Video ID | |
*/ | |
interface YoutubeResolverParams { | |
url?: string; | |
videoId?: string; | |
} | |
/** | |
* Youtube HLS Subtitle interface. | |
* @interface Subtitle | |
* @property {string} url - URL to the subtitle stream | |
* @property {string} language - Display name of the subtitle language | |
* @property {string} languageCode - ISO language code | |
*/ | |
interface YoutubeHlsSubtitle { | |
url: string; | |
language: string; | |
languageCode: string; | |
} | |
/** | |
* Youtube Video Stream Quality enum. | |
* @enum {string} - Video stream quality | |
*/ | |
enum YoutubeVideoStreamQuality { | |
'2160p' = '2160p', | |
'1440p' = '1440p', | |
'1080p' = '1080p', | |
'720p' = '720p', | |
'480p' = '480p', | |
'360p' = '360p', | |
'240p' = '240p', | |
'144p' = '144p', | |
} | |
/** | |
* Youtube Audio Stream Quality enum. | |
* @enum {string} - Audio stream quality | |
*/ | |
enum YoutubeAudioStreamQuality { | |
'high' = 'high', | |
'medium' = 'medium', | |
'low' = 'low', | |
} | |
/** | |
* Youtube stream interface. | |
* @interface YoutubeStream | |
* @property {string} url - URL to the stream | |
* @property {number} width - Width of the video resolution | |
* @property {number} height - Height of the video resolution | |
* @property {number} fps - Frames per second | |
* @property {number} bitrate - Bitrate of the stream | |
* @property {number} channels - Number of audio channels | |
* @property {number} sampleRate - Audio sample rate | |
* @property {string} mimeType - MIME type of the stream | |
* @property {string[]} codecs - Array of codec strings used in the stream | |
* @property {YoutubeVideoStreamQuality} videoQuality - Video stream quality | |
* @property {YoutubeAudioStreamQuality} audioQuality - Audio stream quality | |
*/ | |
interface YoutubeStream { | |
url: string; | |
width?: number; | |
height?: number; | |
fps?: number; | |
bitrate?: number; | |
channels?: number; | |
sampleRate?: number; | |
mimeType?: string; | |
codecs?: string[]; | |
videoQuality?: YoutubeVideoStreamQuality; | |
audioQuality?: YoutubeAudioStreamQuality; | |
} | |
/** | |
* Youtube HLS stream interface. | |
* @interface YoutubeHlsStream | |
* @property {string} video - URL to the video stream | |
* @property {string} audio - URL to the associated audio stream | |
* @property {Subtitle[]} subtitles - Array of associated subtitle streams | |
* @property {number} width - Width of the video resolution | |
* @property {number} height - Height of the video resolution | |
* @property {number} fps - Frames per second | |
* @property {string[]} codecs - Array of codec strings used in the stream | |
*/ | |
interface YoutubeHlsStream { | |
video: string; | |
audio: string; | |
subtitles: YoutubeHlsSubtitle[]; | |
width: number; | |
height: number; | |
fps: number; | |
codecs: string[]; | |
videoQuality?: YoutubeVideoStreamQuality; | |
} | |
/** | |
* Youtube stream type. | |
* @type {YoutubeStreamType} | |
*/ | |
type YoutubeStreamType = YoutubeStream | YoutubeHlsStream; | |
/** | |
* Filter stream options. | |
* @type {FiltertreamOptions} - Filter stream options | |
* @property {boolean} hasAudio - Filter streams with audio | |
* @property {boolean} hasVideo - Filter streams with video | |
* @property {YoutubeVideoStreamQuality} videoQuality - Filter streams by video quality | |
* @property {YoutubeAudioStreamQuality} audioQuality - Filter streams by audio quality | |
* @property {boolean} isHLS - Filter HLS streams | |
*/ | |
type FiltertreamOptions = { | |
hasAudio?: boolean; | |
hasVideo?: boolean; | |
videoQuality?: YoutubeVideoStreamQuality; | |
audioQuality?: YoutubeAudioStreamQuality; | |
isHLS?: boolean; | |
} | |
/** | |
* Parses Youtube M3U8 files to extract audio, video, and subtitle streams. | |
* @class YoutubeM3U8Parser | |
*/ | |
class YoutubeM3U8Parser { | |
private audioMap: Map<string, string> = new Map(); | |
private subtitlesMap: Map<string, YoutubeHlsSubtitle[]> = new Map(); | |
private streams: YoutubeHlsStream[] = []; | |
/** | |
* Parses the M3U8 content and extracts streams with associated audio and subtitles. | |
* @param content - The M3U8 file content as a string. | |
* @returns An array of streams, each containing video and its associated audio and subtitles. | |
*/ | |
public parse(content: string): YoutubeHlsStream[] { | |
// Split the content into lines | |
const lines = content.split(/\r?\n/); | |
// Initialize the line index | |
let i = 0; | |
// Loop through all lines in the content | |
while (i < lines.length) { | |
// Trim the line to remove whitespace | |
const line = lines[i].trim(); | |
// Process Media tags (audio and subtitles) | |
if (line.startsWith('#EXT-X-MEDIA')) { | |
const attributes = this.parseAttributes(line); | |
this.processMedia(attributes); | |
} | |
// Process Stream tags (video) | |
else if (line.startsWith('#EXT-X-STREAM-INF')) { | |
const attributes = this.parseAttributes(line); | |
const uriLine = lines[++i]?.trim() || ''; | |
this.processStream(attributes, uriLine); | |
} | |
// Move to the next line | |
i++; | |
} | |
// Return the list of streams | |
return this.streams; | |
} | |
/** | |
* Processes a media tag and stores audio or subtitles accordingly. | |
* @param attributes - Parsed attributes from the media tag. | |
*/ | |
private processMedia(attributes: { [key: string]: string }): void { | |
// Get the type from the attributes | |
const type = attributes['TYPE']; | |
// Get the group ID from the attributes | |
const groupId = attributes['GROUP-ID']; | |
// Get the URI from the attributes | |
const uri = attributes['URI']; | |
// Check if any of the required attributes are missing | |
if (!type || !groupId || !uri) { | |
console.warn('Media tag missing required attributes:', attributes); | |
return; | |
} | |
// If the type is AUDIO, store the audio URL | |
if (type === 'AUDIO') { | |
this.audioMap.set(groupId, uri); | |
} | |
// If the type is SUBTITLES, store the subtitle URL | |
else if (type === 'SUBTITLES') { | |
// Get the language from the attributes | |
const language = attributes['NAME'] || 'Unknown'; | |
// Get the language code from the attributes | |
const languageCode = attributes['LANGUAGE'] || ''; | |
// Create a new subtitle object with the extracted data | |
const subtitle: YoutubeHlsSubtitle = { language, languageCode, url: uri }; | |
// Check if the subtitles map already has the group ID | |
if (!this.subtitlesMap.has(groupId)) { | |
this.subtitlesMap.set(groupId, []); | |
} | |
// Add the subtitle to the list of subtitles | |
this.subtitlesMap.get(groupId)?.push(subtitle); | |
} | |
} | |
/** | |
* Processes a stream tag and associates it with the corresponding audio and subtitles. | |
* @param attributes - Parsed attributes from the stream tag. | |
* @param uri - URL to the video stream. | |
*/ | |
private processStream(attributes: { [key: string]: string }, uri: string): void { | |
// Get the audio group ID (if available) | |
const audioGroupId = attributes['AUDIO']; | |
// Get the subtitles group ID (if available) | |
const subtitlesGroupId = attributes['SUBTITLES']; | |
// Parse the resolution attribute into width and height | |
const resolution = this.parseResolution(attributes['RESOLUTION'] || '0x0'); | |
// Get the codecs used in the stream | |
const codecs = attributes['CODECS'].split(',') || []; | |
// Get the frame rate from the attributes | |
const frameRate = parseInt(attributes['FRAME-RATE'] || '0', 10); | |
// Get the audio URL from the audio group ID | |
const audioUrl = audioGroupId ? this.audioMap.get(audioGroupId) || '' : ''; | |
if (audioGroupId && !audioUrl) { | |
console.warn(`No audio stream found for GROUP-ID: ${audioGroupId}`); | |
} | |
// Get the subtitles from the subtitles group ID | |
const subtitles = subtitlesGroupId ? this.subtitlesMap.get(subtitlesGroupId) || [] : []; | |
// Create a new stream object with the extracted data | |
const stream: YoutubeHlsStream = { | |
video: uri, | |
audio: audioUrl, | |
subtitles: subtitles, | |
width: resolution.width, | |
height: resolution.height, | |
fps: frameRate, | |
codecs: codecs, | |
}; | |
// Add the stream to the list of streams | |
this.streams.push(stream); | |
} | |
/** | |
* Parses resolution string into width and height. | |
* @param resolutionStr Resolution string in the format 'widthxheight'. | |
* @returns An object containing width and height as numbers. | |
*/ | |
private parseResolution(resolutionStr: string): { width: number; height: number } { | |
// Split the resolution string into width and height | |
const [width, height] = resolutionStr.split('x').map((num) => parseInt(num, 10)); | |
// Return an object with width and height as numbers | |
return { width, height }; | |
} | |
/** | |
* Parses attributes from a tag line. | |
* @param line The tag line containing attributes. | |
* @returns An object with key-value pairs of attributes. | |
*/ | |
private parseAttributes(line: string): { [key: string]: string } { | |
// Create an empty object to store the attributes | |
const result: { [key: string]: string } = {}; | |
// Find the first colon in the line | |
const firstColon = line.indexOf(':'); | |
// If no colon is found, return the empty object | |
if (firstColon === -1) { | |
return result; | |
} | |
// Get the attributes string after the colon | |
const attrsString = line.substring(firstColon + 1); | |
// Create a regex to match key-value pairs | |
const regex = /([A-Z0-9\-]+)=("([^"]*)"|[^,]*)/g; | |
// Initialize a match variable | |
let match: RegExpExecArray | null; | |
// Loop through all matches and extract key-value pairs | |
while ((match = regex.exec(attrsString)) !== null) { | |
const key = match[1]; | |
const value = match[3] !== undefined ? match[3] : match[2]; | |
result[key] = value; | |
} | |
// Return the object with extracted attributes | |
return result; | |
} | |
} | |
/** | |
* Youtube Resolver class to resolve video information and streaming data from the Youtube API. | |
* @class YoutubeResolver | |
*/ | |
class YoutubeResolver { | |
private readonly url: string = 'https://youtubei.googleapis.com/youtubei/v1/player?prettyPrint=false'; | |
private readonly clientName: string = 'IOS'; | |
private readonly clientVersion: string = '19.42.1'; | |
private _videoId: string | null; | |
private _title: string = ''; | |
private _author: string = ''; | |
private _keywords: string[] = []; | |
private _duration: number = 0; | |
private _description: string = ''; | |
private _views: number = 0; | |
private _thumbnails: string[] = []; | |
/* | |
isCrawlable | |
isLiveContent | |
isOwnerViewing | |
isPrivate | |
isUnpluggedCorpus | |
*/ | |
private _streams: YoutubeStream[] = []; | |
private _hlsStreams: YoutubeHlsStream[] = []; | |
/** | |
* Create a new Youtube Resolver instance | |
* @param {YoutubeResolverParams} params - The Youtube Resolver Parameters | |
* @returns {Promise<YoutubeResolver>} The Youtube Resolver instance | |
*/ | |
static async create(params: YoutubeResolverParams): Promise<YoutubeResolver> { | |
// Create a new Youtube Resolver instance | |
const resolver = new YoutubeResolver(params); | |
// Resolve the video from the Youtube API | |
await resolver.resolveVideo(); | |
// Return the Youtube Resolver instance with resolved video data | |
return resolver; | |
} | |
/** | |
* Youtube Constructor | |
* @private This constructor is private so it can only be called from the static resolve method. | |
* @param {YoutubeResolverParams} params - The Youtube Resolver Parameters | |
*/ | |
private constructor(params: YoutubeResolverParams) { | |
// Make sure the required parameters are provided | |
if (!params.url && !params.videoId) { | |
throw new Error('Missing required parameter: url or videoId'); | |
} | |
// Set the video ID from the provided parameters | |
if (params.videoId) { | |
this._videoId = params.videoId; | |
} | |
else if (params.url) { | |
this._videoId = this.getVideoId(params.url); | |
} | |
else { | |
this._videoId = null; | |
} | |
// Validate the video ID | |
if (this._videoId === null) { | |
throw new Error('Invalid Youtube URL or Video ID'); | |
} | |
} | |
/** | |
* Get the Youtube Video ID from a URL | |
* @param url - The URL of the Youtube video | |
* @returns The Youtube Video ID or null if not found | |
*/ | |
private getVideoId(url: string): string | null { | |
if (/youtu\.?be/.test(url)) { | |
// Known patterns for YouTube video IDs | |
const patterns: RegExp[] = [ | |
/youtu\.be\/([^#\&\?]{11})/, // youtu.be/<id> | |
/\?v=([^#\&\?]{11})/, // ?v=<id> | |
/\&v=([^#\&\?]{11})/, // &v=<id> | |
/embed\/([^#\&\?]{11})/, // embed/<id> | |
/\/v\/([^#\&\?]{11})/ // /v/<id> | |
]; | |
// Check patterns for a match and return the ID if found | |
for (let i = 0; i < patterns.length; i++) { | |
const match = patterns[i].exec(url); | |
if (match) { | |
return match[1]; | |
} | |
} | |
// Fallback: Split URL into tokens and look for an ID | |
const tokens = url.split(/[\/\&\?=#\.\s]/g); | |
for (let token of tokens) { | |
if (/^[^#\&\?]{11}$/.test(token)) { | |
return token; | |
} | |
} | |
} | |
return null; | |
} | |
/** | |
* Extract the codecs from a MIME type string (e.g. video/mp4; codecs="avc1.42001E, mp4a.40.2") | |
* @param mimeType - The MIME type string | |
* @returns The codecs as a string or array of strings | |
*/ | |
private extractCodecs(mimeType: string): string[] { | |
const codecs: string[] = []; | |
// Regular expression to match codecs within quotes | |
const regex = /codecs\s*=\s*"([^"]+)"/i; | |
const match = mimeType.match(regex); | |
// If a match is found, split the codecs by comma and trim whitespace | |
if (match && match[1]) { | |
codecs.push(...match[1].split(',').map((codec) => codec.trim())); | |
} | |
// Return the list of codecs | |
return codecs; | |
} | |
/** | |
* Get the video quality based on the stream resolution | |
* @param stream - The video stream object | |
* @returns The video quality | |
*/ | |
private getVideoQuality(stream: any): YoutubeVideoStreamQuality { | |
if (stream.height) { | |
if (stream.height >= 2160 || stream.qualityLabel === '2160p') { | |
return YoutubeVideoStreamQuality['2160p']; | |
} | |
if (stream.height >= 1440 || stream.qualityLabel === '1440p') { | |
return YoutubeVideoStreamQuality['1440p']; | |
} | |
if (stream.height >= 1080 || stream.qualityLabel === '1080p') { | |
return YoutubeVideoStreamQuality['1080p']; | |
} | |
if (stream.height >= 720 || stream.qualityLabel === '720p') { | |
return YoutubeVideoStreamQuality['720p']; | |
} | |
if (stream.height >= 480 || stream.qualityLabel === '480p') { | |
return YoutubeVideoStreamQuality['480p']; | |
} | |
if (stream.height >= 360 || stream.qualityLabel === '360p') { | |
return YoutubeVideoStreamQuality['360p']; | |
} | |
if (stream.height >= 240 || stream.qualityLabel === '240p') { | |
return YoutubeVideoStreamQuality['240p']; | |
} | |
if (stream.height >= 144 || stream.qualityLabel === '144p') { | |
return YoutubeVideoStreamQuality['144p']; | |
} | |
} | |
return YoutubeVideoStreamQuality['144p']; | |
} | |
/** | |
* Get the audio quality based on the stream audio quality label | |
* @param stream - The audio stream object | |
* @returns The audio quality | |
*/ | |
private getAudioQuality(stream: any): YoutubeAudioStreamQuality { | |
if (stream.audioQuality) { | |
if (stream.audioQuality === 'AUDIO_QUALITY_HIGH') { | |
return YoutubeAudioStreamQuality.high; | |
} | |
if (stream.audioQuality === 'AUDIO_QUALITY_MEDIUM') { | |
return YoutubeAudioStreamQuality.medium; | |
} | |
if (stream.audioQuality === 'AUDIO_QUALITY_LOW') { | |
return YoutubeAudioStreamQuality.low; | |
} | |
} | |
return YoutubeAudioStreamQuality.low; | |
} | |
/** | |
* Sort the video streams by video quality | |
* @param streams - The list of video streams | |
* @returns The sorted list of video streams | |
*/ | |
private sortVideoStreams(streams: YoutubeStreamType[]): YoutubeStreamType[] { | |
return streams.sort((a, b) => { | |
// Sort by video quality if available | |
if (a.videoQuality && b.videoQuality) { | |
return Object.values(YoutubeVideoStreamQuality).indexOf(a.videoQuality) - Object.values(YoutubeVideoStreamQuality).indexOf(b.videoQuality); | |
} | |
// If only stream a has video quality, prioritize it | |
else if (a.videoQuality) { | |
return -1; | |
} | |
// If only stream b has video quality, prioritize it | |
else if (b.videoQuality) { | |
return 1; | |
} | |
// Keep the order unchanged if no video quality is available | |
return 0; | |
}); | |
} | |
/** | |
* Sort the audio streams by audio quality | |
* @param streams - The list of audio streams | |
* @returns The sorted list of audio streams | |
*/ | |
private sortAudioStreams(streams: YoutubeStream[]): YoutubeStream[] { | |
return streams.sort((a, b) => { | |
// Sort by audio quality if available | |
if (a.audioQuality && b.audioQuality) { | |
return Object.values(YoutubeAudioStreamQuality).indexOf(a.audioQuality) - Object.values(YoutubeAudioStreamQuality).indexOf(b.audioQuality); | |
} | |
// If only stream a has audio quality, prioritize it | |
else if (a.audioQuality) { | |
return -1; | |
} | |
// If only stream b has audio quality, prioritize it | |
else if (b.audioQuality) { | |
return 1; | |
} | |
// Keep the order unchanged if no audio quality is available | |
return 0; | |
}); | |
} | |
/** | |
* Sort the streams by video and audio quality | |
* @param streams - The list of streams | |
* @returns The sorted list of streams | |
*/ | |
private sortStreams(streams: YoutubeStreamType[]): YoutubeStreamType[] { | |
// Separate the streams into video and audio streams | |
const videoStreams = streams.filter((stream) => stream.width && stream.height); | |
const audioStreams = streams.filter((stream) => (!stream.width || !stream.height)) as YoutubeStream[]; | |
// Sort the video streams by video quality | |
const sortedVideoStreams = this.sortVideoStreams(videoStreams); | |
// Sort the audio streams by audio quality | |
const sortedAudioStreams = this.sortAudioStreams(audioStreams); | |
// Combine the sorted video and audio streams | |
return [...sortedVideoStreams, ...sortedAudioStreams]; | |
} | |
/** | |
* Resolve the video info and streaming data from the Youtube API | |
* @returns void | |
*/ | |
private async resolveVideo(): Promise<void> { | |
// Fetch the video info from the Youtube API (non-public: innerTube API) | |
const response = await fetch(this.url, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
videoId: this._videoId, | |
context: { | |
client: { | |
clientName: this.clientName, | |
clientVersion: this.clientVersion | |
} | |
} | |
}) | |
}); | |
// Validate the response | |
if (!response.ok) { | |
throw new Error(`Failed to resolve video info: ${response.statusText}`); | |
} | |
// Parse the response JSON | |
const data = await response.json() as any; | |
// Get the playability status from the response | |
const playabilityStatus = data.playabilityStatus; | |
if (!playabilityStatus || playabilityStatus.status !== 'OK') { | |
throw new Error(`Video is not playable: ${playabilityStatus.reason}`); | |
} | |
// Get the video details from the response | |
const videoDetails = data.videoDetails; | |
// Get the streaming data from the response | |
const streamingData = data.streamingData; | |
// Validate the video details | |
if (!videoDetails) { | |
throw new Error('Failed to resolve video details'); | |
} | |
// Validate the streaming data | |
if (!streamingData) { | |
throw new Error('Failed to resolve streaming data'); | |
} | |
// Set the video details | |
this._title = videoDetails.title; | |
this._author = videoDetails.author; | |
this._keywords = videoDetails.keywords; | |
this._duration = parseInt(videoDetails.lengthSeconds, 10); | |
this._description = videoDetails.shortDescription; | |
this._views = parseInt(videoDetails.viewCount, 10); | |
this._thumbnails = [ | |
`https://i.ytimg.com/vi/${this.videoId}/maxresdefault.jpg`, | |
`https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`, | |
`https://i.ytimg.com/vi/${this.videoId}/sddefault.jpg`, | |
`https://i.ytimg.com/vi_webp/${this.videoId}/maxresdefault.webp`, | |
`https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`, | |
`https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`, | |
]; | |
// If there is a hlsManifestUrl, resolve the HLS streams | |
if (streamingData.hlsManifestUrl) { | |
// Fetch the HLS manifest from the URL | |
const hlsResponse = await fetch(streamingData.hlsManifestUrl); | |
// Get the content of the HLS manifest as text | |
const hlsContent = await hlsResponse.text(); | |
// Create a new M3U8 parser instance | |
const m3u8Parser = new YoutubeM3U8Parser(); | |
// Parse the HLS content and extract the streams with audio and subtitles | |
this._hlsStreams = m3u8Parser.parse(hlsContent); | |
// Set the video quality for each HLS stream | |
this._hlsStreams.forEach((stream) => { | |
stream.videoQuality = this.getVideoQuality(stream); | |
}); | |
} | |
// Combine the formats and adaptiveFormats into a single list | |
const formats = [ | |
...streamingData.formats || [], | |
...streamingData.adaptiveFormats || [] | |
]; | |
// Validate the streaming formats | |
if (formats.length === 0) { | |
throw new Error('No streaming formats found'); | |
} | |
// Resolve the streams from the streaming data | |
formats.forEach((format: any) => { | |
// Extract the stream data from the format | |
const { url, width, height, fps, bitrate, audioChannels, audioSampleRate, mimeType } = format; | |
// Add the stream to the list of streams | |
this._streams.push({ | |
url, | |
width: width ? parseInt(width, 10) : undefined, | |
height: height ? parseInt(height, 10) : undefined, | |
fps: fps ? parseInt(fps, 10) : undefined, | |
bitrate: bitrate ? parseInt(bitrate, 10) : undefined, | |
channels: audioChannels ? parseInt(audioChannels, 10) : undefined, | |
sampleRate: audioSampleRate ? parseInt(audioSampleRate, 10) : undefined, | |
mimeType, | |
codecs: mimeType ? this.extractCodecs(mimeType) : undefined, | |
videoQuality: width && height ? this.getVideoQuality(format) : undefined, | |
audioQuality: audioSampleRate ? this.getAudioQuality(format) : undefined, | |
}) | |
}); | |
} | |
/** | |
* Filter the streams based on the provided options | |
* @param options - The options for filtering streams | |
* @returns The list of streams that match the provided options | |
*/ | |
public filterStreams(options: FiltertreamOptions): YoutubeStreamType[] { | |
// Extract the options for filtering streams | |
const { hasAudio, hasVideo, videoQuality, audioQuality, isHLS } = options; | |
// Array to store the filtered streams | |
let streams = []; | |
// If isHLS is true, we only need to consider HLS streams | |
if (isHLS) { | |
streams = [...this._hlsStreams]; | |
} | |
// Otherwise, consider all streams | |
else { | |
streams = [...this._streams]; | |
} | |
// Filter streams that have audio (non-HLS) | |
if (hasAudio && !isHLS) { | |
streams = streams.filter((stream) => (stream as YoutubeStream).audioQuality); | |
} | |
// Filter streams that have video (non-HLS) | |
if (hasVideo && !isHLS) { | |
streams = streams.filter((stream) => stream.videoQuality); | |
} | |
// Filter streams by video quality | |
if (videoQuality) { | |
streams = streams.filter((stream) => stream.videoQuality === videoQuality); | |
} | |
// Filter streams by audio quality | |
if (audioQuality) { | |
streams = streams.filter((stream) => (stream as YoutubeStream).audioQuality === audioQuality); | |
} | |
// Sort the streams by video and audio quality | |
return this.sortStreams(streams); | |
} | |
/** | |
* Get the Youtube Video ID | |
* @returns The Youtube Video ID | |
*/ | |
public get videoId(): string { | |
return this._videoId!; | |
} | |
/** | |
* Get the video title | |
* @returns The video title | |
*/ | |
public get title(): string { | |
return this._title; | |
} | |
/** | |
* Get the video author | |
* @returns The video author | |
*/ | |
public get author(): string { | |
return this._author; | |
} | |
/** | |
* Get the video keywords | |
* @returns The video keywords | |
*/ | |
public get keywords(): string[] { | |
return this._keywords; | |
} | |
/** | |
* Get the video duration in seconds | |
* @returns The video duration in seconds | |
*/ | |
public get duration(): number { | |
return this._duration; | |
} | |
/** | |
* Get the video description | |
* @returns The video description | |
*/ | |
public get description(): string { | |
return this._description; | |
} | |
/** | |
* Get the video views count | |
* @returns The video views count | |
*/ | |
public get views(): number { | |
return this._views; | |
} | |
/** | |
* Get the thumbnail URLs | |
* @returns The video thumbnails | |
*/ | |
public get thumbnails(): string[] { | |
return this._thumbnails; | |
} | |
/** | |
* Get the video streams (non-HLS) | |
* @returns The video streams | |
*/ | |
public get streams(): YoutubeStream[] { | |
return this.sortStreams(this._streams) as YoutubeStream[]; | |
} | |
/** | |
* Get the video HLS streams | |
* @returns The video HLS streams | |
*/ | |
public get hlsStreams(): YoutubeHlsStream[] { | |
return this.sortVideoStreams(this._hlsStreams) as YoutubeHlsStream[]; | |
} | |
/** | |
* Youtube Resolver static properties | |
* @property {YoutubeVideoStreamQuality} videoStreamQuality - Video stream quality enum | |
* @property {YoutubeAudioStreamQuality} audioStreamQuality - Audio stream quality enum | |
*/ | |
static videoStreamQuality = YoutubeVideoStreamQuality; | |
static audioStreamQuality = YoutubeAudioStreamQuality; | |
} | |
export default YoutubeResolver; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment