Skip to content

Commit

Permalink
Merge pull request #15 from goatandsheep/master
Browse files Browse the repository at this point in the history
added webvtt exporter (i.e. compiler)
  • Loading branch information
osk authored Oct 20, 2018
2 parents 071bd45 + 252cda5 commit 1a19d10
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 6 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

# WebVTT parser and segmenter
# WebVTT compiler parser and segmenter

Parse WebVTT files, segments and generates HLS playlists for them.
Compiles, parses WebVTT files, segments and generates HLS playlists for them.

## Usage

Expand All @@ -23,7 +23,7 @@ Foo
Bar
```

We can parse, segment and create HLS playlists:
We can parse, segment and create HLS playlists, and compile back to WebVTT format:

```javascript
const webvtt = require('node-webvtt');
Expand All @@ -32,6 +32,7 @@ const segmentDuration = 10; // default to 10
const startOffset = 0; // Starting MPEG TS offset to be used in timestamp map, default 900000

const parsed = webvtt.parse(input);
const compile = webvtt.compile(input);
const segmented = webvtt.parse(input, segmentDuration);
const playlist = webvtt.hls.hlsSegmentPlaylist(input, segmentDuration);
const segments = webvtt.hls.hlsSegment(input, segmentDuration, startOffset);
Expand Down Expand Up @@ -88,6 +89,13 @@ For the above example we'd get:
}
```

### Compiling

Compiles JSON from the above format back into a WebVTT string.

If the object is missing any attributes, the compiler will throw a `CompilerError` exception. So
for safety, calls to `compile` should be in `try catch`.

### Segmenting

Segments a subtitle according to how it should be segmented for HLS subtitles.
Expand Down
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';

const parse = require('./lib/parser').parse;
const compile = require('./lib/compiler').compile;
const segment = require('./lib/segmenter').segment;
const hls = require('./lib/hls');

module.exports = { parse, segment, hls };
module.exports = { parse, compile, segment, hls };
148 changes: 148 additions & 0 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
'use strict';

/**
* See spec: https://www.w3.org/TR/webvtt1/#file-structure
*/

function CompilerError (message, error) {
this.message = message;
this.error = error;
}

CompilerError.prototype = Object.create(Error.prototype);

function compile (input) {

if (!input) {
throw new CompilerError('Input must be non-null');
}

if (typeof input !== 'object') {
throw new CompilerError('Input must be an object');
}

if (input.isArray) {
throw new CompilerError('Input cannot be array');
}

if (!input.valid) {
throw new CompilerError('Input must be valid');
}


let output = 'WEBVTT\n';

input.cues.forEach((cue) => {
output += '\n';
output += compileCue(cue);
output += '\n';
});

return output;
}

/**
* Compile a single cue block.
*
* @param {array} cue Array of content for the cue
*
* @returns {object} cue Cue object with start, end, text and styles.
* Null if it's a note
*/
function compileCue (cue) {
// TODO: check for malformed JSON
if (typeof cue !== 'object') {
throw new CompilerError('Cue malformed: not of type object');
}

if (typeof cue.identifier !== 'string' &&
typeof cue.identifier !== 'number' &&
cue.identifier !== null) {
throw new CompilerError(`Cue malformed: identifier value is not a string.
${JSON.stringify(cue)}`);
}

if (isNaN(cue.start)) {
throw new CompilerError(`Cue malformed: null start value.
${JSON.stringify(cue)}`);
}

if (isNaN(cue.end)) {
throw new CompilerError(`Cue malformed: null end value.
${JSON.stringify(cue)}`);
}

if (cue.start >= cue.end) {
throw new CompilerError(`Cue malformed: start timestamp greater than end
${JSON.stringify(cue)}`);
}

if (typeof cue.text !== 'string') {
throw new CompilerError(`Cue malformed: null text value.
${JSON.stringify(cue)}`);
}

if (typeof cue.styles !== 'string') {
throw new CompilerError(`Cue malformed: null styles value.
${JSON.stringify(cue)}`);
}

let output = '';

if (cue.identifier.length > 0) {
output += `${cue.identifier}\n`;
}

const startTimestamp = convertTimestamp(cue.start);
const endTimestamp = convertTimestamp(cue.end);

output += `${startTimestamp} --> ${endTimestamp}`;
output += cue.styles ? ` ${cue.styles}` : '';
output += `\n${cue.text}`;

return output;
}

function convertTimestamp (time) {
if (isNaN(time)) {
throw new CompilerError('Timestamp: not type number');
}

const hours = pad(calculateHours(time), 2);
const minutes = pad(calculateMinutes(time), 2);
const seconds = pad(calculateSeconds(time), 2);
const milliseconds = pad(calculateMs(time), 3);
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}

function pad (num, zeroes) {
if (isNaN(zeroes)) {
throw new CompilerError('Pad: length is not type number');
}

let output = `${num}`;

while (output.length < zeroes) {
output = `0${ output }`;
}

return output;
}

function calculateHours (time) {
return Math.floor(time / 60 / 60);
}

function calculateMinutes (time) {
return (Math.floor(time / 60) % 60);
}

function calculateSeconds (time) {
return Math.floor((time) % 60);
}

function calculateMs (time) {
return Math.round((time % 1) * 1000);
}

module.exports = { CompilerError, compile};
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "node-webvtt",
"version": "1.1.0",
"description": "WebVTT parser and segmenter with HLS support",
"description": "WebVTT parser, compiler, and segmenter with HLS support",
"main": "index.js",
"scripts": {
"eslint": "eslint *.js **/*.js",
Expand Down
Loading

0 comments on commit 1a19d10

Please sign in to comment.