Skip to content

Commit

Permalink
Complete rework
Browse files Browse the repository at this point in the history
* Use typescript and export types
* Refactor playlist reading to `hls-playlist-reader` module
* Split processing into segment-reader and segment-streamer
* Preliminary HLS server control and LL-HLS support
* More tests
* Drop Travis
  • Loading branch information
kanongil authored Nov 12, 2020
1 parent a7519b1 commit ef41bf5
Show file tree
Hide file tree
Showing 21 changed files with 3,405 additions and 1,679 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/test/fixtures/**
60 changes: 56 additions & 4 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
'use strict';

const EslintPluginHapi = require('@hapi/eslint-plugin');
const TypescriptRules = require('@typescript-eslint/eslint-plugin').rules;


const tsifyRules = function (from) {

const rules = {};

for (const rule in from) {
if (TypescriptRules[rule]) {
rules[rule] = 'off';
rules[`@typescript-eslint/${rule}`] = from[rule];
}
}

return rules;
};


module.exports = {
extends: '@hapi/eslint-config-hapi',
root: true,
extends: [
'plugin:@hapi/recommended',
'plugin:@typescript-eslint/eslint-recommended'
],
plugins: [
'@typescript-eslint'
],
parserOptions: {
ecmaVersion: 2018
}
}
ecmaVersion: 2019
},
ignorePatterns: ['/lib/*.js', '/lib/*.d.ts'],
overrides: [{
files: ['lib/**/*.ts'],
extends: [
'plugin:@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname
},
rules: {
...tsifyRules(EslintPluginHapi.configs.recommended.rules),
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',

'@typescript-eslint/member-delimiter-style': 'warn',
'@typescript-eslint/no-throw-literal': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/type-annotation-spacing': 'warn',
'@typescript-eslint/unified-signatures': 'warn'
}
}]
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/node_modules/
/lib/*.js
/lib/*.d.ts
coverage.html
7 changes: 7 additions & 0 deletions .labrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

module.exports = {
transform: require.resolve('./build/transform-typescript'),
sourcemaps: true,
flat: true
};
4 changes: 2 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
*
!lib/**
!.npmignore
!lib/**/*.js
!lib/**/*.d.ts
6 changes: 0 additions & 6 deletions .travis.yml

This file was deleted.

