Skip to content

Instantly share code, notes, and snippets.

@jasonbyrne
Created January 3, 2019 20:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonbyrne/d0c42365cb8bcc5855ba5e5353995cd5 to your computer and use it in GitHub Desktop.
Save jasonbyrne/d0c42365cb8bcc5855ba5e5353995cd5 to your computer and use it in GitHub Desktop.
HLS Checker Lambda - Loads an HLS playlist and then all of the underlying chunklists and segments to make sure all of the files exist and basic validity checks.
const HLS = require('hls-parser');
const request = require('request');
const URL = require('url').URL
// Error codes
const ERROR = {
INVALID_PLAYLIST_URL: {
code: 100,
message: 'Invalid playlist URL'
},
HLS_PLAYLIST_PARSE_ERROR: {
code: 101,
message: 'HLS Parser reported invalid format of the playlist.'
},
HLS_CHUNKLIST_PARSE_ERROR: {
code: 102,
message: 'HLS Parser reported invalid format of the chunklist.'
},
NO_VARIANTS_FOUND: {
code: 103,
message: 'Playlist did not have any variants (renditions).'
},
NO_SEGMENTS_FOUND: {
code: 104,
message: 'A chunklist did not have any segments.'
},
PLAYLIST_LOAD_FAILED: {
code: 105,
message: 'The playlist file failed to load.'
},
CHUKLIST_LOAD_FAILED: {
code: 106,
message: 'A chunklist file failed to load.'
},
SEGMENT_LOAD_FAILED: {
code: 107,
message: 'A segment file failed to load.'
},
UNKNOWN_EXCEPTION: {
code: 199,
message: 'Non-bucketed error was caught.'
}
};
// Custom Exception Class
function HLS_Checker_Exception(err, context) {
err.context = context;
return err;
}
// Handle API call
exports.handler = async (event, context, callback) => {
const playlistUrl = event.queryStringParameters.url;
// This will get populated later to contain all files loaded
let files = [];
// Baseline response object
let response = {
request: {
playlistUri: playlistUrl,
started: (new Date().toISOString()),
finished: null,
allGood: false
},
error: null,
files: []
};
// Response creator
function HLS_Checker_Response(statusCode) {
response.files = files;
response.request.finished = (new Date().toISOString());
return {
statusCode: statusCode,
body: JSON.stringify(response, null, 2)
}
}
// Just bail out if there is no playlist url
if (!playlistUrl) {
response.error = new HLS_Checker_Exception(ERROR.INVALID_PLAYLIST_URL, 'URL value is : ' + playlistUrl);
return new HLS_Checker_Response(400);
}
function tsExists(uri) {
return new Promise((resolve, reject) => {
request(uri, { method: 'HEAD' }, function (err, res, body) {
if (err || res.statusCode < 200 || res.statusCode > 299) {
files.push({
uri: uri,
type: 'segment',
loaded: false
});
}
else {
files.push({
uri: uri,
type: 'segment',
loaded: true
});
resolve();
}
// We'll resolve on pass or fail, failed load gets handled later
resolve();
});
});
}
function getChunklist(chunklistUrl) {
return new Promise((resolve, reject) => {
request(chunklistUrl, function (err, res, body) {
// If error get out
if (err || res.statusCode < 200 || res.statusCode > 299) {
files.push({
uri: chunklistUrl,
type: 'chunklist',
loaded: false
});
// We'll resolve on pass or fail, failed load gets handled later
return resolve();
}
// Got response so parse playlist
let chunklist;
try {
chunklist = HLS.parse(body);
} catch (err) {
return reject(
new HLS_Checker_Exception(
ERROR.HLS_CHUNKLIST_PARSE_ERROR,
'Error parsing chunklist ' + chunklistUrl + '. ERROR = ' + err + ' | CONTENT = ' + body
)
);
}
files.push({
uri: chunklistUrl,
type: 'chunklist',
loaded: true
});
let promises = [];
let segments = [];
if (chunklist.segments && Array.isArray(chunklist.segments) && chunklist.segments.length > 0) {
chunklist.segments.forEach(function (segment) {
let uri = new URL(segment.uri, chunklistUrl).href;
if (segments.indexOf(uri) < 0) {
segments.push(uri);
promises.push(tsExists(uri));
}
});
}
else {
return reject(
new HLS_Checker_Exception(
ERROR.NO_SEGMENTS_FOUND,
'No segments found in ' + chunklistUrl + '. CONTENT = ' + body
)
);
}
Promise.all(promises).then(function () {
resolve(segments);
}).catch(function (err) {
reject(
new HLS_Checker_Exception(
ERROR.UNKNOWN_EXCEPTION,
'Error on chunklist: ' + chunklistUrl + '. ERROR = ' + err
)
);
});
});
});
}
function getPlaylist(playlistUrl) {
return new Promise((resolve, reject) => {
request(playlistUrl, function (err, res, body) {
// If error get out
if (err || res.statusCode < 200 || res.statusCode > 299) {
files.push({
uri: playlistUrl,
type: 'playlist',
loaded: false
});
// We'll resolve on pass or fail, failed load gets handled later
return resolve();
}
// Fixing some errors on the fly that don't cause failure
body = body.replace(/,AUDIO="audio-0"/g, '');
// Got response so parse playlist
let playlist;
try {
playlist = HLS.parse(body);
} catch (err) {
return reject(
new HLS_Checker_Exception(
ERROR.HLS_PLAYLIST_PARSE_ERROR,
'Error parsing playlist ' + playlistUrl + '. ERROR = ' + err + ' | CONTENT = ' + body
)
);
}
files.push({
uri: playlistUrl,
type: 'playlist',
loaded: true
});
let promises = [];
if (playlist.variants && Array.isArray(playlist.variants) && playlist.variants.length > 0) {
playlist.variants.forEach(function (variant) {
promises.push(getChunklist(new URL(variant.uri, playlistUrl).href));
});
}
else {
return reject(
new HLS_Checker_Exception(
ERROR.NO_VARIANTS_FOUND,
'No renditions found in ' + playlistUrl + '. CONTENT = ' + body
)
);
}
Promise.all(promises).then(function () {
resolve();
}).catch(function (err) {
reject(
new HLS_Checker_Exception(
ERROR.UNKNOWN_EXCEPTION,
'Error on playlist: ' + playlistUrl + '. ERROR = ' + err
)
);
});
});
});
}
await getPlaylist(playlistUrl).then(function () {
response.request.allGood = (function () {
let isGood = true;
files.some(function (file) {
if (!file.loaded) {
isGood = false;
let err = (
file.type == 'segment' ?
ERROR.SEGMENT_LOAD_FAILED :
file.type == 'chunklist' ?
ERROR.SEGMENT_LOAD_FAILED :
ERROR.PLAYLIST_LOAD_FAILED
);
response.error = new HLS_Checker_Exception(
err,
'Error loading: ' + file.uri
);
return true;
}
});
return isGood;
})();
}).catch(function (exception) {
response.error = exception;
});
return new HLS_Checker_Response(200);
};
{
"name": "hls-checker",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jason Byrne <jason.byrne@flosports.tv>",
"license": "ISC",
"dependencies": {
"hls-parser": "^0.2.4",
"request": "^2.88.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment