Skip to content

Commit

Permalink
improve dash parsin
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeffrey Brignoli committed May 5, 2023
1 parent 88c9bfa commit 7cbfe18
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 81 deletions.
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
80 changes: 78 additions & 2 deletions src/manifests/utils/dashManifestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down Expand Up @@ -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);
});
});
Expand Down
114 changes: 49 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,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;
}
});
}
}
16 changes: 5 additions & 11 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 @@ -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.
Expand All @@ -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}` : ''}`;
}

Expand Down

0 comments on commit 7cbfe18

Please sign in to comment.