DASH Manifest Transform Extension Guide (Advanced)

This guide explains how to extend or modify DASH manifest transformations in Vinyl.

Overview

DASH manifest transformers filter, sort, and modify DASH manifests before playback. The system uses a functional architecture where transformers are composable functions over ObservableValue<Promise<DashManifestAndPath>>.

Default Implementation

createDefaultDashManifestTransformer() composes the following using flowAsync:

The result is mapped over the manifest controller using mapManifestTransform.

Creating Custom Filters

Quality-Based Filters (Recommended)

Quality metadata filters work across both Dash and HLS:

import { filterDashQualities, MediaUnsupportedError } from '@amzn/vinyl'
import type { MediaQualityMetadata } from '@amzn/vinyl/dist/types/current'
import type { DashManifest } from '@amzn/vinyl-mpd-parser/dist/types/current'

function canPlayBitrate(metadata: MediaQualityMetadata): boolean {
    return metadata.bandwidth <= 1_000_000
}

function throwBitrateUnsupported(): never {
    throw new MediaUnsupportedError(
        'No representations within bitrate range',
        'bitrate_filter'
    )
}

// Dash-specific transform which filters bitrates above 1_000_000 bps
export function filterDashBitrates(manifest: DashManifest) {
    return filterDashQualities(
        { mediaQualityMetadataResolver: deps.mediaQualityMetadataResolver },
        canPlayBitrate,
        throwBitrateUnsupported,
        manifest
    )
}

Async Quality Filters

For filters requiring async operations (e.g. DRM checks):

import { filterDashQualitiesAsync, MediaUnsupportedError } from '@amzn/vinyl'

async function canPlayKeySystem(
    deps: { drmController: DrmController },
    metadata: MediaQualityMetadata
): Promise<boolean> {
    if (!metadata.contentProtections.length) return true
    return (await deps.drmController.isSupported(metadata)).supported
}

function throwKeySystemUnsupported(): never {
    throw new MediaUnsupportedError('No key system supported.', 'key-system')
}

const filteredManifest = await filterDashQualitiesAsync(
    { mediaQualityMetadataResolver: deps.mediaQualityMetadataResolver },
    (metadata) => canPlayKeySystem(deps, metadata),
    throwKeySystemUnsupported,
    manifest
)

Direct Representation / Adaptation Set Filters

For filtering without quality metadata:

import {
    filterDashRepresentations,
    filterDashAdaptationSets,
    MediaUnsupportedError,
} from '@amzn/vinyl'

const filteredManifest = filterDashRepresentations(
    (representation) => representation.bandwidth <= MAX_BITRATE,
    () => {
        throw new MediaUnsupportedError(
            'No representations within bitrate range',
            'bitrate_filter'
        )
    },
    manifest
)

Creating Custom Transformers

Transform Function

Use flowAsync to compose multiple filter/sort steps, and mapManifestTransform to apply them over a manifest observable:

import {
    filterDashQualities,
    sortDashRepresentations,
    mapManifestTransform,
} from '@amzn/vinyl'
import { flowAsync } from '@amzn/vinyl-util'

function createCustomTransformer(deps: DashManifestTransformerDeps) {
    const transformManifest = flowAsync(
        (m) => filterDashQualities(deps, customPredicate, throwCustomError, m),
        (m) => sortDashRepresentations(customComparator, m)
    )

    return mapManifestTransform(deps.manifestController, transformManifest)
}

Filter Factory

Transformers are factory functions with the signature (ObservableValue<Promise<DashManifestAndPath>>) => ObservableValue<Promise<DashManifestAndPath>>. This allows composition with flow:

function createCustomFilter(deps: CustomDeps) {
    return (
        manifestController: ObservableValue<Promise<DashManifestAndPath>>
    ): ObservableValue<Promise<DashManifestAndPath>> =>
        mapManifestTransform(manifestController, (manifest) =>
            filterDashRepresentations(
                (rep) => customPredicate(deps, rep),
                throwCustomError,
                manifest
            )
        )
}

For filters that depend on additional reactive inputs, use combineData:

function createConfigFilter(deps: {
    readonly configProvider: ObservableValue<FilterConfig>
}): (
    manifestAndPath: ObservableValue<Promise<DashManifestAndPath>>
) => ObservableValue<Promise<DashManifestAndPath>> {
    return (manifestAndPath) =>
        combineData({
            manifestAndPath,
            config: deps.configProvider,
        }).map(async ({ manifestAndPath, config }) => {
            const { manifest, baseUrl } = await manifestAndPath
            return {
                baseUrl,
                manifest: filterDashRepresentations(
                    (rep) => rep.bandwidth <= config.maxBitrate,
                    throwConfigFilterError,
                    manifest
                ),
            }
        })
}

Composing Transformers with flow

import { flow } from '@amzn/vinyl-util'

function createMyManifestTransformer(
    deps: MyTransformerDeps
): ObservableValue<Promise<DashManifestAndPath>> {
    return flow(
        createCustomFilter(deps),
        createQualityFilter(deps),
        createSortTransformer()
    )(createDefaultDashManifestTransformer(deps))
}

Integration with createVinylPlayer

import {
    createVinylPlayer,
    createDefaultDashManifestTransformer,
    createDashFactories,
    filterDashQualities,
    mapManifestTransform,
    MediaUnsupportedError,
} from '@amzn/vinyl'
import { flowAsync } from '@amzn/vinyl-util'

function canPlayStereoOnly(metadata: MediaQualityMetadata): boolean {
    return (
        !metadata.audioChannelConfiguration ||
        metadata.audioChannelConfiguration <= 2
    )
}

function throwStereoUnsupported(): never {
    throw new MediaUnsupportedError(
        'Only stereo audio supported',
        'stereo_only'
    )
}

const player = createVinylPlayer(
    { media: new Audio() },
    {
        createDashFactories: (deps: DashFactoryDeps) => {
            return (loadOptions: DashTrackLoadOptions) => ({
                ...createDashFactories({
                    /* options */
                })(deps)(loadOptions),
                manifestTransformed: (deps: DashManifestTransformerDeps) => {
                    const transformManifest = flowAsync((m) =>
                        filterDashQualities(
                            deps,
                            canPlayStereoOnly,
                            throwStereoUnsupported,
                            m
                        )
                    )

                    return mapManifestTransform(
                        createDefaultDashManifestTransformer(deps),
                        transformManifest
                    )
                },
            })
        },
    }
)

Utility Functions

Best Practices

  1. Error Handling: Always provide meaningful error codes and messages
  2. Performance: Use async filters only for expensive operations (e.g. DRM)
  3. Composition: Prefer small, focused filter factories composed with flow
  4. Immutability: Filter utilities clone manifests internally
  5. Testing: Ensure 100% test coverage for custom transformers