diff --git a/src/manifests/handlers/dash/index.ts b/src/manifests/handlers/dash/index.ts index 0ec8784..6f80961 100644 --- a/src/manifests/handlers/dash/index.ts +++ b/src/manifests/handlers/dash/index.ts @@ -20,7 +20,6 @@ export default async function dashHandler(event: ALBEvent): Promise { try { const originalDashManifestResponse = await fetch(url); - const responseCopy = originalDashManifestResponse.clone(); if (!originalDashManifestResponse.ok) { return generateErrorResponse({ status: originalDashManifestResponse.status, @@ -28,7 +27,7 @@ export default async function dashHandler(event: ALBEvent): Promise { }); } const reqQueryParams = new URLSearchParams(event.queryStringParameters); - const text = await responseCopy.text(); + const text = await originalDashManifestResponse.text(); const dashUtils = dashManifestUtils(); const proxyManifest = dashUtils.createProxyDASHManifest( text, diff --git a/src/manifests/handlers/dash/segment.ts b/src/manifests/handlers/dash/segment.ts index 347e040..8b0813f 100644 --- a/src/manifests/handlers/dash/segment.ts +++ b/src/manifests/handlers/dash/segment.ts @@ -68,7 +68,7 @@ export default async function dashSegmentHandler( allMutations ); const segUrl = new URL(segmentUrl); - const cleanSegUrl = segUrl.origin + segUrl.pathname; + const cleanSegUrl = segUrl.origin + segUrl.pathname + segUrl.search; let eventParamsString: string; if (mergedMaps.size < 1) { eventParamsString = `url=${cleanSegUrl}`; diff --git a/src/manifests/utils/dashManifestUtils.test.ts b/src/manifests/utils/dashManifestUtils.test.ts index fe18d32..538d600 100644 --- a/src/manifests/utils/dashManifestUtils.test.ts +++ b/src/manifests/utils/dashManifestUtils.test.ts @@ -37,7 +37,45 @@ describe('dashManifestTools', () => { parser.parseString(dashFile, function (err, result) { DASH_JSON = result; }); - const expected: string = builder.buildObject(DASH_JSON); + const expected: string = decodeURIComponent( + builder.buildObject(DASH_JSON) + ); + expect(proxyManifest).toEqual(expected); + }); + + it('should replace initialization urls & media urls in compressed dash manifest with base urls, with absolute source url & proxy url with query parameters respectively', async () => { + // Arrange + const mockManifestPath = + '../../testvectors/dash/dash1_compressed/manifest.xml'; + const mockDashManifest = fs.readFileSync( + path.join(__dirname, mockManifestPath), + 'utf8' + ); + const queryString = + 'url=https://mock.mock.com/stream/manifest.mpd&statusCode=[{i:0,code:404},{i:2,code:401}]&timeout=[{i:3}]&delay=[{i:2,ms:2000}]'; + const urlSearchParams = new URLSearchParams(queryString); + // Act + const manifestUtils = dashManifestUtils(); + const proxyManifest: string = manifestUtils.createProxyDASHManifest( + mockDashManifest, + urlSearchParams + ); + // Assert + const parser = new xml2js.Parser(); + const builder = new xml2js.Builder(); + const proxyManifestPath = + '../../testvectors/dash/dash1_compressed/proxy-manifest.xml'; + const dashFile: string = fs.readFileSync( + path.join(__dirname, proxyManifestPath), + 'utf8' + ); + let DASH_JSON; + parser.parseString(dashFile, function (err, result) { + DASH_JSON = result; + }); + const expected: string = decodeURIComponent( + builder.buildObject(DASH_JSON) + ); expect(proxyManifest).toEqual(expected); }); @@ -71,7 +109,45 @@ describe('dashManifestTools', () => { parser.parseString(dashFile, function (err, result) { DASH_JSON = result; }); - const expected: string = builder.buildObject(DASH_JSON); + const expected: string = decodeURIComponent( + builder.buildObject(DASH_JSON) + ); + expect(proxyManifest).toEqual(expected); + }); + + it('should replace initialization urls & media urls in compressed dash manifest with base urls, with absolute source url & proxy url with query parameters respectively', async () => { + // Arrange + const mockManifestPath = + '../../testvectors/dash/dash1_compressed/manifest.xml'; + const mockDashManifest = fs.readFileSync( + path.join(__dirname, mockManifestPath), + 'utf8' + ); + const queryString = + 'url=https://mock.mock.com/stream/manifest.mpd&statusCode=[{i:0,code:404},{i:2,code:401}]&timeout=[{i:3}]&delay=[{i:2,ms:2000}]'; + const urlSearchParams = new URLSearchParams(queryString); + // Act + const manifestUtils = dashManifestUtils(); + const proxyManifest: string = manifestUtils.createProxyDASHManifest( + mockDashManifest, + urlSearchParams + ); + // Assert + const parser = new xml2js.Parser(); + const builder = new xml2js.Builder(); + const proxyManifestPath = + '../../testvectors/dash/dash1_compressed/proxy-manifest.xml'; + const dashFile: string = fs.readFileSync( + path.join(__dirname, proxyManifestPath), + 'utf8' + ); + let DASH_JSON; + parser.parseString(dashFile, function (err, result) { + DASH_JSON = result; + }); + const expected: string = decodeURIComponent( + builder.buildObject(DASH_JSON) + ); expect(proxyManifest).toEqual(expected); }); }); diff --git a/src/manifests/utils/dashManifestUtils.ts b/src/manifests/utils/dashManifestUtils.ts index b2d981c..18e26dc 100644 --- a/src/manifests/utils/dashManifestUtils.ts +++ b/src/manifests/utils/dashManifestUtils.ts @@ -5,15 +5,15 @@ import { proxyPathBuilder } from '../../shared/utils'; interface DASHManifestUtils { mergeMap: ( - segmentListSize: number, - configsMap: IndexedCorruptorConfigMap + segmentListSize: number, + configsMap: IndexedCorruptorConfigMap ) => CorruptorConfigMap; } export interface DASHManifestTools { createProxyDASHManifest: ( - dashManifestText: string, - originalUrlQuery: URLSearchParams + dashManifestText: string, + originalUrlQuery: URLSearchParams ) => Manifest; // look def again utils: DASHManifestUtils; } @@ -21,8 +21,8 @@ export interface DASHManifestTools { export default function (): DASHManifestTools { const utils = { mergeMap( - targetSegmentIndex: number, - configsMap: IndexedCorruptorConfigMap + targetSegmentIndex: number, + configsMap: IndexedCorruptorConfigMap ): CorruptorConfigMap { const outputMap = new Map(); const d = configsMap.get('*'); @@ -51,8 +51,8 @@ export default function (): DASHManifestTools { return { utils, createProxyDASHManifest( - dashManifestText: string, - originalUrlQuery: URLSearchParams + dashManifestText: string, + originalUrlQuery: URLSearchParams ): string { const parser = new xml2js.Parser(); const builder = new xml2js.Builder(); @@ -65,77 +65,30 @@ export default function (): DASHManifestTools { let baseUrl; if (DASH_JSON.MPD.BaseURL) { // There should only ever be one baseurl according to schema - baseUrl = DASH_JSON.MPD.BaseURL[0]; + baseUrl = DASH_JSON.MPD.BaseURL[0].match(/^http/) + ? DASH_JSON.MPD.BaseURL[0] + : new URL(DASH_JSON.MPD.BaseURL[0], originalUrlQuery.get('url')).href; // Remove base url from manifest since we are using relative paths for proxy DASH_JSON.MPD.BaseURL = []; - } + } else baseUrl = originalUrlQuery.get('url'); DASH_JSON.MPD.Period.map((period) => { period.AdaptationSet.map((adaptationSet) => { - if (adaptationSet.SegmentTemplate) { - // There should only be one segment template with this format - const segmentTemplate = adaptationSet.SegmentTemplate[0]; - - // Media attr - const mediaUrl = segmentTemplate.$.media; - // Clone params to avoid mutating input argument - const urlQuery = new URLSearchParams(originalUrlQuery); - - segmentTemplate.$.media = proxyPathBuilder( - mediaUrl.match(/^http/) ? mediaUrl : baseUrl + mediaUrl, - urlQuery, - 'proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$' + if (adaptationSet.SegmentTemplate) + forgeSegment( + baseUrl, + adaptationSet.SegmentTemplate, + originalUrlQuery ); - // Initialization attr. - const initUrl = segmentTemplate.$.initialization; - if (!initUrl.match(/^http/)) { - try { - // Use original query url if baseUrl is undefined, combine if relative, or use just baseUrl if its absolute - if (!baseUrl) { - baseUrl = originalUrlQuery.get('url'); - } else if (!baseUrl.match(/^http/)) { - baseUrl = new URL(baseUrl, originalUrlQuery.get('url')).href; - } - const absoluteInitUrl = new URL(initUrl, baseUrl).href; - segmentTemplate.$.initialization = absoluteInitUrl; - } catch (e) { - throw new Error(e); - } - } - } else { - // Uses segment ids - adaptationSet.Representation.map((representation) => { - if (representation.SegmentTemplate) { - representation.SegmentTemplate.map((segmentTemplate) => { - // Media attr. - const mediaUrl = segmentTemplate.$.media; - // Clone params to avoid mutating input argument - const urlQuery = new URLSearchParams(originalUrlQuery); - if (representation.$.bandwidth) { - urlQuery.set('bitrate', representation.$.bandwidth); - } - - segmentTemplate.$.media = proxyPathBuilder( - mediaUrl, - urlQuery, - 'proxy-segment/segment_$Number$.mp4' - ); - // Initialization attr. - const masterDashUrl = originalUrlQuery.get('url'); - const initUrl = segmentTemplate.$.initialization; - if (!initUrl.match(/^http/)) { - try { - const absoluteInitUrl = new URL(initUrl, masterDashUrl) - .href; - segmentTemplate.$.initialization = absoluteInitUrl; - } catch (e) { - throw new Error(e); - } - } - }); - } - }); - } + adaptationSet.Representation.map((representation) => { + if (representation.SegmentTemplate) + forgeSegment( + baseUrl, + representation.SegmentTemplate, + originalUrlQuery, + representation + ); + }); }); }); @@ -145,3 +98,34 @@ export default function (): DASHManifestTools { } }; } + +function forgeSegment(baseUrl, segment, originalUrlQuery, representation?) { + if (segment) { + segment.map((segmentTemplate) => { + // Media attr. + const mediaUrl = segmentTemplate.$.media; + + // Clone params to avoid mutating input argument + const urlQuery = new URLSearchParams(originalUrlQuery); + if (representation?.$?.bandwidth) + urlQuery.set('bitrate', representation.$.bandwidth); + + segmentTemplate.$.media = decodeURIComponent( + proxyPathBuilder( + mediaUrl.match(/^http/) ? mediaUrl : new URL(mediaUrl, baseUrl).href, + urlQuery, + representation + ? 'proxy-segment/segment_$Number$.mp4' + : 'proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$' + ) + ); + + // Initialization attr. + const initUrl = segmentTemplate.$.initialization; + if (!initUrl?.match(/^http/)) { + const absoluteInitUrl = new URL(initUrl, baseUrl).href; + segmentTemplate.$.initialization = absoluteInitUrl; + } + }); + } +} diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 4a8d763..c466816 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -86,9 +86,9 @@ export async function composeALBEvent( } // Create ALBEvent from Fastify Request... - const [path, queryString] = url.split('?'); + const [path, ...queryString] = url.split('?'); const queryStringParameters = Object.fromEntries( - new URLSearchParams(queryString) + new URLSearchParams(decodeURI(queryString.join('?').replace(/amp;/g, ''))) ); const requestContext = { elb: { targetGroupArn: '' } }; const headers: Record = {}; @@ -126,19 +126,10 @@ export async function parseM3U8Text(res: Response): Promise { We set PLAYLIST-TYPE here if that is the case to ensure, that 'm3u.toString()' will later return a m3u8 string with the endlist tag. */ - let setPlaylistTypeToVod = false; const parser = m3u8.createStream(); - const responseCopy = res.clone(); - const m3u8String = await responseCopy.text(); - if (m3u8String.indexOf('#EXT-X-ENDLIST') !== -1) { - setPlaylistTypeToVod = true; - } res.body.pipe(parser); return new Promise((resolve, reject) => { parser.on('m3u', (m3u: M3U) => { - if (setPlaylistTypeToVod && m3u.get('playlistType') !== 'VOD') { - m3u.set('playlistType', 'VOD'); - } resolve(m3u); }); parser.on('error', (err) => { @@ -217,6 +208,7 @@ export function proxyPathBuilder( if (!urlSearchParams) { return ''; } + const allQueries = new URLSearchParams(urlSearchParams); let sourceItemURL = ''; // Do not build an absolute source url If ItemUri is already an absolut url. @@ -226,12 +218,14 @@ export function proxyPathBuilder( const sourceURL = allQueries.get('url'); const baseURL: string = path.dirname(sourceURL); const [_baseURL, _itemUri] = cleanUpPathAndURI(baseURL, itemUri); + sourceItemURL = `${_baseURL}/${_itemUri}`; } if (sourceItemURL) { allQueries.set('url', sourceItemURL); } const allQueriesString = allQueries.toString(); + return `${proxy}${allQueriesString ? `?${allQueriesString}` : ''}`; }