UNPKG

newpipe-extractor-js

Version:
357 lines 13.2 kB
"use strict"; // DASH Manifest Parser for YouTube // Handles parsing of DASH MPD (Media Presentation Description) manifests Object.defineProperty(exports, "__esModule", { value: true }); exports.DashManifestParser = void 0; const types_1 = require("../../types"); // Import xmldom DOMParser for Node.js environment let Parser; try { // Try to import xmldom DOMParser const xmldom = require('xmldom'); Parser = xmldom.DOMParser; } catch (e) { // Fallback to global DOMParser if available (browser environment) Parser = typeof globalThis !== 'undefined' && globalThis.DOMParser || null; } class DashManifestParser { constructor() { } /** * Parse DASH MPD manifest from XML string */ async parseDashManifest(mpdContent) { try { if (!Parser) { throw new Error('XML parser not available'); } const parser = new Parser(); const doc = parser.parseFromString(mpdContent, 'text/xml'); if (!doc || doc.getElementsByTagName('parsererror').length > 0) { throw new Error('Failed to parse DASH manifest XML'); } const mpdElement = doc.documentElement; if (mpdElement.tagName !== 'MPD') { throw new Error('Invalid DASH manifest: root element is not MPD'); } const manifest = { audioStreams: [], videoStreams: [], videoOnlyStreams: [], isLive: this.isLiveManifest(mpdElement) }; // Parse duration const mediaPresentationDuration = mpdElement.getAttribute('mediaPresentationDuration'); if (mediaPresentationDuration) { manifest.duration = this.parseDuration(mediaPresentationDuration); } // Parse periods const periods = mpdElement.getElementsByTagName('Period'); for (let i = 0; i < periods.length; i++) { const period = periods[i]; this.parsePeriod(period, manifest); } return manifest; } catch (error) { throw new Error(`DASH manifest parsing failed: ${error.message}`); } } /** * Parse a Period element */ parsePeriod(period, manifest) { const adaptationSets = period.getElementsByTagName('AdaptationSet'); for (let i = 0; i < adaptationSets.length; i++) { const adaptationSet = adaptationSets[i]; this.parseAdaptationSet(adaptationSet, manifest); } } /** * Parse an AdaptationSet element */ parseAdaptationSet(adaptationSet, manifest) { const mimeType = adaptationSet.getAttribute('mimeType') || ''; const contentType = this.getContentType(mimeType); if (!contentType) { return; // Skip unsupported content types } const dashAdaptationSet = { id: adaptationSet.getAttribute('id') || undefined, mimeType, codecs: adaptationSet.getAttribute('codecs') || undefined, contentType, lang: adaptationSet.getAttribute('lang') || undefined, representations: [] }; // Parse segment template at adaptation set level const segmentTemplates = adaptationSet.getElementsByTagName('SegmentTemplate'); if (segmentTemplates.length > 0) { dashAdaptationSet.segmentTemplate = this.parseSegmentTemplate(segmentTemplates[0]); } // Parse representations const representations = adaptationSet.getElementsByTagName('Representation'); for (let i = 0; i < representations.length; i++) { const representation = representations[i]; const dashRep = this.parseRepresentation(representation, dashAdaptationSet); if (dashRep) { dashAdaptationSet.representations.push(dashRep); } } // Convert to stream objects this.convertAdaptationSetToStreams(dashAdaptationSet, manifest); } /** * Parse a Representation element */ parseRepresentation(representation, adaptationSet) { const id = representation.getAttribute('id'); const bandwidth = parseInt(representation.getAttribute('bandwidth') || '0'); if (!id || bandwidth === 0) { return null; } const dashRep = { id, bandwidth, mimeType: representation.getAttribute('mimeType') || adaptationSet.mimeType, codecs: representation.getAttribute('codecs') || adaptationSet.codecs, width: this.parseIntAttribute(representation, 'width'), height: this.parseIntAttribute(representation, 'height'), frameRate: representation.getAttribute('frameRate') || undefined, audioSamplingRate: this.parseIntAttribute(representation, 'audioSamplingRate') }; // Parse BaseURL const baseUrls = representation.getElementsByTagName('BaseURL'); if (baseUrls.length > 0) { dashRep.baseUrl = baseUrls[0].textContent || undefined; } // Parse SegmentBase for range information const segmentBases = representation.getElementsByTagName('SegmentBase'); if (segmentBases.length > 0) { const segmentBase = segmentBases[0]; // Parse initialization range const initialization = segmentBase.getElementsByTagName('Initialization'); if (initialization.length > 0) { const range = initialization[0].getAttribute('range'); if (range) { dashRep.initRange = this.parseRange(range); } } // Parse index range const indexRange = segmentBase.getAttribute('indexRange'); if (indexRange) { dashRep.indexRange = this.parseRange(indexRange); } } // Parse SegmentTemplate const segmentTemplates = representation.getElementsByTagName('SegmentTemplate'); if (segmentTemplates.length > 0) { dashRep.segmentTemplate = this.parseSegmentTemplate(segmentTemplates[0]); } else if (adaptationSet.segmentTemplate) { dashRep.segmentTemplate = adaptationSet.segmentTemplate; } return dashRep; } /** * Parse SegmentTemplate element */ parseSegmentTemplate(segmentTemplate) { return { initialization: segmentTemplate.getAttribute('initialization') || undefined, media: segmentTemplate.getAttribute('media') || undefined, startNumber: this.parseIntAttribute(segmentTemplate, 'startNumber'), timescale: this.parseIntAttribute(segmentTemplate, 'timescale'), duration: this.parseIntAttribute(segmentTemplate, 'duration') }; } /** * Convert DASH adaptation set to stream objects */ convertAdaptationSetToStreams(adaptationSet, manifest) { for (const representation of adaptationSet.representations) { const stream = this.createStreamFromRepresentation(representation, adaptationSet); if (!stream) continue; if (adaptationSet.contentType === 'audio') { manifest.audioStreams.push(stream); } else if (adaptationSet.contentType === 'video') { if (representation.width && representation.height) { if (this.isVideoOnly(adaptationSet.mimeType)) { manifest.videoOnlyStreams.push(stream); } else { manifest.videoStreams.push(stream); } } } } } /** * Create stream object from DASH representation */ createStreamFromRepresentation(representation, adaptationSet) { const format = this.getMediaFormat(representation.mimeType); if (!format) return null; const baseStream = { url: representation.baseUrl, bitrate: Math.round(representation.bandwidth / 1000), // Convert to kbps format, codec: representation.codecs, initRange: representation.initRange, indexRange: representation.indexRange, itag: this.extractItag(representation.id) }; if (adaptationSet.contentType === 'audio') { const audioStream = { ...baseStream, samplingRate: representation.audioSamplingRate, quality: this.getAudioQuality(representation.bandwidth) }; return audioStream; } else { const videoStream = { ...baseStream, resolution: representation.width && representation.height ? `${representation.width}x${representation.height}` : undefined, width: representation.width, height: representation.height, fps: representation.frameRate ? parseInt(representation.frameRate) : undefined, isVideoOnly: this.isVideoOnly(representation.mimeType), quality: this.getVideoQuality(representation.width, representation.height) }; return videoStream; } } /** * Determine content type from MIME type */ getContentType(mimeType) { if (mimeType.startsWith('audio/')) { return 'audio'; } else if (mimeType.startsWith('video/')) { return 'video'; } return null; } /** * Convert MIME type to MediaFormat */ getMediaFormat(mimeType) { switch (mimeType) { case 'video/mp4': return types_1.MediaFormat.MPEG_4; case 'video/webm': return types_1.MediaFormat.WEBM; case 'audio/mp4': case 'audio/mp4a-latm': return types_1.MediaFormat.M4A; case 'audio/webm': return types_1.MediaFormat.WEBMA_OPUS; default: // Try to extract from codecs if (mimeType.includes('mp4')) { return types_1.MediaFormat.MPEG_4; } else if (mimeType.includes('webm')) { return types_1.MediaFormat.WEBM; } return null; } } /** * Check if manifest represents live content */ isLiveManifest(mpdElement) { const type = mpdElement.getAttribute('type'); return type === 'dynamic'; } /** * Check if MIME type represents video-only content */ isVideoOnly(mimeType) { // Video-only streams typically don't have audio codecs return mimeType.startsWith('video/') && !mimeType.includes('audio'); } /** * Parse range string (e.g., "0-1234") */ parseRange(rangeStr) { const parts = rangeStr.split('-'); if (parts.length === 2) { return { start: parts[0], end: parts[1] }; } return undefined; } /** * Parse duration string (ISO 8601 format: PT1H2M3S) */ parseDuration(durationStr) { const match = durationStr.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?$/); if (!match) return 0; const hours = parseInt(match[1] || '0'); const minutes = parseInt(match[2] || '0'); const seconds = parseFloat(match[3] || '0'); return hours * 3600 + minutes * 60 + seconds; } /** * Parse integer attribute with fallback */ parseIntAttribute(element, attributeName) { const value = element.getAttribute(attributeName); return value ? parseInt(value) : undefined; } /** * Extract itag from representation ID if possible */ extractItag(id) { const match = id.match(/^(\d+)$/); return match ? parseInt(match[1]) : undefined; } /** * Get audio quality description from bitrate */ getAudioQuality(bandwidth) { const kbps = bandwidth / 1000; if (kbps >= 256) return 'high'; if (kbps >= 128) return 'medium'; if (kbps >= 64) return 'low'; return 'poor'; } /** * Get video quality description from resolution */ getVideoQuality(_width, height) { if (!height) return 'unknown'; if (height >= 2160) return '4K'; if (height >= 1440) return '1440p'; if (height >= 1080) return '1080p'; if (height >= 720) return '720p'; if (height >= 480) return '480p'; if (height >= 360) return '360p'; if (height >= 240) return '240p'; return '144p'; } } exports.DashManifestParser = DashManifestParser; //# sourceMappingURL=DashManifestParser.js.map