Skip to content

Commit

Permalink
Merge pull request #57 from vitalygashkov/next
Browse files Browse the repository at this point in the history
Added separate filter functions for audio languages & channels; fixed handling audio-only manifests; improved language parsing
  • Loading branch information
vitalygashkov authored Sep 1, 2024
2 parents d57b709 + 251d053 commit 16daa60
Show file tree
Hide file tree
Showing 9 changed files with 1,046 additions and 2,627 deletions.
17 changes: 15 additions & 2 deletions dasha.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@

const dash = require('./lib/dash');
const hls = require('./lib/hls');
const { filterByResolution, filterByQuality, filterByCodecs } = require('./lib/track');
const {
filterByResolution,
filterByQuality,
filterByCodecs,
filterByLanguages,
filterByChannels,
} = require('./lib/track');

const parse = (text, url, fallbackLanguage) => {
if (text.includes('<MPD')) return dash.parseManifest(text, url, fallbackLanguage);
else if (text.includes('#EXTM3U')) return hls.parseManifest(text, url);
else throw new Error('Invalid manifest');
};

module.exports = { parse, filterByResolution, filterByQuality, filterByCodecs };
module.exports = {
parse,
filterByResolution,
filterByQuality,
filterByCodecs,
filterByLanguages,
filterByChannels,
};
6 changes: 3 additions & 3 deletions lib/dash.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
createSubtitleLanguageFilter,
createVideoCodecFilter,
createAudioCodecFilter,
createAudioChannelsFilter,
} = require('./track');

