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..26ee0c7 100644 --- a/src/manifests/utils/dashManifestUtils.test.ts +++ b/src/manifests/utils/dashManifestUtils.test.ts @@ -74,6 +74,74 @@ describe('dashManifestTools', () => { const expected: string = 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 = 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 = builder.buildObject(DASH_JSON); + expect(proxyManifest).toEqual(expected); + }); }); }); diff --git a/src/manifests/utils/dashManifestUtils.ts b/src/manifests/utils/dashManifestUtils.ts index b2d981c..ffb5371 100644 --- a/src/manifests/utils/dashManifestUtils.ts +++ b/src/manifests/utils/dashManifestUtils.ts @@ -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,33 @@ 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 = proxyPathBuilder( + mediaUrl?.match(/^http/) ? mediaUrl : new URL(mediaUrl, baseUrl).href, + urlQuery, + representation + ? 'proxy-segment/segment_$Number$.mp4' + : 'proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$', + false + ); + + // Initialization attr. + const initUrl = segmentTemplate.$.initialization; + if (!initUrl?.match(/^http/)) { + const absoluteInitUrl = new URL(initUrl, baseUrl).href; + segmentTemplate.$.initialization = absoluteInitUrl; + } + }); + } +} diff --git a/src/manifests/utils/hlsManifestUtils.ts b/src/manifests/utils/hlsManifestUtils.ts index 7cc0e33..831a6db 100644 --- a/src/manifests/utils/hlsManifestUtils.ts +++ b/src/manifests/utils/hlsManifestUtils.ts @@ -93,20 +93,22 @@ export default function (): HLSManifestTools { if (bitRate) { urlQuery.set('bitrate', bitRate); } - streamItem.set( - 'uri', - proxyPathBuilder(currentUri, urlQuery, 'proxy-media.m3u8') - ); + if (currentUri) + streamItem.set( + 'uri', + proxyPathBuilder(currentUri, urlQuery, 'proxy-media.m3u8') + ); return streamItem; }); // [Audio/Subtitles/IFrame] m3u.items.MediaItem = m3u.items.MediaItem.map((mediaItem) => { const currentUri = mediaItem.get('uri'); - mediaItem.set( - 'uri', - proxyPathBuilder(currentUri, originalUrlQuery, 'proxy-media.m3u8') - ); + if (currentUri) + mediaItem.set( + 'uri', + proxyPathBuilder(currentUri, originalUrlQuery, 'proxy-media.m3u8') + ); return mediaItem; }); @@ -137,8 +139,8 @@ export default function (): HLSManifestTools { for (let i = 0; i < m3u.items.PlaylistItem.length; i++) { const item = m3u.items.PlaylistItem[i]; const corruption = corruptions[i]; - let sourceSegURL: string = item.get('uri'); - if (!sourceSegURL.match(/^http/)) { + let sourceSegURL: string | null = item.get('uri'); + if (sourceSegURL && !sourceSegURL?.match(/^http/)) { sourceSegURL = `${sourceBaseURL}/${item.get('uri')}`; } @@ -153,11 +155,21 @@ export default function (): HLSManifestTools { proxyPathBuilder( item.get('uri'), new URLSearchParams(params), - '../../segments/proxy-segment' + '../../segments/proxy-segment', + false ) ); } - return m3u.toString(); + + // FIX EXT-X-MAP:URI not catching by @eyevinn/m3u8 to add baseUrl to the URI for (ZATOO manifest) + let data = m3u.toString(); + if (/#EXT-X-MAP:URI="(?!http).*"/.test(data)) { + data = data.replace( + /#EXT-X-MAP:URI="([^"]*)"/g, + `#EXT-X-MAP:URI="${sourceBaseURL}/$1"` + ); + } + return data; } }); } diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 4a8d763..ed7bae7 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) => { @@ -190,7 +181,7 @@ type ProxyBasenames = * @returns ex. [ "http://abc.origin.com/streams/vod1", "subfolder3/media/segment.ts" ] */ const cleanUpPathAndURI = (originPath: string, uri: string): string[] => { - const matchList: string[] | null = uri.match(/\.\.\//g); + const matchList: string[] | null = uri?.match(/\.\.\//g); if (matchList) { const jumpsToParentDir = matchList.length; if (jumpsToParentDir > 0) { @@ -212,27 +203,42 @@ const cleanUpPathAndURI = (originPath: string, uri: string): string[] => { export function proxyPathBuilder( itemUri: string, urlSearchParams: URLSearchParams, - proxy: ProxyBasenames + proxy: ProxyBasenames, + encoded = true ): string { if (!urlSearchParams) { return ''; } + const allQueries = new URLSearchParams(urlSearchParams); let sourceItemURL = ''; // Do not build an absolute source url If ItemUri is already an absolut url. - if (itemUri.match(/^http/)) { + if (itemUri?.match(/^http/)) { sourceItemURL = itemUri; } else { const sourceURL = allQueries.get('url'); const baseURL: string = path.dirname(sourceURL); const [_baseURL, _itemUri] = cleanUpPathAndURI(baseURL, itemUri); + sourceItemURL = `${_baseURL}/${_itemUri}`; } - if (sourceItemURL) { + if (encoded) { allQueries.set('url', sourceItemURL); + const allQueriesString = allQueries.toString(); + return `${proxy}${allQueriesString ? `?${allQueriesString}` : ''}`; } + allQueries.delete('url'); const allQueriesString = allQueries.toString(); - return `${proxy}${allQueriesString ? `?${allQueriesString}` : ''}`; + const [url, ...params] = sourceItemURL.split('?'); + const nestedParams = params.join('?'); + console.log( + `${proxy}?url=${url}${ + nestedParams ? encodeURIComponent(`?${nestedParams}`) : '' + }${allQueriesString ? `&${allQueriesString}` : ''}` + ); + return `${proxy}?url=${url}${ + nestedParams ? encodeURIComponent(`?${nestedParams}`) : '' + }${allQueriesString ? `&${allQueriesString}` : ''}`; } export function segmentUrlParamString( diff --git a/src/testvectors/dash/dash1_compressed/proxy-manifest.xml b/src/testvectors/dash/dash1_compressed/proxy-manifest.xml index a1f96a0..e66a1fd 100644 --- a/src/testvectors/dash/dash1_compressed/proxy-manifest.xml +++ b/src/testvectors/dash/dash1_compressed/proxy-manifest.xml @@ -34,7 +34,7 @@ + media="proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$?url=https://mock.mock.com/stream/relative_base/audiotrack/$RepresentationID$/$Time$.m4s&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D"> @@ -62,7 +62,7 @@ + media="proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$?url=https://mock.mock.com/stream/relative_base/videotrack/$RepresentationID$/$Time$.m4s&statusCode=%5B%7Bi%3A0%2Ccode%3A404%7D%2C%7Bi%3A2%2Ccode%3A401%7D%5D&timeout=%5B%7Bi%3A3%7D%5D&delay=%5B%7Bi%3A2%2Cms%3A2000%7D%5D"> diff --git a/src/testvectors/dash/dash1_multitrack/proxy-manifest.xml b/src/testvectors/dash/dash1_multitrack/proxy-manifest.xml index af6bc91..1b88eb4 100644 --- a/src/testvectors/dash/dash1_multitrack/proxy-manifest.xml +++ b/src/testvectors/dash/dash1_multitrack/proxy-manifest.xml @@ -3,28 +3,28 @@ - + - + - + - + @@ -35,7 +35,7 @@ - + @@ -64,7 +64,7 @@ - +