Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parse dash manifest if segment template is out of representation #26

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 1 addition & 2 deletions src/manifests/handlers/dash/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ export default async function dashHandler(event: ALBEvent): Promise<ALBResult> {

try {
const originalDashManifestResponse = await fetch(url);
const responseCopy = originalDashManifestResponse.clone();
if (!originalDashManifestResponse.ok) {
return generateErrorResponse({
status: originalDashManifestResponse.status,
message: 'Unsuccessful Source Manifest fetch'
});
}
const reqQueryParams = new URLSearchParams(event.queryStringParameters);
const text = await responseCopy.text();
const text = await originalDashManifestResponse.text();
const dashUtils = dashManifestUtils();
const proxyManifest = dashUtils.createProxyDASHManifest(
text,
Expand Down
2 changes: 1 addition & 1 deletion src/manifests/handlers/dash/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
68 changes: 68 additions & 0 deletions src/manifests/utils/dashManifestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

Expand Down
113 changes: 48 additions & 65 deletions src/manifests/utils/dashManifestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
});
});
});

Expand All @@ -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;
}
});
}
}
36 changes: 24 additions & 12 deletions src/manifests/utils/hlsManifestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down Expand Up @@ -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')}`;
}

Expand All @@ -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;
}
});
}
38 changes: 22 additions & 16 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {};
Expand Down Expand Up @@ -126,19 +126,10 @@ export async function parseM3U8Text(res: Response): Promise<M3U> {
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) => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand Down
Loading