diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..3d3a116
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: '/'
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..aba1753
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,35 @@
+name: ci
+
+on:
+ push:
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+ pull_request:
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.x
+
+ - name: Install
+ run: |
+ npm install
+
+ - name: Lint
+ run: |
+ npm run lint
+
+ - name: Run tests
+ run: |
+ npm run test
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index 64e6b9f..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Release
-
-on:
- push:
- tags:
- - '*.*.*'
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: 'npm'
- check-latest: true
-
- - name: Install dependencies
- run: npm ci
-
- - name: Get name and version from package.json
- run: |
- test -n $(node -p -e "require('./package.json').name") &&
- test -n $(node -p -e "require('./package.json').version") &&
- echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV &&
- echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1
-
- - name: Release
- uses: softprops/action-gh-release@v2
- with:
- draft: true
- token: ${{ secrets.GITHUB_TOKEN }}
- tag_name: 'v${{ env.PACKAGE_VERSION }}'
- generate_release_notes: true
-
-permissions:
- contents: write
diff --git a/README.md b/README.md
index 8944fe2..160012c 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,11 @@
Library for parsing MPEG-DASH and HLS manifests. Made with the purpose of obtaining a simplified representation convenient for further downloading of segments.
+
+
## Install
```shell
@@ -15,9 +20,17 @@ npm i dasha
## Quick start
```js
+import fs from 'node:fs/promises';
import { parse } from 'dasha';
const url = 'https://dash.akamaized.net/dash264/TestCases/1a/sony/SNE_DASH_SD_CASE1A_REVISED.mpd';
const body = await fetch(url).then((res) => res.text());
const manifest = await parse(body, url);
+
+for (const track of manifest.tracks.all) {
+ for (const segment of track.segments) {
+ const content = await fetch(url).then((res) => res.arrayBuffer());
+ await fs.appendFile(`${track.id}.mp4`, content);
+ }
+}
```
diff --git a/dasha.js b/dasha.js
index 1c3eb09..6130b64 100644
--- a/dasha.js
+++ b/dasha.js
@@ -2,6 +2,7 @@
const dash = require('./lib/dash');
const hls = require('./lib/hls');
+const { filterByResolution, filterByQuality, filterByCodecs } = require('./lib/track');
const parse = (text, url, fallbackLanguage) => {
if (text.includes(' {
else throw new Error('Invalid manifest');
};
-module.exports = { parse };
+module.exports = { parse, filterByResolution, filterByQuality, filterByCodecs };
diff --git a/docs/README.ru.md b/docs/README.ru.md
new file mode 100644
index 0000000..9c9651a
--- /dev/null
+++ b/docs/README.ru.md
@@ -0,0 +1,36 @@
+# dasha
+
+[![npm version](https://img.shields.io/npm/v/dasha?style=flat&color=white)](https://www.npmjs.com/package/dasha)
+[![npm downloads/month](https://img.shields.io/npm/dm/dasha?style=flat&color=white)](https://www.npmjs.com/package/dasha)
+[![npm downloads](https://img.shields.io/npm/dt/dasha?style=flat&color=white)](https://www.npmjs.com/package/dasha)
+
+Библиотека для парсинга MPEG-DASH и HLS манифестов. Создана с целью получения упрощенного представления, удобного для дальнейшей загрузки сегментов.
+
+
+
+## Установка
+
+```shell
+npm i dasha
+```
+
+## Быстрый старт
+
+```js
+import fs from 'node:fs/promises';
+import { parse } from 'dasha';
+
+const url = 'https://dash.akamaized.net/dash264/TestCases/1a/sony/SNE_DASH_SD_CASE1A_REVISED.mpd';
+const body = await fetch(url).then((res) => res.text());
+const manifest = await parse(body, url);
+
+for (const track of manifest.tracks.all) {
+ for (const segment of track.segments) {
+ const content = await fetch(url).then((res) => res.arrayBuffer());
+ await fs.appendFile(`${track.id}.mp4`, content);
+ }
+}
+```
diff --git a/lib/dash.js b/lib/dash.js
index 65b6e63..c3ee1e1 100644
--- a/lib/dash.js
+++ b/lib/dash.js
@@ -21,6 +21,8 @@ const {
createVideoQualityFilter,
createAudioLanguageFilter,
createSubtitleLanguageFilter,
+ createVideoCodecFilter,
+ createAudioCodecFilter,
} = require('./track');
const appendUtils = (element) => {
@@ -173,10 +175,14 @@ const parseSegmentsFromTemplate = (
const startNumber = Number(segmentTemplate.get('startNumber') || 1);
const segmentTimeline = segmentTemplate.get('SegmentTimeline');
resolveSegmentTemplateUrls(segmentTemplate, baseUrl, manifestUrl);
- if (!duration) throw new Error('Duration of the Period was unable to be determined.');
const segmentDuration = parseFloat(segmentTemplate.get('duration'));
const segmentTimescale = parseFloat(segmentTemplate.get('timescale') || 1);
- const segmentsCount = Math.ceil(duration / (segmentDuration / segmentTimescale));
+ // TODO: Support live manifests with type=dynamic
+ const DEFAULT_SEGMENTS_COUNT = 35;
+ // if (!duration) throw new Error('Duration of the Period was unable to be determined.');
+ const segmentsCount = duration
+ ? Math.ceil(duration / (segmentDuration / segmentTimescale))
+ : DEFAULT_SEGMENTS_COUNT;
const bandwidth = representation.get('bandwidth');
const id = representation.get('id');
const segments = [];
@@ -222,12 +228,13 @@ const parseSegmentFromBase = async (segmentBase, baseUrl) => {
const initialization = segmentBase.get('Initialization');
let mediaRange = '';
if (initialization) {
- const range = initialization.get('range');
- const headers = range ? { Range: `bytes=${range}` } : undefined;
- const response = await fetch(baseUrl, headers);
- const initData = await response.arrayBuffer();
- const totalSize = response.headers.get('Content-Range').split('/')[-1];
- if (totalSize) mediaRange = `${initData.byteLength}-${totalSize}`;
+ // const range = initialization.get('range');
+ // const headers = range ? { Range: `bytes=${range}` } : undefined;
+ // const response = await fetch(baseUrl, headers);
+ // const initData = await response.arrayBuffer();
+ // console.log(response.headers);
+ // const totalSize = response.headers.get('Content-Range').split('/')[-1];
+ // if (totalSize) mediaRange = `${initData.byteLength}-${totalSize}`;
}
return { url: baseUrl, range: mediaRange };
};
@@ -398,6 +405,11 @@ 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,
tracks: {
@@ -406,7 +418,9 @@ const parseManifest = async (text, url, fallbackLanguage) => {
audios,
subtitles,
withResolution: createResolutionFilter(videos),
+ withVideoCodecs: createVideoCodecFilter(videos),
withVideoQuality: createVideoQualityFilter(videos),
+ withAudioCodecs: createAudioCodecFilter(audios),
withAudioLanguages: createAudioLanguageFilter(audios),
withSubtitleLanguages: createSubtitleLanguageFilter(subtitles),
},
diff --git a/lib/track.js b/lib/track.js
index 70a2579..bc4d028 100644
--- a/lib/track.js
+++ b/lib/track.js
@@ -1,6 +1,6 @@
'use strict';
-const { getWidth, getBestTrack } = require('./util');
+const { getBestTrack } = require('./util');
const parseMimes = (codecs) =>
codecs
@@ -16,15 +16,29 @@ const createResolutionFilter = (videos) => {
};
};
+const createCodecFilter = (tracks) => {
+ return (codecs) => {
+ if (!codecs?.length) return tracks;
+ const results = tracks.filter((track) => codecs.includes(track.codec));
+ results.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
+ return results;
+ };
+};
+
+const createVideoCodecFilter = (videos) => createCodecFilter(videos);
+
const createVideoQualityFilter = (videos) => {
return (quality) => {
if (!quality) return [getBestTrack(videos)];
const trackQuality = String(quality).includes('p') ? quality : `${quality}p`;
const results = videos.filter((track) => track.quality === trackQuality);
+ results.sort((a, b) => b.bitrate.bps - a.bitrate.bps);
return results.length ? results : [getBestTrack(videos)];
};
};
+const createAudioCodecFilter = (audios) => createCodecFilter(audios);
+
const createAudioLanguageFilter = (audios) => {
return (languages = [], maxTracksPerLanguage) => {
if (!languages.length) {
@@ -61,10 +75,19 @@ const createSubtitleLanguageFilter = (subtitles) => {
};
};
+const filterByResolution = (tracks, resolution) => createResolutionFilter(tracks)(resolution);
+const filterByQuality = (tracks, quality) => createVideoQualityFilter(tracks)(quality);
+const filterByCodecs = (tracks, codecs) => createCodecFilter(tracks)(codecs);
+
module.exports = {
parseMimes,
createResolutionFilter,
+ createVideoCodecFilter,
createVideoQualityFilter,
+ createAudioCodecFilter,
createAudioLanguageFilter,
createSubtitleLanguageFilter,
+ filterByResolution,
+ filterByQuality,
+ filterByCodecs,
};
diff --git a/lib/xml.js b/lib/xml.js
index 6400198..9db2f41 100644
--- a/lib/xml.js
+++ b/lib/xml.js
@@ -28,7 +28,7 @@ function parse(text, options = {}) {
var closeTag = text.substring(closeStart, pos);
if (closeTag.indexOf(tagName) == -1) {
- var parsedText = text.substring(0, pos).split('\n');
+ const parsedText = text.substring(0, pos).split('\n');
throw new Error(
'Unexpected close tag\nLine: ' +
(parsedText.length - 1) +
@@ -103,7 +103,7 @@ function parse(text, options = {}) {
node.children = [];
}
} else {
- var parsedText = parseText();
+ const parsedText = parseText();
if (keepWhitespace) {
if (parsedText.length > 0) {
children.push(parsedText);
@@ -191,12 +191,12 @@ function parse(text, options = {}) {
// optional parsing of children
if (text.charCodeAt(pos - 1) !== slashCC) {
if (tagName == 'script') {
- var start = pos + 1;
+ const start = pos + 1;
pos = text.indexOf('', pos);
children = [text.slice(start, pos)];
pos += 9;
} else if (tagName == 'style') {
- var start = pos + 1;
+ const start = pos + 1;
pos = text.indexOf('', pos);
children = [text.slice(start, pos)];
pos += 8;
@@ -241,10 +241,10 @@ function parse(text, options = {}) {
}
}
- var out = null;
+ let out = null;
if (options.attrValue !== undefined) {
options.attrName = options.attrName || 'id';
- var out = [];
+ out = [];
while ((pos = findElements()) !== -1) {
pos = text.lastIndexOf('<', pos);
@@ -264,10 +264,6 @@ function parse(text, options = {}) {
out = filter(out, options.filter);
}
- if (options.simplify) {
- return simplify(Array.isArray(out) ? out : [out]);
- }
-
if (options.setPos) {
out.pos = pos;
}
diff --git a/test/dasha.test.js b/test/dasha.test.js
index 4481a57..fd00484 100644
--- a/test/dasha.test.js
+++ b/test/dasha.test.js
@@ -26,6 +26,6 @@ test('DASH: parse without URL parameter', async () => {
const manifest = await parse(text);
strictEqual(manifest.tracks.all.length, 8);
const initUrl =
- 'https://a-vrv.akamaized.net/evs3/8a1b3acce53d49eea0ce2104fae30046/assets/p/c46e06c5fd496e8aec0b6776b97eca3f_,3748583.mp4,3748584.mp4,3748582.mp4,3748580.mp4,3748581.mp4,.urlset/init-f1-v1-x3.mp4?t=exp=1713871228~acl=/evs3/8a1b3acce53d49eea0ce2104fae30046/assets/p/c46e06c5fd496e8aec0b6776b97eca3f_,3748583.mp4,3748584.mp4,3748582.mp4,3748580.mp4,3748581.mp4,.urlset/*~hmac=7dc7daeb338da040c65111a88cbf947505a5897f42d1c433c3858f8d890ed29c';
+ 'https://a-vrv.akamaized.net/evs3/8a1b3acce53d49eea0ce2104fae30046/assets/p/c46e06c5fd496e8aec0b6776b97eca3f_,3748583.mp4,3748584.mp4,3748582.mp4,3748580.mp4,3748581.mp4,.urlset/init-f2-v1-x3.mp4?t=exp=1713871228~acl=/evs3/8a1b3acce53d49eea0ce2104fae30046/assets/p/c46e06c5fd496e8aec0b6776b97eca3f_,3748583.mp4,3748584.mp4,3748582.mp4,3748580.mp4,3748581.mp4,.urlset/*~hmac=7dc7daeb338da040c65111a88cbf947505a5897f42d1c433c3858f8d890ed29c';
strictEqual(manifest.tracks.videos[0].segments[0].url, initUrl);
});
diff --git a/types/dasha.d.ts b/types/dasha.d.ts
index f84d6c5..7b71784 100644
--- a/types/dasha.d.ts
+++ b/types/dasha.d.ts
@@ -8,12 +8,19 @@ export interface Manifest {
audios: AudioTrack[];
subtitles: SubtitleTrack[];
withResolution(resolution: { width?: string; height?: string }): VideoTrack[];
+ withVideoCodecs(codecs: VideoCodec[]): VideoTrack[];
withVideoQuality(quality: number | string): VideoTrack[];
+ withAudioCodecs(codecs: AudioCodec[]): AudioTrack[];
withAudioLanguages(languages: string[], maxTracksPerLanguage?: number): AudioTrack[];
withSubtitleLanguages(languages: string[]): SubtitleTrack[];
};
}
+export function filterByResolution(resolution: { width?: string; height?: string }): VideoTrack[];
+export function filterByCodecs(tracks: VideoTrack[], codecs: VideoCodec[]): VideoTrack[];
+export function filterByCodecs(tracks: AudioTrack[], codecs: AudioCodec[]): AudioTrack[];
+export function filterByQuality(tracks: VideoTrack[], quality: number | string): VideoTrack[];
+
export interface Track {
id: string;
type: 'video' | 'audio' | 'text';