This guide explains how to extend or modify DASH manifest transformations in Vinyl.
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>>.
createDefaultDashManifestTransformer() composes the following using
flowAsync:
canPlayMimeType)canPlayKeySystem)supportsAudioSamplingRate)selectionPriority (descending)qualityRanking (ascending), then bandwidth
(descending)The result is mapped over the manifest controller using mapManifestTransform.
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
)
}
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
)
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
)
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)
}
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
),
}
})
}
import { flow } from '@amzn/vinyl-util'
function createMyManifestTransformer(
deps: MyTransformerDeps
): ObservableValue<Promise<DashManifestAndPath>> {
return flow(
createCustomFilter(deps),
createQualityFilter(deps),
createSortTransformer()
)(createDefaultDashManifestTransformer(deps))
}
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
)
},
})
},
}
)
filterDashAdaptationSets() - Filter adaptation sets with error handlingfilterDashRepresentations() - Filter representations with error handlingfilterDashQualities() - Filter representations by media quality metadatafilterDashQualitiesAsync() - Async version of filterDashQualitiessortDashAdaptationSets() - Sort adaptation sets by comparatorsortDashRepresentations() - Sort representations by comparatormanifestIsPlayable() - Check if manifest has playable contentmapManifestTransform() - Apply a
(DashManifest) => MaybePromise<DashManifest> over a manifest observable,
preserving baseUrlflow