newpipe-extractor-js
Version:
JavaScript/Node.js port of NewPipeExtractor
357 lines • 13.2 kB
JavaScript
"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