39 changes: 0 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,6 @@ Read segments from any [Apple HLS](http://tools.ietf.org/html/draft-pantos-http-

[![Build Status](https://travis-ci.org/kanongil/node-hls-segment-reader.svg?branch=master)](https://travis-ci.org/kanongil/node-hls-segment-reader)

## API

### new HlsSegmentReader(uri, [options])

Creates an `objectMode` `Readable`, which returns segments from the `uri`, as specified in `options`.

#### Options

* `fullStream` - Always start from first segment. Otherwise, follow standard client behavior.
* `withData` - Set to open & return data streams for each segment.
* `startDate` - Select initial segment based on datetime information in the index.
* `stopDate` - Stop stream after this date based on datetime information in the index.
* `maxStallTime` - Stop live/event stream if index has not been updated in `maxStallTime` ms.
* `extensions` - Allow specified index extensions, as specified in `m3u8parse`.

### Event: `index`

* `index` - `M3U8Playlist` with parsed index.

Emitted whenever a new remote index has been parsed.

### Event: `data`

* `obj` - `HlsSegmentObject` containing segment data.

### HlsSegmentReader#abort([graceful])

Stop the reader.

### HlsSegmentObject

* `type` - `'segment'` or `'init'`.
* `file` - File metadata from remote server.
* `stream` - `uristream` `Readable` with segment data when `withData` is set.
* `segment` - Object with segment data, when type is `'segment'`:
* `seq` - Sequence number.
* `details` - `M3U8Segment` info.
* `init` - m3u8 `AttrList` with map segment data when type is `'init'`.

## Installation

```sh
Expand Down
61 changes: 61 additions & 0 deletions build/transform-typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';

const Ts = require('typescript');

const cache = new Map();

const tsify = function (content, fileName) {

const searchPath = Ts.normalizePath(Ts.sys.getCurrentDirectory());
const configFileName = process.env.TSCONFIG || Ts.findConfigFile(searchPath, Ts.sys.fileExists);

const compilerOptions = getCompilerOptionsViaCache(configFileName);
compilerOptions.sourceMap = false;
compilerOptions.inlineSourceMap = true;

const { outputText/*, diagnostics*/ } = Ts.transpileModule(content, {
fileName,
compilerOptions,
reportDiagnostics: false
});

const splicePoint = outputText.indexOf('Object.defineProperty(exports, "__esModule", { value: true })');
if (splicePoint !== -1) {
return '/* $lab:coverage:off$ */' + outputText.slice(0, splicePoint) + '/* $lab:coverage:on$ */' + outputText.slice(splicePoint);
}

return outputText;
};

const getCompilerOptionsViaCache = function (configFileName) {

let options;

if (!(options = cache[configFileName])) {
options = cache[configFileName] = getCompilerOptions(configFileName);
}

return options;
};

const getCompilerOptions = function (configFileName) {

const { config, error } = Ts.readConfigFile(configFileName, Ts.sys.readFile);
if (error) {
throw new Error(`TS config error in ${configFileName}: ${error.messageText}`);
}

const { options } = Ts.parseJsonConfigFileContent(
config,
Ts.sys,
Ts.getDirectoryPath(configFileName),
{},
configFileName);

return options;
};

module.exports = [{
ext: '.ts',
transform: tsify
}];
25 changes: 25 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { HlsPlaylistReader, HlsPlaylistReaderOptions } from 'hls-playlist-reader';
import { HlsSegmentReader, HlsSegmentReaderOptions } from './segment-reader';
import { HlsSegmentStreamer, HlsSegmentStreamerOptions } from './segment-streamer';

export { HlsReaderObject } from './segment-reader';
export type { HlsIndexMeta } from 'hls-playlist-reader';
export { HlsStreamerObject } from './segment-streamer';

const createSimpleReader = function (uri: string, options: HlsSegmentReaderOptions & HlsSegmentStreamerOptions = {}): HlsSegmentStreamer {

const reader = new HlsSegmentReader(uri, options);

options.withData ??= false;

const streamer = new HlsSegmentStreamer(reader, options);

reader.on('problem', (err) => streamer.emit('problem', err));

return streamer;
};

export { createSimpleReader, HlsPlaylistReader, HlsSegmentReader, HlsSegmentStreamer };
export type { HlsPlaylistReaderOptions, HlsSegmentReaderOptions, HlsSegmentStreamerOptions };

export default createSimpleReader;
88 changes: 88 additions & 0 deletions lib/segment-downloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { URL } from 'url';
import type { Byterange } from 'hls-playlist-reader/lib/helpers';

import { finished } from 'stream';
import { promisify } from 'util';

import { applyToDefaults, assert } from '@hapi/hoek';

import { performFetch } from 'hls-playlist-reader/lib/helpers';


const internals = {
defaults: {
probe: false
},
streamFinished: promisify(finished)
};


// eslint-disable-next-line @typescript-eslint/ban-types
type FetchToken = object | string | number;

export class SegmentDownloader {

probe: boolean;

#fetches = new Map<FetchToken, ReturnType<typeof performFetch>>();

constructor(options: { probe?: boolean }) {

options = applyToDefaults(internals.defaults, options);

this.probe = !!options.probe;
}

fetchSegment(token: FetchToken, uri: URL, byterange?: Required<Byterange>, { tries = 3 } = {}): ReturnType<typeof performFetch> {

const promise = performFetch(uri, { byterange, probe: this.probe, retries: tries - 1 });
this._startTracking(token, promise);
return promise;
}

/**
* Stops any fetch not in token list
*
* @param {Set<FetchToken>} tokens
*/
setValid(tokens = new Set()): void {

for (const [token, fetch] of this.#fetches) {

if (!tokens.has(token)) {
this._stopTracking(token);
fetch.abort();
}
}
}

private _startTracking(token: FetchToken, promise: ReturnType<typeof performFetch>) {

assert(!this.#fetches.has(token), 'A token can only be tracked once');

// Setup auto-untracking

promise.then(({ stream }) => {

if (!stream) {
return this._stopTracking(token);
}

if (!this.#fetches.has(token)) {
return; // It has already been aborted
}

finished(stream, () => this._stopTracking(token));
}).catch((/*err*/) => {

this._stopTracking(token);
});

this.#fetches.set(token, promise);
}

private _stopTracking(token: FetchToken) {

this.#fetches.delete(token);
}
}
Loading

0 comments on commit ef41bf5

Please sign in to comment.