diff --git a/dasha.js b/dasha.js index bfe6580..67e642a 100644 --- a/dasha.js +++ b/dasha.js @@ -46,6 +46,29 @@ const trackDto = (data, duration) => { segments: segmentsDto(data.segments), }; if (data.attributes.RESOLUTION) result.resolution = data.attributes.RESOLUTION; + if (data.language) result.language = data.language; + if (data.contentProtection) { + result.protection = {}; + if (data.contentProtection?.mp4protection) + result.protection.common = { + id: data.contentProtection.mp4protection.attributes.schemeIdUri, + value: data.contentProtection.mp4protection.attributes.value, + keyId: data.contentProtection.mp4protection.attributes['cenc:default_KID'], + }; + if (data.contentProtection['com.microsoft.playready']) + result.protection.playready = { + id: data.contentProtection['com.microsoft.playready'].attributes.schemeIdUri, + value: data.contentProtection['com.microsoft.playready'].attributes.value, + pssh: Buffer.from(data.contentProtection['com.microsoft.playready'].pssh).toString( + 'base64' + ), + }; + if (data.contentProtection['com.widevine.alpha']) + result.protection.widevine = { + id: data.contentProtection['com.widevine.alpha'].attributes.schemeIdUri, + pssh: Buffer.from(data.contentProtection['com.widevine.alpha'].pssh).toString('base64'), + }; + } return result; }; @@ -53,7 +76,7 @@ const audioDto = (data) => { const result = []; if (!data) return result; for (const [key, value] of Object.entries(data)) { - result.push(...value.playlists); + result.push(...value.playlists.map((item) => ({ ...item, language: value.language }))); } return result; }; @@ -62,7 +85,7 @@ const subsDto = (data) => { const result = []; if (!data) return result; for (const [key, value] of Object.entries(data)) { - result.push(...value.playlists); + result.push(...value.playlists.map((item) => ({ ...item, language: value.language }))); } return result; }; @@ -77,7 +100,6 @@ const parseMpd = (manifestString, manifestUri) => { contentSteering: parsedManifestInfo.contentSteeringInfo, eventStream: parsedManifestInfo.eventStream, }); - manifest.allPlaylists = playlists; const toTrackWithSize = (data) => trackDto(data, manifest.duration); const videoPlaylists = manifest.playlists; @@ -91,7 +113,6 @@ const parseMpd = (manifestString, manifestUri) => { subtitles: subtitlePlaylists.map(toTrackWithSize), }, }; - return mpd; }; @@ -103,9 +124,9 @@ const parseM3U8 = (manifestString) => { return manifest; }; -const parse = (text, url, eventHandler) => { - if (text.includes('MPD')) return parseMpd(text, url, eventHandler); - else if (text.includes('#EXTM3U')) return parseM3U8(text); +const parse = (body, url) => { + if (body.includes(' { module.exports = { parse, - parseMpd, - parseM3U8, getPssh, getVideoTrack, getAudioTracks, diff --git a/package.json b/package.json index c14f322..736d5d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dasha", - "version": "3.0.0.alpha.1", + "version": "3.0.0.alpha.2", "author": "Vitaly Gashkov ", "description": "Parser for MPEG-DASH & HLS manifests", "license": "AGPL-3.0", diff --git a/test/dasha.test.js b/test/dasha.test.js index 760723a..2d9999b 100644 --- a/test/dasha.test.js +++ b/test/dasha.test.js @@ -1,19 +1,16 @@ const { test } = require('node:test'); const { strictEqual } = require('node:assert'); -const { getQualityLabel, parseMpd } = require('../dasha'); const { readFileSync } = require('node:fs'); -const { parse } = require('../lib/xml-parser'); +const { getQualityLabel, parse } = require('../dasha'); test('getQualityLabel', () => { strictEqual(getQualityLabel({ width: 1920, height: 1080 }), '1080p'); }); -const iviMpdBody = readFileSync('./test/ivi.mpd', 'utf8'); -const iviMpdUrl = - 'https://region.dfs.ivi.ru/jW1IJMiotdNBiHD4lSg9lP6hagbTPkSWIEwaCAAZ06jiyKhGIU4g50599/voddash-abrshq,4000/k5SE_TzrVR8MZwRunaX1ZQ,1711416086/storage4/contents/3/c/fb597d9676983b3c79d547dc082f70.ks/3438f09cad005bf5eae8b2fea20500c7.mpd'; +const kionMpdBody = readFileSync('./test/kion.mpd', 'utf8'); +const kionMpdUrl = + 'https://htv-mag2-moscow2.mts.ru/htv-rrs.mts.ru/88888888/16/20230707/268697239/268697239.mpd'; -test('parseMpd', () => { - const manifest = parseMpd(iviMpdBody); - // console.log(manifest); - // strictEqual(getQualityLabel({ width: 1920, height: 1080 }), '1080p'); +test('DASH parsing', () => { + const manifest = parse(kionMpdBody, kionMpdUrl); }); diff --git a/test/ivi.mpd b/test/ivi.mpd deleted file mode 100644 index 7e9a20e..0000000 --- a/test/ivi.mpd +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/kion.mpd b/test/kion.mpd new file mode 100644 index 0000000..9797b88 --- /dev/null +++ b/test/kion.mpd @@ -0,0 +1,33 @@ + + + + + + + + + AAADPnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAx4eAwAAAQABABQDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBuADgAZgB4AFEAVwB2AGoAUgA5AEQAMQBRAHAAcwBQAEsAeAB1AGEAcgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AHgAMQBRAFUATgBBAFQATgBBAGIANAA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAGgAdAB2AC0AcAByAGwAcwAuAG0AdABzAC4AcgB1AC8AUABsAGEAeQBSAGUAYQBkAHkALwByAGkAZwBoAHQAcwBtAGEAbgBhAGcAZQByAC4AYQBzAG0AeAA8AC8ATABBAF8AVQBSAEwAPgA8AEwAVQBJAF8AVQBSAEwAPgBoAHQAdABwAHMAOgAvAC8AaAB0AHYALQBwAHIAbABzAC4AbQB0AHMALgByAHUALwBQAGwAYQB5AFIAZQBhAGQAeQAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAFUASQBfAFUAUgBMAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= + + + AAAAXXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAD0IARIQQfHHn+Nr0Ef1QpsPKxuarRoNdmVyaW1hdHJpeG10cyIRcj03NTY4MDYwNDImcz05NTMqBVNEX0hE + + + + + + + + + + + + + AAADPnBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAx4eAwAAAQABABQDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBuADgAZgB4AFEAVwB2AGoAUgA5AEQAMQBRAHAAcwBQAEsAeAB1AGEAcgBRAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AHgAMQBRAFUATgBBAFQATgBBAGIANAA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAGgAdAB2AC0AcAByAGwAcwAuAG0AdABzAC4AcgB1AC8AUABsAGEAeQBSAGUAYQBkAHkALwByAGkAZwBoAHQAcwBtAGEAbgBhAGcAZQByAC4AYQBzAG0AeAA8AC8ATABBAF8AVQBSAEwAPgA8AEwAVQBJAF8AVQBSAEwAPgBoAHQAdABwAHMAOgAvAC8AaAB0AHYALQBwAHIAbABzAC4AbQB0AHMALgByAHUALwBQAGwAYQB5AFIAZQBhAGQAeQAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAFUASQBfAFUAUgBMAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA= + + + AAAAXXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAD0IARIQQfHHn+Nr0Ef1QpsPKxuarRoNdmVyaW1hdHJpeG10cyIRcj03NTY4MDYwNDImcz05NTMqBVNEX0hE + + + + + diff --git a/types/dasha.d.ts b/types/dasha.d.ts index ae554c4..52ac17c 100644 --- a/types/dasha.d.ts +++ b/types/dasha.d.ts @@ -1,121 +1,54 @@ -export * from './manifest'; -export * from './constants'; - -export type ParseEventHandler = (event: { type: string; message: string }) => void; +export function parse(body: string, url: string): Manifest | null; export interface Manifest { - uri: string; - resolvedUri: string; duration: number; - targetDuration: number; - discontinuityStarts: number[]; - timelineStarts: { start: number; timeline: number }[]; - timeline: number; - mediaSequence: number; - discontinuitySequence: number; - allowCache: boolean; - endList: boolean; - segments: Segment[]; - playlists: Playlist[]; - mediaGroups: MediaGroups; - - allPlaylists: Playlist[]; - - // TODO: Check types below - contentSteering?: { - defaultServiceLocation: string; - proxyServerURL: string; - queryBeforeStart: boolean; - serverURL: string; + tracks: { + videos: VideoTrack[]; + audios: AudioTrack[]; + subtitles: SubtitleTrack[]; }; - playlistType?: string; - dateTimeString?: string; - dateTimeObject?: Date; - totalDuration?: number; } -export interface Segment { - number: number; - uri: string; - resolvedUri: string; - timeline: number; - duration: number; - presentationTime: number; - map: { - uri: string; - resolvedUri: string; - // TODO: Check types below - byterange?: { - length: number; - offset: number; - }; - }; - - // TODO: Check types below - byterange?: { - length: number; - offset: number; +export interface Track { + id: string; + codecs: string; + bandwidth: { + bps: number; + kbps: number; + mbps: number; + gbps: number; + toString: () => string; }; - discontinuity?: number; - key?: { - method: string; - uri: string; - iv: string; + size: { + b: number; + kb: number; + mb: number; + gb: number; + toString: () => string; }; - 'cue-out'?: string; - 'cue-out-cont'?: string; - 'cue-in'?: string; + segments: Segment[]; + protection?: TrackProtection; } -export interface Playlist extends Omit { - attributes: Attributes; - contentProtection?: { - mp4protection?: { - attributes: { schemeIdUri: string; value: string; 'cenc:default_KID': string }; - }; - 'com.microsoft.playready'?: { - attributes: { schemeIdUri: string; value: string }; - pssh: Uint8Array; - }; - 'com.widevine.alpha'?: { - attributes: { schemeIdUri: string }; - pssh: Uint8Array; - }; - }; +export interface Segment { + url: string; + init?: boolean; } -export interface MediaGroups { - AUDIO: { - audio?: { [groupId: string]: MediaGroup }; - }; - SUBTITLES: { - subs?: { [groupId: string]: MediaGroup }; - }; - VIDEO: {}; - 'CLOSED-CAPTIONS': {}; +export interface TrackProtection { + common?: { id: string; value: string; keyId?: string }; + playready?: { id: string; pssh: string; value?: string }; + widevine?: { id: string; pssh: string }; } -export interface MediaGroup { - language: string; - default: boolean; - autoselect: boolean; - playlists: Playlist[]; - uri: string; +export interface VideoTrack extends Track { + resolution: { width: number; height: number }; +} - // TODO: Check types below - instreamId?: string; - characteristics?: string; - forced?: boolean; +export interface AudioTrack extends Track { + language: string; } -export interface Attributes { - NAME: string; - AUDIO: string; - SUBTITLES: string; - RESOLUTION: { width: number; height: number }; - CODECS: string; - BANDWIDTH: number; - 'PROGRAM-ID': number; - 'FRAME-RATE': number; - [key: string]: any; +export interface SubtitleTrack extends Track { + language: string; } diff --git a/types/mpd-parser.d.ts b/types/mpd-parser.d.ts new file mode 100644 index 0000000..c07a1c2 --- /dev/null +++ b/types/mpd-parser.d.ts @@ -0,0 +1,145 @@ +declare module 'mpd-parser' { + export function parse(manifestString: string, options?: ParseOptions): Manifest; + export function stringToMpdXml(manifestString: string): HTMLElement; + export function inheritAttributes(mpd: Node, options?: ParseOptions): InheritAttributesResult; + export function toPlaylists(representations: unknown[]): unknown[]; + + export interface ParseOptions { + manifestUri: string; + NOW?: number; + clientOffset?: number; + eventHandler?: EventHandler; + } + + export type EventHandler = (event: { type: string; message: string }) => void; + + export interface InheritAttributesResult { + locations: unknown; + contentSteeringInfo: unknown | null; + representationInfo: RepresentationInfo[]; + eventStream: unknown; + } + + export interface RepresentationInfo { + segmentInfo: SegmentInformation; + attributes: Record; + } + + interface SegmentInformation { + template: Record; + segmentTimeline: Record[]; + list: Record; + base: Record; + } + + export interface Manifest { + uri: string; + resolvedUri: string; + duration: number; + targetDuration: number; + discontinuityStarts: number[]; + timelineStarts: { start: number; timeline: number }[]; + timeline: number; + mediaSequence: number; + discontinuitySequence: number; + allowCache: boolean; + endList: boolean; + segments: Segment[]; + playlists: Playlist[]; + mediaGroups: MediaGroups; + + contentSteering?: { + defaultServiceLocation: string; + proxyServerURL: string; + queryBeforeStart: boolean; + serverURL: string; + }; + playlistType?: string; + dateTimeString?: string; + dateTimeObject?: Date; + totalDuration?: number; + } + + export interface Segment { + number: number; + uri: string; + resolvedUri: string; + timeline: number; + duration: number; + presentationTime: number; + map: { + uri: string; + resolvedUri: string; + byterange?: { + length: number; + offset: number; + }; + }; + + byterange?: { + length: number; + offset: number; + }; + discontinuity?: number; + key?: { + method: string; + uri: string; + iv: string; + }; + 'cue-out'?: string; + 'cue-out-cont'?: string; + 'cue-in'?: string; + } + + export interface Playlist extends Omit { + attributes: Attributes; + contentProtection?: { + mp4protection?: { + attributes: { schemeIdUri: string; value: string; 'cenc:default_KID': string }; + }; + 'com.microsoft.playready'?: { + attributes: { schemeIdUri: string; value: string }; + pssh: Uint8Array; + }; + 'com.widevine.alpha'?: { + attributes: { schemeIdUri: string }; + pssh: Uint8Array; + }; + }; + } + + export interface MediaGroups { + AUDIO: { + audio?: { [groupId: string]: MediaGroup }; + }; + SUBTITLES: { + subs?: { [groupId: string]: MediaGroup }; + }; + VIDEO: {}; + 'CLOSED-CAPTIONS': {}; + } + + export interface MediaGroup { + language: string; + default: boolean; + autoselect: boolean; + playlists: Playlist[]; + uri: string; + + instreamId?: string; + characteristics?: string; + forced?: boolean; + } + + export interface Attributes { + NAME: string; + BANDWIDTH: number; + CODECS: string; + 'PROGRAM-ID': number; + RESOLUTION?: { width: number; height: number }; + 'FRAME-RATE'?: number; + AUDIO?: string; + SUBTITLES?: string; + [key: string]: any; + } +}