Skip to content

Commit 9431fc1

Browse files
author
Jeffrey Brignoli
committed
improve dash
1 parent 88c9bfa commit 9431fc1

File tree

5 files changed

+142
-89
lines changed

5 files changed

+142
-89
lines changed

src/manifests/handlers/dash/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@ export default async function dashHandler(event: ALBEvent): Promise<ALBResult> {
2020

2121
try {
2222
const originalDashManifestResponse = await fetch(url);
23-
const responseCopy = originalDashManifestResponse.clone();
2423
if (!originalDashManifestResponse.ok) {
2524
return generateErrorResponse({
2625
status: originalDashManifestResponse.status,
2726
message: 'Unsuccessful Source Manifest fetch'
2827
});
2928
}
3029
const reqQueryParams = new URLSearchParams(event.queryStringParameters);
31-
const text = await responseCopy.text();
30+
const text = await originalDashManifestResponse.text();
3231
const dashUtils = dashManifestUtils();
3332
const proxyManifest = dashUtils.createProxyDASHManifest(
3433
text,

src/manifests/handlers/dash/segment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default async function dashSegmentHandler(
6868
allMutations
6969
);
7070
const segUrl = new URL(segmentUrl);
71-
const cleanSegUrl = segUrl.origin + segUrl.pathname;
71+
const cleanSegUrl = segUrl.origin + segUrl.pathname + segUrl.search;
7272
let eventParamsString: string;
7373
if (mergedMaps.size < 1) {
7474
eventParamsString = `url=${cleanSegUrl}`;

src/manifests/utils/dashManifestUtils.test.ts

+78-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,45 @@ describe('dashManifestTools', () => {
3737
parser.parseString(dashFile, function (err, result) {
3838
DASH_JSON = result;
3939
});
40-
const expected: string = builder.buildObject(DASH_JSON);
40+
const expected: string = decodeURIComponent(
41+
builder.buildObject(DASH_JSON)
42+
);
43+
expect(proxyManifest).toEqual(expected);
44+
});
45+
46+
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 () => {
47+
// Arrange
48+
const mockManifestPath =
49+
'../../testvectors/dash/dash1_compressed/manifest.xml';
50+
const mockDashManifest = fs.readFileSync(
51+
path.join(__dirname, mockManifestPath),
52+
'utf8'
53+
);
54+
const queryString =
55+
'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}]';
56+
const urlSearchParams = new URLSearchParams(queryString);
57+
// Act
58+
const manifestUtils = dashManifestUtils();
59+
const proxyManifest: string = manifestUtils.createProxyDASHManifest(
60+
mockDashManifest,
61+
urlSearchParams
62+
);
63+
// Assert
64+
const parser = new xml2js.Parser();
65+
const builder = new xml2js.Builder();
66+
const proxyManifestPath =
67+
'../../testvectors/dash/dash1_compressed/proxy-manifest.xml';
68+
const dashFile: string = fs.readFileSync(
69+
path.join(__dirname, proxyManifestPath),
70+
'utf8'
71+
);
72+
let DASH_JSON;
73+
parser.parseString(dashFile, function (err, result) {
74+
DASH_JSON = result;
75+
});
76+
const expected: string = decodeURIComponent(
77+
builder.buildObject(DASH_JSON)
78+
);
4179
expect(proxyManifest).toEqual(expected);
4280
});
4381

@@ -71,7 +109,45 @@ describe('dashManifestTools', () => {
71109
parser.parseString(dashFile, function (err, result) {
72110
DASH_JSON = result;
73111
});
74-
const expected: string = builder.buildObject(DASH_JSON);
112+
const expected: string = decodeURIComponent(
113+
builder.buildObject(DASH_JSON)
114+
);
115+
expect(proxyManifest).toEqual(expected);
116+
});
117+
118+
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 () => {
119+
// Arrange
120+
const mockManifestPath =
121+
'../../testvectors/dash/dash1_compressed/manifest.xml';
122+
const mockDashManifest = fs.readFileSync(
123+
path.join(__dirname, mockManifestPath),
124+
'utf8'
125+
);
126+
const queryString =
127+
'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}]';
128+
const urlSearchParams = new URLSearchParams(queryString);
129+
// Act
130+
const manifestUtils = dashManifestUtils();
131+
const proxyManifest: string = manifestUtils.createProxyDASHManifest(
132+
mockDashManifest,
133+
urlSearchParams
134+
);
135+
// Assert
136+
const parser = new xml2js.Parser();
137+
const builder = new xml2js.Builder();
138+
const proxyManifestPath =
139+
'../../testvectors/dash/dash1_compressed/proxy-manifest.xml';
140+
const dashFile: string = fs.readFileSync(
141+
path.join(__dirname, proxyManifestPath),
142+
'utf8'
143+
);
144+
let DASH_JSON;
145+
parser.parseString(dashFile, function (err, result) {
146+
DASH_JSON = result;
147+
});
148+
const expected: string = decodeURIComponent(
149+
builder.buildObject(DASH_JSON)
150+
);
75151
expect(proxyManifest).toEqual(expected);
76152
});
77153
});

