Skip to content
Merged
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
42 changes: 25 additions & 17 deletions packages/mabr-route-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ ROUTE/FDT `contentLocation` as the cache key, and stores DASH/HLS manifests and
CMAF objects. Shaka keeps its normal manifest and segment URLs:

```ts
import { createRouteMabrSession } from '@blockcast/mabr-route-proxy';
import { createRouteMabrSession, discoverSlsEndpoint } from '@blockcast/mabr-route-proxy';

// ROUTE objects are mounted under baseUrl: an FDT Content-Location of
// `live/manifest.mpd` is served at `${baseUrl}live/manifest.mpd`.
const baseUrl = 'https://player.example/mabr/';
const manifestUrl = new URL('live/manifest.mpd', baseUrl).href;
const toSls = await discoverSlsEndpoint({
sltUrl: 'https://live.example',
serviceId: 'generic-service',
});
if (!toSls.manifestUrl) throw new Error('DS SLS did not advertise a DASH/HLS manifest');
// ROUTE Content-Location and the public DS manifest must share this cache
// scope; derive it from the URL signalled by the DS.
const baseUrl = new URL('.', toSls.manifestUrl).href;
const proxy = createRouteMabrSession({
gateway: window.multicast,
wasm: routeWasm,
// `toSls` is the service SLS endpoint selected by the sender/TO. Use
// `toSls` is the service SLS endpoint selected by the public DS. Use
// ingress: 'amt'; with an SSM source the gateway discovers the relay via
// RFC 8777 DNS (relay/relayPort are only an explicit override).
ingress: 'native',
Expand All @@ -28,7 +32,7 @@ const proxy = createRouteMabrSession({
const restoreFetch = proxy.installFetch();
await proxy.start();

await shakaPlayer.load(manifestUrl); // DASH MPD or HLS .m3u8
await shakaPlayer.load(toSls.manifestUrl); // DASH MPD or HLS .m3u8
```

`baseUrl` is also the cache scope. Only Content-Location URLs under its
Expand All @@ -39,26 +43,30 @@ own, never the origin root. A root `baseUrl` lets an on-path injector shadow
any same-origin URL.

When a network does not carry LLS on the ATSC 3 ASM group, bootstrap the
service SLS over HTTPS. `sltUrl` is the HTTP LLS/SLT authority exposed by the
TO-selected delivery service; it is used only to select the first service SLS
leg. Its hostname is never used as the AMT relay. The browser still receives
ROUTE bytes through DIMT or AMT, and the S-TSID on that
leg supplies the rendition `(S,G,port)` values:
service SLS over HTTPS. `sltUrl` may be the public delivery-service origin or
an explicit `/slt/<group>` URL. For an origin, the library reads the public
`/slt/` listing and fetches the advertised groups; it never assumes a group
number. The selected service's `SvcInetUrl` supplies the DASH or HLS manifest
URL, while its SLS leg supplies the first multicast join. Its hostname is
never used as the AMT relay. The browser still receives ROUTE bytes through
DIMT or AMT, and the S-TSID on that leg supplies the rendition `(S,G,port)`
values. Manifest discovery is HTTPS-only; an HTTP discovery origin can still
be used for local testing, but it does not produce a `manifestUrl`.

```ts
const sls = await discoverSlsEndpoint({sltUrl: 'https://live.example', serviceId: 'generic-service'});
if (!sls.manifestUrl) throw new Error('DS SLS did not advertise a DASH/HLS manifest');
const proxy = createRouteMabrSession({
gateway: window.multicast,
wasm: routeWasm,
ingress: 'native', // use 'amt' for AMT/DRIAD when SLS has an SSM source
sltUrl: 'https://live.example/slt/',
sltGroup: 7, // optional when /slt/ lists one group
serviceId: 'nasa-iss-a', // globalServiceID or numeric serviceId
slsEndpoint: sls,
discoverEndpoints: true,
baseUrl,
baseUrl: new URL('.', sls.manifestUrl).href,
});
const restoreFetch = proxy.installFetch();
await proxy.start();
await shakaPlayer.load(manifestUrl); // .mpd or .m3u8
await shakaPlayer.load(sls.manifestUrl); // .mpd or .m3u8
```

The client fetches `/slt/` to list access groups, then `/slt/<group>` to read
Expand Down
170 changes: 158 additions & 12 deletions packages/mabr-route-proxy/src/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRouteMabrSession, type RouteObject } from './route.ts';
import { createRouteMabrSession, discoverSlsEndpoint, type RouteObject } from './route.ts';

function fakeWasm(mpd = '#EXTM3U\n# generated by SLS') {
const memory = new WebAssembly.Memory({ initial: 18 });
Expand Down Expand Up @@ -88,6 +88,17 @@ test('feeds ROUTE objects to DASH and HLS URLs and detaches cleanly', async () =
assert.equal(listener, undefined);
});

