Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CodSpeed

on:
push:
branches:
- master
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:

permissions:
contents: read
id-token: write # for OpenID Connect authentication with CodSpeed

jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Install benchmark dependencies
working-directory: benchmarks
run: npm install

- name: Run benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
working-directory: benchmarks
run: npx vitest bench --run
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# dash.js

[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/slederer/dash.js?utm_source=badge)

A reference client implementation for the playback of MPEG DASH via JavaScript and compliant browsers. Learn more about DASH IF Reference Client.

If your intent is to use the player code without contributing back to this project, then use the MASTER branch which holds the approved and stable public releases.
Expand All @@ -25,3 +27,16 @@ Download 'master' or latest tagged release, extract and open main folder dash.js
```
grunt --config Gruntfile.js --force
```

## Benchmarks

Performance benchmarks live in the `benchmarks/` directory and are measured
continuously with [CodSpeed](https://codspeed.io/). They exercise the MPD
manifest parser (`DashParser`), which runs on every manifest load and refresh.

Run them locally with:
```
cd benchmarks
npm install
npm run bench
```
39 changes: 39 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# dash.js benchmarks

Performance benchmarks for dash.js, measured continuously with
[CodSpeed](https://codspeed.io/).

These benchmarks are intentionally isolated in their own package so they can run
on a modern Node.js toolchain (vitest) without pulling in the legacy,
browser-only build tooling used by the rest of the project.

## What is benchmarked

The suite targets `Dash.dependencies.DashParser`, the component that turns a raw
MPEG-DASH manifest (an XML string) into the JavaScript object tree the player
consumes. Parsing happens on every manifest load and every live-manifest
refresh, so it sits on a hot, CPU-bound path.

The original parser targets the browser and relies on global variables (`Dash`,
`X2JS`, `ObjectIron`, `Q`) and a DOM (`window.DOMParser`). `loadDash.js`
recreates that environment in Node.js using [`@xmldom/xmldom`](https://github.com/xmldom/xmldom)
and evaluates the relevant source files in a shared `vm` context, so the
benchmarks run the real, unmodified parser code.

`fixtures.js` generates representative manifests of varying sizes:

- SegmentTemplate-based manifests (compact, live-style)
- SegmentList-based manifests (large, with many `SegmentURL` entries)

## Running

```bash
npm install
npm run bench
```

To run with the CodSpeed CLI in simulation mode:

```bash
codspeed run --mode simulation -- npx vitest bench --run
```
42 changes: 42 additions & 0 deletions benchmarks/dashParser.bench.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// CodSpeed benchmarks for the dash.js MPD parser.
//
// DashParser.parse() is the entry point that turns a raw MPEG-DASH manifest
// (an XML string) into the JavaScript object tree the player consumes. It runs
// on every manifest load and manifest refresh, so it sits on a hot path and is
// a good CPU-bound target for continuous performance measurement.

import { bench, describe } from "vitest";
import { createParser } from "./loadDash.js";
import {
buildSegmentListManifest,
buildSegmentTemplateManifest,
} from "./fixtures.js";

const baseUrl = "http://dashdemo.edgesuite.net/envivio/dashpr/clear/";

const parser = createParser();

// Pre-build the manifest strings so the benchmark measures parsing only, not
// fixture generation.
const smallTemplateMpd = buildSegmentTemplateManifest(6);
const largeTemplateMpd = buildSegmentTemplateManifest(50);
const segmentListMpd = buildSegmentListManifest(60);
const largeSegmentListMpd = buildSegmentListManifest(300);

describe("DashParser.parse", () => {
bench("SegmentTemplate manifest (small)", async () => {
await parser.parse(smallTemplateMpd, baseUrl);
});

bench("SegmentTemplate manifest (50 representations)", async () => {
await parser.parse(largeTemplateMpd, baseUrl);
});

bench("SegmentList manifest (60 segments/representation)", async () => {
await parser.parse(segmentListMpd, baseUrl);
});

bench("SegmentList manifest (300 segments/representation)", async () => {
await parser.parse(largeSegmentListMpd, baseUrl);
});
});
78 changes: 78 additions & 0 deletions benchmarks/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Generates representative MPEG-DASH manifests (MPDs) of varying sizes for
// benchmarking the DashParser. The shapes mirror the manifests used by the
// project's own test suite (SegmentList- and SegmentTemplate-based manifests).

function segmentListRepresentation(id, codecs, width, height, bandwidth, segmentCount) {
const segments = [];
for (let i = 1; i <= segmentCount; i++) {
segments.push(`<SegmentURL media="mp4-main-multi-${id}-${i}.m4s"/>`);
}
return `
<Representation id="${id}" mimeType="video/mp4" codecs="${codecs}" width="${width}" height="${height}" frameRate="25" sar="1:1" startWithSAP="1" bandwidth="${bandwidth}">
<SegmentList timescale="1000" duration="10000">
<Initialization sourceURL="mp4-main-multi-${id}-.mp4"/>
${segments.join("\n ")}
</SegmentList>
</Representation>`;
}

// A large, static, SegmentList-based manifest similar to MPD2 in the test suite.
export function buildSegmentListManifest(segmentsPerRepresentation = 60) {
const representations = [
segmentListRepresentation("h264bl_low", "avc1.42c00d", 320, 180, 50877, segmentsPerRepresentation),
segmentListRepresentation("h264bl_mid", "avc1.42c01e", 640, 360, 194870, segmentsPerRepresentation),
segmentListRepresentation("h264bl_hd", "avc1.42c01f", 1280, 720, 514828, segmentsPerRepresentation),
segmentListRepresentation("h264bl_full", "avc1.42c033", 1920, 1080, 1553336, segmentsPerRepresentation),
];

return `<MPD xmlns="urn:mpeg:DASH:schema:MPD:2011" type="static" minBufferTime="PT1.5S" mediaPresentationDuration="PT0H10M0.00S" profiles="urn:mpeg:dash:profile:isoff-main:2011">
<ProgramInformation moreInformationURL="http://gpac.sourceforge.net">
<Title>mp4-main-multi-mpd-AV-NBS.mpd generated by GPAC</Title>
<Copyright>TelecomParisTech(c)2012</Copyright>
</ProgramInformation>
<Period start="PT0S" duration="PT0H10M0.00S">
<AdaptationSet segmentAlignment="true" maxWidth="1920" maxHeight="1080" maxFrameRate="25" par="16:9">
<ContentComponent id="1" contentType="video"/>
${representations.join("\n ")}
</AdaptationSet>
<AdaptationSet segmentAlignment="true">
<ContentComponent id="2" contentType="audio"/>
<Representation id="aaclc" mimeType="audio/mp4" codecs="mp4a.40.2" audioSamplingRate="44100" startWithSAP="1" bandwidth="132483">
<SegmentList timescale="1000" duration="10000">
<Initialization sourceURL="mp4-main-multi-aaclc-.mp4"/>
${Array.from({ length: segmentsPerRepresentation }, (_, i) => `<SegmentURL media="mp4-main-multi-aaclc-${i + 1}.m4s"/>`).join("\n ")}
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>`;
}

// A compact, SegmentTemplate-based live-style manifest similar to MPD1.
export function buildSegmentTemplateManifest(representationsCount = 6) {
const videoReps = [];
for (let i = 0; i < representationsCount; i++) {
videoReps.push(
`<Representation id="v${i}" codecs="avc1.4d401e" width="720" height="576" bandwidth="${(i + 1) * 200000}"/>`
);
}

return `<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:mpeg:dash:schema:mpd:2011"
xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"
type="static"
minBufferTime="PT2S"
profiles="urn:mpeg:dash:profile:isoff-live:2011"
mediaPresentationDuration="PT234S">
<Period>
<AdaptationSet mimeType="video/mp4" segmentAlignment="true" startWithSAP="1">
<SegmentTemplate duration="2" startNumber="1" media="video_$Number$_$Bandwidth$bps.mp4" initialization="video_$Bandwidth$bps.mp4"/>
${videoReps.join("\n ")}
</AdaptationSet>
<AdaptationSet mimeType="audio/mp4" codecs="mp4a.40.5" segmentAlignment="true" startWithSAP="1">
<SegmentTemplate duration="2" startNumber="1" media="audio_$Number$_$Bandwidth$bps.mp4" initialization="audio_$Bandwidth$bps.mp4"/>
<Representation id="a2" bandwidth="56000"/>
</AdaptationSet>
</Period>
</MPD>`;
}
58 changes: 58 additions & 0 deletions benchmarks/loadDash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Loads the browser-oriented dash.js source files into a Node.js sandbox so
// that the MPD parsing pipeline (DashParser + X2JS + ObjectIron) can be
// benchmarked outside of a browser.
//
// The original source targets the browser and relies on global variables
// (Dash, X2JS, ObjectIron, Q) and a DOM (window.DOMParser). We recreate that
// environment with @xmldom/xmldom and evaluate the relevant scripts in a shared
// vm context, then hand back the pieces needed to run a parse.

import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import vm from "node:vm";
import { createRequire } from "node:module";
import { DOMParser } from "@xmldom/xmldom";

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = join(__dirname, "..");
const require = createRequire(import.meta.url);

// Q ships a proper CommonJS build, so require it directly.
const Q = require(join(repoRoot, "app/lib/q.js"));

function read(relativePath) {
return readFileSync(join(repoRoot, relativePath), "utf-8");
}

export function createParser() {
// A minimal browser-like global scope for the legacy scripts.
const sandbox = {
window: { DOMParser },
DOMParser,
Q,
console,
};
sandbox.global = sandbox;
vm.createContext(sandbox);

// Order matters: the Dash namespace and the helper libraries must exist
// before DashParser attaches itself to Dash.dependencies.
const scripts = [
"app/lib/xml2json.js",
"app/lib/objectiron.js",
"app/js/dash/Dash.js",
"app/js/dash/DashParser.js",
];

for (const script of scripts) {
vm.runInContext(read(script), sandbox, { filename: script });
}

const parser = sandbox.Dash.dependencies.DashParser();
// DashParser expects a `debug` collaborator injected via DI; a no-op stub is
// enough for parsing.
parser.debug = { log() {} };

return parser;
}
Loading
Loading