const appendUtils = (element) => {
Expand Down Expand Up @@ -89,7 +90,7 @@ const parseLanguage = (representation, adaptationSet, fallbackLanguage) => {
options.push(lang);
if (id) {
const m = id.match(/\w+_(\w+)=\d+/);
if (m) options.push(m.group(1));
if (m && m[1]) options.push(m[1]);
}
}
options.push(adaptationSet.get('lang'));
Expand Down Expand Up @@ -407,8 +408,6 @@ const parseManifest = async (text, url, fallbackLanguage) => {
}

videos.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
audios.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
subtitles.sort((a, b) => b.bitrate.bps - a.bitrate.bps);

return {
duration,
Expand All @@ -422,6 +421,7 @@ const parseManifest = async (text, url, fallbackLanguage) => {
withVideoQuality: createVideoQualityFilter(videos),
withAudioCodecs: createAudioCodecFilter(audios),
withAudioLanguages: createAudioLanguageFilter(audios),
withAudioChannels: createAudioChannelsFilter(audios),
withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
},
};
Expand Down
118 changes: 91 additions & 27 deletions lib/hls.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
'use strict';

const { dirname, basename } = require('node:path');
const m3u8Parser = require('m3u8-parser');
const { parseBitrate, getQualityLabel } = require('./util');
const {
createResolutionFilter,
createVideoQualityFilter,
createAudioLanguageFilter,
createSubtitleLanguageFilter,
createVideoCodecFilter,
createAudioCodecFilter,
createAudioChannelsFilter,
} = require('./track');
const { createAudioTrack } = require('./audio');
const { createVideoTrack } = require('./video');

const parseM3u8 = (manifestString) => {
const parser = new m3u8Parser.Parser();
Expand All @@ -22,9 +28,9 @@ const fetchPlaylist = async (url) =>
.then(parseM3u8);

const parseUrl = (playlistUri, manifestUri) => {
if (playlistUri.includes('https://')) return playlistUri;
const uri = new URL(manifestUri);
return uri.origin + playlistUri;
let value = playlistUri;
if (!value.startsWith('https://')) value = new URL(value, manifestUri).toString();
return value;
};

const urlsSame = (url1, url2) => {
Expand All @@ -51,18 +57,22 @@ const parseMediaGroup = (groups, manifestUri) => {
};

const getAudioPlaylists = (m3u8, manifestUri) => {
if (!m3u8.mediaGroups) return [];
return parseMediaGroup(m3u8.mediaGroups.AUDIO, manifestUri);
};

const getSubtitlePlaylists = (m3u8, manifestUri) => {
if (!m3u8.mediaGroups) return [];
return parseMediaGroup(m3u8.mediaGroups.SUBTITLES, manifestUri);
};

const getVideoPlaylists = (m3u8, manifestUri) => {
if (!m3u8.playlists) return [];
return m3u8.playlists.map((data) => {
const bandwidth = data.attributes.BANDWIDTH;
const bandwidth = data.attributes?.BANDWIDTH;
const url = data.resolvedUri || parseUrl(data.uri, manifestUri);
const track = { bitrate: parseBitrate(bandwidth), url };
track.type = 'video';
if (data.attributes.RESOLUTION) {
track.resolution = data.attributes.RESOLUTION;
track.quality = getQualityLabel(track.resolution);
Expand All @@ -73,13 +83,20 @@ const getVideoPlaylists = (m3u8, manifestUri) => {
});
};

const segmentsDto = (data = []) => {
const mapSegment = (item) => ({
url: item.resolvedUri || item.uri,
duration: item.duration,
number: item.number,
presentationTime: item.presentationTime,
});
const segmentsDto = (data = [], track) => {
const mapSegment = (item) => {
let url = item.resolvedUri || item.uri;
if (!url.startsWith('https://')) {
const baseUrl = dirname(track.url) + '/';
url = new URL(url, baseUrl).toString();
}
return {
url,
duration: item.duration,
number: item.number,
presentationTime: item.presentationTime,
};
};
const segments = data.map(mapSegment);
if (data.length && data[0].map?.resolvedUri)
segments.unshift({
Expand All @@ -92,21 +109,25 @@ const segmentsDto = (data = []) => {
return segments;
};

const parseSegments = (playlist, track) => {
track.segments = segmentsDto(playlist.segments, track);
if (playlist.contentProtection) {
track.protection = {};
const fairplayLegacy = playlist.contentProtection['com.apple.fps.1_0'];
if (fairplayLegacy)
track.protection.fairplay = {
keyFormat: fairplayLegacy.attributes.KEYFORMAT,
uri: fairplayLegacy.attributes.URI,
method: fairplayLegacy.attributes.METHOD,
};
}
};

const fetchTrackSegments = (tracks) => {
return Promise.all(
tracks.map(async (track) => {
const playlist = await fetchPlaylist(track.url);
track.segments = segmentsDto(playlist.segments);
if (playlist.contentProtection) {
track.protection = {};
const fairplayLegacy = playlist.contentProtection['com.apple.fps.1_0'];
if (fairplayLegacy)
track.protection.fairplay = {
keyFormat: fairplayLegacy.attributes.KEYFORMAT,
uri: fairplayLegacy.attributes.URI,
method: fairplayLegacy.attributes.METHOD,
};
}
parseSegments(playlist, track);
})
);
};
Expand All @@ -117,11 +138,51 @@ const parseManifest = async (manifestString, manifestUri) => {
const audios = getAudioPlaylists(m3u8, manifestUri);
const subtitles = getSubtitlePlaylists(m3u8, manifestUri);

await Promise.all([
fetchTrackSegments(videos),
fetchTrackSegments(audios),
fetchTrackSegments(subtitles),
]);
if (!m3u8.playlists && m3u8.segments) {
// TODO: Handle audio-only manifests
const { pathname } = new URL(manifestUri);
const isAudio =
pathname.includes('.m4a') || pathname.includes('.mp3') || pathname.includes('.opus');
if (isAudio) {
const track = createAudioTrack({
id: 'audio' + basename(pathname),
label: 'audio',
type: 'audio',
codec: '',
channels: 2,
jointObjectCoding: '',
isDescriptive: false,
bitrate: NaN,
duration: NaN,
language: '',
});
parseSegments(m3u8, track);
audios.push(track);
} else {
const track = createVideoTrack({
id: 'video' + basename(pathname),
label: 'video',
type: 'video',
codec: '',
dynamicRange: '',
contentProtection: '',
bitrate: NaN,
duration: NaN,
width: NaN,
height: NaN,
fps: NaN,
language: '',
});
parseSegments(m3u8, track);
videos.push(track);
}
} else {
await Promise.all([
fetchTrackSegments(videos),
fetchTrackSegments(audios),
fetchTrackSegments(subtitles),
]);
}

const manifest = {
tracks: {
Expand All @@ -130,8 +191,11 @@ const parseManifest = async (manifestString, manifestUri) => {
audios,
subtitles,
withResolution: createResolutionFilter(videos),
withVideoCodecs: createVideoCodecFilter(videos),
withVideoQuality: createVideoQualityFilter(videos),
withAudioCodecs: createAudioCodecFilter(audios),
withAudioLanguages: createAudioLanguageFilter(audios),
withAudioChannels: createAudioChannelsFilter(audios),
withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
},
};
Expand Down
1 change: 1 addition & 0 deletions lib/subtitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const parseSubtitleCodecFromMime = (mime) => {
const target = mime.toLowerCase().trim().split('.')[0];
switch (target) {
case 'srt':
case 'x-subrip':
return SUBTITLE_CODECS.SubRip;
case 'ssa':
return SUBTITLE_CODECS.SubStationAlpha;
Expand Down
14 changes: 14 additions & 0 deletions lib/track.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ const createAudioLanguageFilter = (audios) => {
};
};

const createAudioChannelsFilter = (audios) => {
return (channels) => {
if (!channels) return audios;
const value = typeof channels === 'string' ? parseFloat(channels) : channels;
return audios.filter((track) => track.channels === value);
};
};

const createSubtitleLanguageFilter = (subtitles) => {
return (languages) => {
if (!languages.length) return subtitles;
Expand All @@ -78,6 +86,9 @@ const createSubtitleLanguageFilter = (subtitles) => {
const filterByResolution = (tracks, resolution) => createResolutionFilter(tracks)(resolution);
const filterByQuality = (tracks, quality) => createVideoQualityFilter(tracks)(quality);
const filterByCodecs = (tracks, codecs) => createCodecFilter(tracks)(codecs);
const filterByLanguages = (tracks, languages, maxTracksPerLanguage) =>
createAudioLanguageFilter(tracks)(languages, maxTracksPerLanguage);
const filterByChannels = (tracks, channels) => createAudioChannelsFilter(tracks)(channels);

module.exports = {
parseMimes,
Expand All @@ -86,8 +97,11 @@ module.exports = {
createVideoQualityFilter,
createAudioCodecFilter,
createAudioLanguageFilter,
createAudioChannelsFilter,
createSubtitleLanguageFilter,
filterByResolution,
filterByQuality,
filterByCodecs,
filterByLanguages,
filterByChannels,
};
1 change: 1 addition & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const qualities = [
{ width: 2560, height: 1440 },
{ width: 1920, height: 1080 },
{ width: 1280, height: 720 },
{ width: 1024, height: 576 },
{ width: 854, height: 480 },
{ width: 640, height: 360 },
{ width: 426, height: 240 },
Expand Down
Loading

0 comments on commit 16daa60

Please sign in to comment.