dizquetv/src/program-player.js

113 lines
4.4 KiB
JavaScript

/******************
* This module is to take a "program" and return a stream that plays the
* program. OR the promise fails which would mean that there was an error
* playing the program.
*
* The main purpose is to have an abstract interface for playing program
* objects without having to worry the source of the program object.
* A long-term goal is to be able to have sources other than plex to play
* videos. This is the first step towards that goal.
*
* Returns an event emitter that will have the 'data' or 'end' events.
* The contract is that the emitter will stream at least some media stream
* before ending. Any errors that occur after sending the first data will
* be dealt with internally and be presented as an 'end' event.
*
* If there is a timeout when receiving the initial data, or if the program
* can't load at all for some reason, an Error will be thrown. Make sure to
* deal with the thrown error.
**/
let OfflinePlayer = require('./offline-player');
let PlexPlayer = require('./plex-player');;
const EventEmitter = require('events');
const helperFuncs = require('./helperFuncs');
class ProgramPlayer {
constructor( context ) {
this.context = context;
let program = context.lineupItem;
if (context.m3u8) {
context.ffmpegSettings.normalizeAudio = false;
// people might want the codec normalization to stay because of player support
context.ffmpegSettings.normalizeResolution = false;
}
if ( typeof(program.err) !== 'undefined') {
console.log("About to play error stream");
this.delegate = new OfflinePlayer(true, context);
} else if (program.type === 'loading') {
console.log("About to play loading stream");
/* loading */
context.isLoading = true;
this.delegate = new OfflinePlayer(false, context);
} else if (program.type === 'offline') {
console.log("About to play offline stream");
/* offline */
this.delegate = new OfflinePlayer(false, context);
} else {
console.log("About to play plex stream");
/* plex */
this.delegate = new PlexPlayer(context);
}
this.context.watermark = helperFuncs.getWatermark( context.ffmpegSettings, context.channel, context.lineupItem.type);
}
cleanUp() {
this.delegate.cleanUp();
}
async playDelegate(outStream) {
return await new Promise( async (accept, reject) => {
try {
let stream = await this.delegate.play(outStream);
accept(stream);
let emitter = new EventEmitter();
function end() {
reject( Error("Stream ended with no data") );
stream.removeAllListeners("data");
stream.removeAllListeners("end");
stream.removeAllListeners("close");
stream.removeAllListeners("error");
emitter.emit("end");
}
stream.on("error", err => {
reject( Error("Stream ended in error with no data. " + JSON.stringify(err) ) );
end();
});
stream.on("end", end);
stream.on("close", end);
} catch (err) {
reject(err);
}
})
}
async play(outStream) {
try {
return await this.playDelegate(outStream);
} catch(err) {
if (! (err instanceof Error) ) {
err= Error("Program player had an error before receiving any data. " + JSON.stringify(err) );
}
if (this.context.lineupItem.err instanceof Error) {
console.log(err.stack);
throw Error("Additional error when attempting to play error stream.");
}
console.log("Error when attempting to play video. Fallback to error stream: " + err.stack);
//Retry once with an error stream:
this.context.lineupItem = {
err: err,
start: this.context.lineupItem.start,
streamDuration: this.context.lineupItem.streamDuration,
}
this.delegate.cleanUp();
this.delegate = new OfflinePlayer(true, this.context);
return await this.play(outStream);
}
}
}
module.exports = ProgramPlayer;