sync/src/custom-media.js

163 lines
4.7 KiB
JavaScript
Raw Normal View History

import { ValidationError } from './errors';
import { parse as parseURL } from 'url';
import net from 'net';
import Media from './media';
import { hash } from './util/hash';
const SOURCE_QUALITIES = new Set([
240,
360,
480,
540,
720,
1080,
1440,
2160
]);
const SOURCE_CONTENT_TYPES = new Set([
'application/x-mpegURL',
'audio/aac',
'audio/ogg',
'audio/mpeg',
'video/mp4',
'video/ogg',
'video/webm'
]);
export function convert(data) {
validate(data);
if (data.live) data.duration = 0;
const sources = {};
for (let source of data.sources) {
if (!sources.hasOwnProperty(source.quality))
sources[source.quality] = [];
sources[source.quality].push({
link: source.url,
contentType: source.contentType,
quality: source.quality
});
}
const meta = {
direct: sources,
textTracks: data.textTracks,
thumbnail: data.thumbnail, // Currently ignored by Media
live: !!data.live // Currently ignored by Media
};
const id = hash('sha256', JSON.stringify([
data.title,
data.duration,
meta
]), 'base64');
return new Media(id, data.title, data.duration, 'cm', meta);
}
export function validate(data) {
if (typeof data.title !== 'string')
throw new ValidationError('title must be a string');
if (!data.title)
throw new ValidationError('title must not be blank');
if (typeof data.duration !== 'number')
throw new ValidationError('duration must be a number');
if (!isFinite(data.duration) || data.duration < 0)
throw new ValidationError('duration must be a non-negative finite number');
if (data.hasOwnProperty('live') && typeof data.live !== 'boolean')
throw new ValidationError('live must be a boolean');
if (data.hasOwnProperty('thumbnail')) {
if (typeof data.thumbnail !== 'string')
throw new ValidationError('thumbnail must be a string');
validateURL(data.thumbnail);
}
validateSources(data.sources);
validateTextTracks(data.textTracks);
}
function validateSources(sources) {
if (!Array.isArray(sources))
throw new ValidationError('sources must be a list');
if (sources.length === 0)
throw new ValidationError('source list must be nonempty');
for (let source of sources) {
if (typeof source.url !== 'string')
throw new ValidationError('source URL must be a string');
validateURL(source.url);
if (!SOURCE_CONTENT_TYPES.has(source.contentType))
throw new ValidationError(
`unacceptable source contentType "${source.contentType}"`
);
if (!SOURCE_QUALITIES.has(source.quality))
throw new ValidationError(`unacceptable source quality "${source.quality}"`);
if (source.hasOwnProperty('bitrate')) {
if (typeof source.bitrate !== 'number')
throw new ValidationError('source bitrate must be a number');
if (!isFinite(source.bitrate) || source.bitrate < 0)
throw new ValidationError(
'source bitrate must be a non-negative finite number'
);
}
}
}
function validateTextTracks(textTracks) {
if (typeof textTracks === 'undefined') {
return;
}
if (!Array.isArray(textTracks))
throw new ValidationError('textTracks must be a list');
for (let track of textTracks) {
if (typeof track.url !== 'string')
throw new ValidationError('text track URL must be a string');
validateURL(track.url);
if (track.contentType !== 'text/vtt')
throw new ValidationError(
`unacceptable text track contentType "${track.contentType}"`
);
if (typeof track.name !== 'string')
throw new ValidationError('text track name must be a string');
if (!track.name)
throw new ValidationError('text track name must be nonempty');
}
}
function validateURL(urlstring) {
let url;
try {
url = parseURL(urlstring);
// legacy url.parse doesn't check this
if (url.protocol == null || url.host == null) {
throw new Error();
}
} catch (error) {
throw new ValidationError(`invalid URL "${urlstring}"`);
}
if (url.protocol !== 'https:')
throw new ValidationError(`URL protocol must be HTTPS (invalid: "${urlstring}")`);
if (net.isIP(url.hostname))
throw new ValidationError(
'URL hostname must be a domain name, not an IP address'
+ ` (invalid: "${urlstring}")`
);
}