test('fails loudly when the discovered manifest is outside the cache scope', async () => {
const session = createRouteMabrSession({
gateway: { async subscribe() { throw new Error('must not subscribe'); } },
wasm: fakeWasm(),
ingress: 'native',
slsEndpoint: { group: '232.1.1.66', port: 8000, manifestUrl: 'https://ds.example/live/manifest.mpd' },
baseUrl: 'https://player.example/mabr/',
});
await assert.rejects(session.start(), /outside baseUrl scope/);
});

test('preserves completed ROUTE destination metadata for SLS/FDT routing', async () => {
let listener: ((event: CustomEvent<ArrayBuffer>) => void) | undefined;
let index = 0;
Expand Down Expand Up @@ -308,11 +319,14 @@ test('discovers the service SLS leg from HTTP LLS/SLT', async () => {
return new Response('<ul><li><a href="7" /></li></ul>');
}
if (url === 'https://relay.test/slt/7') {
return new Response(`<SLT><Server serviceId="42" globalServiceID="nasa-iss"><BroadcastSvcSignaling slsProtocol="2" slsDestinationIpAddress="232.0.9.9" slsDestinationUdpPort="5004" /><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.0.1.3" slsDestinationUdpPort="1024" slsSourceIpAddress="0.0.0.0" /></Server></SLT>`);
return new Response(`<SLT><Server serviceId="42" globalServiceID="nasa-iss"><SvcInetUrl urlType="1">/live/manifest.mpd</SvcInetUrl><BroadcastSvcSignaling slsProtocol="2" slsDestinationIpAddress="232.0.9.9" slsDestinationUdpPort="5004" /><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.0.1.3" slsDestinationUdpPort="1024" slsSourceIpAddress="0.0.0.0" /></Server></SLT>`);
}
return new Response('missing', { status: 404 });
}) as typeof fetch;
try {
const discovered = await discoverSlsEndpoint({sltUrl: 'https://relay.test', serviceId: 'nasa-iss'});
assert.equal(discovered.manifestUrl, 'https://relay.test/live/manifest.mpd');
urls.length = 0;
const session = createRouteMabrSession({
gateway: { async subscribe(value: object) {
config = value as Record<string, unknown>;
Expand All @@ -322,6 +336,7 @@ test('discovers the service SLS leg from HTTP LLS/SLT', async () => {
ingress: 'native',
sltUrl: 'https://relay.test',
serviceId: 'nasa-iss',
baseUrl: 'https://relay.test/live/',
});
await session.start();
assert.deepEqual(urls, ['https://relay.test/slt/', 'https://relay.test/slt/7']);
Expand All @@ -334,6 +349,137 @@ test('discovers the service SLS leg from HTTP LLS/SLT', async () => {
}
});

test('discovers an HLS manifest from a DS authority without a fixed SLT group', async () => {
const originalFetch = globalThis.fetch;
const urls: string[] = [];
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
urls.push(url);
if (url === 'https://ds.example.test/slt/') {
return new Response('<html><a href="7/">7/</a></html>');
}
if (url === 'https://ds.example.test/slt/7') {
return new Response('<SLT><Service globalServiceID="generic-service"><SvcInetUrl url="/live/master.m3u8"/><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>');
}
return new Response('missing', {status: 404});
}) as typeof fetch;
try {
assert.deepEqual(await discoverSlsEndpoint({
sltUrl: 'https://ds.example.test',
serviceId: 'generic-service',
}), {
group: '232.1.1.66',
port: 8000,
manifestUrl: 'https://ds.example.test/live/master.m3u8',
});
assert.deepEqual(urls, [
'https://ds.example.test/slt/',
'https://ds.example.test/slt/7',
]);
} finally {
globalThis.fetch = originalFetch;
}
});

test('discovers a TO delivery path from the Blockcast SLT extension', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
'<SLT><Service globalServiceID="generic-service"><PresentationManifest xmlns="urn:blockcast:route:1">/live/manifest.mpd</PresentationManifest><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>',
)) as typeof fetch;
try {
assert.deepEqual(await discoverSlsEndpoint({
sltUrl: 'https://ds.example.test/slt/7',
serviceId: 'generic-service',
}), {
group: '232.1.1.66',
port: 8000,
manifestUrl: 'https://ds.example.test/live/manifest.mpd',
});
} finally {
globalThis.fetch = originalFetch;
}
});

test('accepts dsUrl as the browser discovery bootstrap', async () => {
const originalFetch = globalThis.fetch;
const urls: string[] = [];
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
urls.push(url);
if (url === 'https://ds.example.test/slt/') return new Response('<a href="7">7</a>');
return new Response('<SLT><Service globalServiceID="generic-service"><SvcInetUrl>/live/manifest.mpd</SvcInetUrl><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>');
}) as typeof fetch;
try {
const endpoint = await discoverSlsEndpoint({dsUrl: 'https://ds.example.test', serviceId: 'generic-service'});
assert.equal(endpoint.manifestUrl, 'https://ds.example.test/live/manifest.mpd');
assert.deepEqual(urls, ['https://ds.example.test/slt/', 'https://ds.example.test/slt/7']);
} finally {
globalThis.fetch = originalFetch;
}
});

