Skip to content

azot-labs/dasha

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

447 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dasha

npm version npm downloads/month npm downloads

Library for working with MPEG-DASH (.mpd) manifests and HLS (.m3u8) playlists through a Mediabunny-compatible Input API. Made with the purpose of obtaining a simplified representation convenient for further downloading of segments by URLs and getting basic metadata about the tracks.

Install

npm install dasha

Usage

Reading HLS

In the example below, we read the segment information for a specific video track and save it to a file.

import fs from 'node:fs/promises';
import { desc, HLS_FORMATS, Input, UrlSource } from 'dasha';

async function saveVideo() {
  const input = new Input({
    source: new UrlSource(
      'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8',
      { requestInit: { headers: { Referer: 'https://bitmovin.com/' } } },
    ),
    formats: HLS_FORMATS,
  });

  const videoTracks = await input.getVideoTracks({
    sortBy: async (track) => [
      desc(await track.getDisplayHeight()),
      // Tracks with matching resolution are sorted by bitrate
      desc(await track.getBitrate()),
    ],
    // Filter out #EXT-X-I-FRAME-STREAM-INF tracks
    filter: async (track) => !(await track.hasOnlyKeyPackets()),
  });

  const bestVideoTrack = videoTracks[0];
  console.log('Dynamic range:', await bestVideoTrack.getDynamicRange()); // sdr

  const segments = await bestVideoTrack.getSegments();

  const outputPath = 'output.mp4';
  const urls = segments.map((segment) => segment.location.path);
  const initSegment = segments[0]?.initSegment;
  if (initSegment) urls.unshift(initSegment.location.path);
  for (const url of urls) {
    const content = await fetch(url).then((res) => res.arrayBuffer());
    await fs.appendFile(outputPath, new Uint8Array(content));
  }
};

Reading DASH

Everything here is identical to the example above, with the sole exception that an URL to a DASH manifest is used instead of an HLS playlist.

import fs from 'node:fs/promises';
import { DASH_FORMATS, Input, UrlSource, desc } from 'dasha';

async function saveDashVideo() {
  const input = new Input({
    source: new UrlSource(
      'https://dash.akamaized.net/dash264/TestCases/1a/netflix/exMPD_BIP_TC1.mpd',
    ),
    formats: DASH_FORMATS,
  });

  const videoTracks = await input.getVideoTracks({
    sortBy: async (track) => [
      desc(await track.getDisplayHeight()),
      desc(await track.getBitrate()),
    ],
  });

  const bestVideoTrack = videoTracks[0];
  const segments = await bestVideoTrack.getSegments();

  const outputPath = 'output.m4s';
  const urls = segments.map((segment) => segment.location.path);
  const initSegment = segments[0]?.initSegment;
  if (initSegment) urls.unshift(initSegment.location.path);
  for (const url of urls) {
    const content = await fetch(url).then((res) => res.arrayBuffer());
    await fs.appendFile(outputPath, new Uint8Array(content));
  }
}

Adding external subtitle tracks

addSubtitleTrack() is useful when subtitle URLs are provided separately from the HLS/DASH manifest. Added subtitles become part of the same Input, so they can be queried, filtered and downloaded through the regular subtitle track API.

import { DASH_FORMATS, Input, UrlSource } from 'dasha';

async function getEnglishSubtitles() {
  const input = new Input({
    source: new UrlSource('https://example.com/manifest.mpd'),
    formats: DASH_FORMATS,
  });

  const primaryVideoTrack = await input.getPrimaryVideoTrack();
  if (!primaryVideoTrack) {
    throw new Error('No video tracks found');
  }

  input.addSubtitleTrack(new UrlSource('https://cdn.example.com/subtitles/en.vtt'), {
    languageCode: 'en',
    name: 'English',
    pairWith: primaryVideoTrack,
  });

  const englishSubtitleTracks = await input.getSubtitleTracks({
    filter: async (track) => (await track.getLanguageCode()) === 'en',
  });

  const englishSubtitleTrack = englishSubtitleTracks[0];
  if (!englishSubtitleTrack) {
    return [];
  }

  return await englishSubtitleTrack.getSegments();
}

Overriding track metadata

Some services expose track metadata outside the manifest. You can override the parsed language code on any dasha track, and the updated value will be visible through the regular query/filter APIs.

import { DASH_FORMATS, Input, UrlSource } from 'dasha';

async function getFrenchAudioTrack() {
  const input = new Input({
    source: new UrlSource('https://example.com/manifest.mpd'),
    formats: DASH_FORMATS,
  });

  const audioTracks = await input.getAudioTracks();
  const apiLanguageCode = 'fr';

  audioTracks[0]?.setLanguageCode(apiLanguageCode);

  return await input.getAudioTracks({
    filter: async (track) => (await track.getLanguageCode()) === apiLanguageCode,
  });
}

Mediabunny with DASH support

Only reading is supported

Similar to downloading an HLS playlist as an MP4 you can do this:

import { Conversion, FilePathTarget, Mp4OutputFormat, Output, Input, UrlSource } from 'mediabunny';
import { DASH_FORMATS } from 'dasha';

const input = new Input({
	source: new UrlSource('https://example.com/manifest.mpd'),
	formats: DASH_FORMATS,
});

const output = new Output({
	format: new Mp4OutputFormat(),
	target: new FilePathTarget('output.mp4'),
});

const conversion = await Conversion.init({ input, output });
await conversion.execute();

// Done

See reading HLS guide for more use cases (many things can be used with DASH as well).

Credits

mediabunny