src/manifests/utils/dashManifestUtils.ts

+57-73
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@ import { proxyPathBuilder } from '../../shared/utils';
55

66
interface DASHManifestUtils {
77
mergeMap: (
8-
segmentListSize: number,
9-
configsMap: IndexedCorruptorConfigMap
8+
segmentListSize: number,
9+
configsMap: IndexedCorruptorConfigMap
1010
) => CorruptorConfigMap;
1111
}
1212

1313
export interface DASHManifestTools {
1414
createProxyDASHManifest: (
15-
dashManifestText: string,
16-
originalUrlQuery: URLSearchParams
15+
dashManifestText: string,
16+
originalUrlQuery: URLSearchParams
1717
) => Manifest; // look def again
1818
utils: DASHManifestUtils;
1919
}
2020

2121
export default function (): DASHManifestTools {
2222
const utils = {
2323
mergeMap(
24-
targetSegmentIndex: number,
25-
configsMap: IndexedCorruptorConfigMap
24+
targetSegmentIndex: number,
25+
configsMap: IndexedCorruptorConfigMap
2626
): CorruptorConfigMap {
2727
const outputMap = new Map();
2828
const d = configsMap.get('*');
@@ -51,8 +51,8 @@ export default function (): DASHManifestTools {
5151
return {
5252
utils,
5353
createProxyDASHManifest(
54-
dashManifestText: string,
55-
originalUrlQuery: URLSearchParams
54+
dashManifestText: string,
55+
originalUrlQuery: URLSearchParams
5656
): string {
5757
const parser = new xml2js.Parser();
5858
const builder = new xml2js.Builder();
@@ -65,77 +65,30 @@ export default function (): DASHManifestTools {
6565
let baseUrl;
6666
if (DASH_JSON.MPD.BaseURL) {
6767
// There should only ever be one baseurl according to schema
68-
baseUrl = DASH_JSON.MPD.BaseURL[0];
68+
baseUrl = DASH_JSON.MPD.BaseURL[0].match(/^http/)
69+
? DASH_JSON.MPD.BaseURL[0]
70+
: new URL(DASH_JSON.MPD.BaseURL[0], originalUrlQuery.get('url')).href;
6971
// Remove base url from manifest since we are using relative paths for proxy
7072
DASH_JSON.MPD.BaseURL = [];
71-
}
73+
} else baseUrl = originalUrlQuery.get('url');
7274

7375
DASH_JSON.MPD.Period.map((period) => {
7476
period.AdaptationSet.map((adaptationSet) => {
75-
if (adaptationSet.SegmentTemplate) {
76-
// There should only be one segment template with this format
77-
const segmentTemplate = adaptationSet.SegmentTemplate[0];
78-
79-
// Media attr
80-
const mediaUrl = segmentTemplate.$.media;
81-
// Clone params to avoid mutating input argument
82-
const urlQuery = new URLSearchParams(originalUrlQuery);
83-
84-
segmentTemplate.$.media = proxyPathBuilder(
85-
mediaUrl.match(/^http/) ? mediaUrl : baseUrl + mediaUrl,
86-
urlQuery,
87-
'proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$'
77+
if (adaptationSet.SegmentTemplate)
78+
forgeSegment(
79+
baseUrl,
80+
adaptationSet.SegmentTemplate,
81+
originalUrlQuery
8882
);
89-
// Initialization attr.
90-
const initUrl = segmentTemplate.$.initialization;
91-
if (!initUrl.match(/^http/)) {
92-
try {
93-
// Use original query url if baseUrl is undefined, combine if relative, or use just baseUrl if its absolute
94-
if (!baseUrl) {
95-
baseUrl = originalUrlQuery.get('url');
96-
} else if (!baseUrl.match(/^http/)) {
97-
baseUrl = new URL(baseUrl, originalUrlQuery.get('url')).href;
98-
}
99-
const absoluteInitUrl = new URL(initUrl, baseUrl).href;
100-
segmentTemplate.$.initialization = absoluteInitUrl;
101-
} catch (e) {
102-
throw new Error(e);
103-
}
104-
}
105-
} else {
106-
// Uses segment ids
107-
adaptationSet.Representation.map((representation) => {
108-
if (representation.SegmentTemplate) {
109-
representation.SegmentTemplate.map((segmentTemplate) => {
110-
// Media attr.
111-
const mediaUrl = segmentTemplate.$.media;
112-
// Clone params to avoid mutating input argument
113-
const urlQuery = new URLSearchParams(originalUrlQuery);
114-
if (representation.$.bandwidth) {
115-
urlQuery.set('bitrate', representation.$.bandwidth);
116-
}
117-
118-
segmentTemplate.$.media = proxyPathBuilder(
119-
mediaUrl,
120-
urlQuery,
121-
'proxy-segment/segment_$Number$.mp4'
122-
);
123-
// Initialization attr.
124-
const masterDashUrl = originalUrlQuery.get('url');
125-
const initUrl = segmentTemplate.$.initialization;
126-
if (!initUrl.match(/^http/)) {
127-
try {
128-
const absoluteInitUrl = new URL(initUrl, masterDashUrl)
129-
.href;
130-
segmentTemplate.$.initialization = absoluteInitUrl;
131-
} catch (e) {
132-
throw new Error(e);
133-
}
134-
}
135-
});
136-
}
137-
});
138-
}
83+
adaptationSet.Representation.map((representation) => {
84+
if (representation.SegmentTemplate)
85+
forgeSegment(
86+
baseUrl,
87+
representation.SegmentTemplate,
88+
originalUrlQuery,
89+
representation
90+
);
91+
});
13992
});
14093
});
14194

@@ -145,3 +98,34 @@ export default function (): DASHManifestTools {
14598
}
14699
};
147100
}
101+
102+
function forgeSegment(baseUrl, segment, originalUrlQuery, representation?) {
103+
if (segment) {
104+
segment.map((segmentTemplate) => {
105+
// Media attr.
106+
const mediaUrl = segmentTemplate.$.media;
107+
108+
// Clone params to avoid mutating input argument
109+
const urlQuery = new URLSearchParams(originalUrlQuery);
110+
if (representation?.$?.bandwidth)
111+
urlQuery.set('bitrate', representation.$.bandwidth);
112+
113+
segmentTemplate.$.media = decodeURIComponent(
114+
proxyPathBuilder(
115+
mediaUrl.match(/^http/) ? mediaUrl : new URL(mediaUrl, baseUrl).href,
116+
urlQuery,
117+
representation
118+
? 'proxy-segment/segment_$Number$.mp4'
119+
: 'proxy-segment/segment_$Number$_$RepresentationID$_$Bandwidth$'
120+
)
121+
);
122+
123+
// Initialization attr.
124+
const initUrl = segmentTemplate.$.initialization;
125+
if (!initUrl?.match(/^http/)) {
126+
const absoluteInitUrl = new URL(initUrl, baseUrl).href;
127+
segmentTemplate.$.initialization = absoluteInitUrl;
128+
}
129+
});
130+
}
131+
}

src/shared/utils.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ export async function composeALBEvent(
8686
}
8787

8888
// Create ALBEvent from Fastify Request...
89-
const [path, queryString] = url.split('?');
89+
const [path, ...queryString] = url.split('?');
9090
const queryStringParameters = Object.fromEntries(
91-
new URLSearchParams(queryString)
91+
new URLSearchParams(decodeURI(queryString.join('?').replace(/amp;/g, '')))
9292
);
9393
const requestContext = { elb: { targetGroupArn: '' } };
9494
const headers: Record<string, string> = {};
@@ -126,19 +126,10 @@ export async function parseM3U8Text(res: Response): Promise<M3U> {
126126
We set PLAYLIST-TYPE here if that is the case to ensure,
127127
that 'm3u.toString()' will later return a m3u8 string with the endlist tag.
128128
*/
129-
let setPlaylistTypeToVod = false;
130129
const parser = m3u8.createStream();
131-
const responseCopy = res.clone();
132-
const m3u8String = await responseCopy.text();
133-
if (m3u8String.indexOf('#EXT-X-ENDLIST') !== -1) {
134-
setPlaylistTypeToVod = true;
135-
}
136130
res.body.pipe(parser);
137131
return new Promise((resolve, reject) => {
138132
parser.on('m3u', (m3u: M3U) => {
139-
if (setPlaylistTypeToVod && m3u.get('playlistType') !== 'VOD') {
140-
m3u.set('playlistType', 'VOD');
141-
}
142133
resolve(m3u);
143134
});
144135
parser.on('error', (err) => {
@@ -217,6 +208,7 @@ export function proxyPathBuilder(
217208
if (!urlSearchParams) {
218209
return '';
219210
}
211+
220212
const allQueries = new URLSearchParams(urlSearchParams);
221213
let sourceItemURL = '';
222214
// Do not build an absolute source url If ItemUri is already an absolut url.
@@ -226,12 +218,14 @@ export function proxyPathBuilder(
226218
const sourceURL = allQueries.get('url');
227219
const baseURL: string = path.dirname(sourceURL);
228220
const [_baseURL, _itemUri] = cleanUpPathAndURI(baseURL, itemUri);
221+
229222
sourceItemURL = `${_baseURL}/${_itemUri}`;
230223
}
231224
if (sourceItemURL) {
232225
allQueries.set('url', sourceItemURL);
233226
}
234227
const allQueriesString = allQueries.toString();
228+
235229
return `${proxy}${allQueriesString ? `?${allQueriesString}` : ''}`;
236230
}
237231

0 commit comments

Comments
 (0)