test('resolves relative discovery URLs before comparing their origins', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url === 'http://localhost/slt/7') {
return new Response('<SLT><Service globalServiceID="generic-service"><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>');
}
return new Response('missing', {status: 404});
}) as typeof fetch;
try {
assert.deepEqual(await discoverSlsEndpoint({
sltUrl: '/slt/7',
dsUrl: 'http://localhost/',
serviceId: 'generic-service',
}), {
group: '232.1.1.66',
port: 8000,
});
} finally {
globalThis.fetch = originalFetch;
}
});

test('rejects duplicate SLS legs with conflicting manifest URLs', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url === 'https://ds.example.test/slt/') return new Response('<a href="7">7</a><a href="8">8</a>');
if (url === 'https://ds.example.test/slt/7') {
return new Response('<SLT><Service globalServiceID="generic-service"><SvcInetUrl>/live/a.mpd</SvcInetUrl><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>');
}
if (url === 'https://ds.example.test/slt/8') {
return new Response('<SLT><Service globalServiceID="generic-service"><SvcInetUrl>/live/b.mpd</SvcInetUrl><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>');
}
return new Response('missing', {status: 404});
}) as typeof fetch;
try {
await assert.rejects(
discoverSlsEndpoint({sltUrl: 'https://ds.example.test', serviceId: 'generic-service'}),
/HTTP LLS advertised 2 ROUTE SLS endpoints/,
);
} finally {
globalThis.fetch = originalFetch;
}
});

test('does not return a cross-origin SvcInetUrl as the DS manifest', async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () => new Response(
'<SLT><Service globalServiceID="generic-service"><SvcInetUrl>https://other.example/live/manifest.mpd</SvcInetUrl><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000"/></Service></SLT>',
)) as typeof fetch;
try {
const endpoint = await discoverSlsEndpoint({
sltUrl: 'https://ds.example.test/slt/7',
serviceId: 'generic-service',
});
assert.equal(endpoint.manifestUrl, undefined);
} finally {
globalThis.fetch = originalFetch;
}
});

test('subscribes to AMT relay bytes', async () => {
let config: Record<string, unknown> | undefined;
const gateway = {
Expand Down Expand Up @@ -421,7 +567,7 @@ test('rejects a sourceless AMT endpoint instead of emitting a DRIAD join', async
assert.equal(subscriptions, 0);
});

test('discovers AMT service bytes through a delivery-service hostname', async () => {
test('discovers AMT service bytes through a public delivery-service authority', async () => {
const originalFetch = globalThis.fetch;
const urls: string[] = [];
const fetchOptions: RequestInit[] = [];
Expand All @@ -430,11 +576,11 @@ test('discovers AMT service bytes through a delivery-service hostname', async ()
const url = String(input);
urls.push(url);
fetchOptions.push(init ?? {});
if (url === 'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/') {
if (url === 'https://ds.example.test/slt/') {
return new Response('<ul><li><a href="7" /></li></ul>');
}
if (url === 'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/7') {
return new Response('<SLT><Server globalServiceID="nasa-iss-a"><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.0.1.3" slsDestinationUdpPort="1024" slsSourceIpAddress="69.25.95.198" /></Server></SLT>');
if (url === 'https://ds.example.test/slt/7') {
return new Response('<SLT><Server globalServiceID="generic-service"><BroadcastSvcSignaling slsProtocol="1" slsDestinationIpAddress="232.1.1.66" slsDestinationUdpPort="8000" slsSourceIpAddress="192.0.2.10" /></Server></SLT>');
}
return new Response('missing', { status: 404 });
}) as typeof fetch;
Expand All @@ -446,21 +592,21 @@ test('discovers AMT service bytes through a delivery-service hostname', async ()
} },
wasm: fakeWasm(),
ingress: 'amt',
sltUrl: 'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net',
serviceId: 'nasa-iss-a',
sltUrl: 'https://ds.example.test',
serviceId: 'generic-service',
});
await session.start();
assert.deepEqual(urls, [
'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/',
'https://cdn.onprem-live-multicast.public-st.cdn.blockcast.net/slt/7',
'https://ds.example.test/slt/',
'https://ds.example.test/slt/7',
]);
assert.deepEqual(fetchOptions, [
{ credentials: 'omit', mode: 'cors' },
{ credentials: 'omit', mode: 'cors' },
]);
assert.deepEqual(config, {
origin: 'amt', group: '232.0.1.3', port: 1024,
source: '69.25.95.198', consume: 'bytes', discovery: 'driad',
origin: 'amt', group: '232.1.1.66', port: 8000,
source: '192.0.2.10', consume: 'bytes', discovery: 'driad',
});
await session.stop();
} finally {
Expand Down
Loading
Loading