Structers/Streamer.ts

import { Base } from "./Base";
import { executePromise } from "./ExecPromise";
import fs from "fs";
import { EventEmitter } from "events";
import { ffmpegAudioCodec, ffmpegPreset, StreamerOptions } from "../types";

/**
 * Streamer Class
 * @class
 * @extends Base
 */
export class Streamer extends Base {

    /**
     * @type {number}
     * @description CPU Core numbers allowed for FFMPEG to use for Stream
     * @public
     */
    public threads: number = 0;

    /**
     * @type {string}
     * @description Video File to Stream
     * @public
     */
    // eslint-disable-next-line
    // @ts-ignore
    public file: string;

    /**
     * @type {string}
     * @description Video Bitrate
     * @public
     */
    public videoBitrate: string = '4500k';

    /**
     * @type {String}
     * @description Audio Bitrate
     * @public
     */
    public audioBitrate: string = '128k';

    /**
     * @type {number}
     * @description Audio Sample Rate
     * @public
     */
    public audioSampleRate: number = 44100;

    /**
     * @description Stream Frame Rate
     * @type {number}
     * @public
     */
    public frameRate: number = 25;

    /**
     * @type {number}
     * @description Audio Channels
     * @public
     */
    public audioChannels: number = 2;

    /**
     * @public
     * @type {number}
     * @description Content Rate Factor, an x264 argument that tries to keep reasonably consistent video quality, while varying bitrate during more 'complicated' scenes, etc. A value of 30 allows somewhat lower quality and bit rate.
     */
    public contentRateFactor: number = 28;

    /**
     * @description Stream output wrapper
     * @type {string}
     * @private
     */
    private wrapper: string = 'flv';

    /**
     * @description Video Codec
     * @type {string}
     * @private
     */
    private videoCodec: string = 'libx264';

    /**
     * @description Audio Codec
     * @type {string}
     * @public
     */
    public audioCodec: ffmpegAudioCodec = 'aac';

    /**
     * @description Encoder Speed
     * @type {string}
     * @public
     */
    public preset: ffmpegPreset = 'medium';

    /**
     * @type {String}
     * @description Bundeled Command to be executed on OS
     * @private
     */
    private BundeledCommand: string = "";

    /**
     * @param {String} rtmpURL RTMP Url Connection
     * @param {StreamerOptions} opts Options related to your stream 
     */
    constructor(rtmpURL: string, opts: StreamerOptions = {})
    {
        super(rtmpURL);
        
        if('threads' in opts && typeof opts.threads == "number")
            this.threads = opts.threads;
        
        if('file' in opts)
            if(fs.existsSync(opts.file as string))
                this.file = opts.file as string;
            else
                throw new Error(`${opts.file} is not valid.`);

        if('audioChannels' in opts)
            this.audioChannels = opts.audioChannels as number;

        if('videoBitrate' in opts)
            this.videoBitrate = opts.videoBitrate as string;

        if('audioBitrate' in opts)
            this.audioBitrate = opts.audioBitrate as string;

        if('audioSampleRate' in opts)
            this.audioSampleRate = opts.audioSampleRate as number;

        if('crf' in opts)
            this.contentRateFactor = opts.crf as number;

        if('frameRate' in opts)
            this.frameRate = opts.frameRate as number;

        this.Bundle();
    }

    /**
     * Set file to stream
     * @param {String} path - File path - required 
     * @returns {Streamer}
     */
    public setFile(path: string): this
    {
        if(path && typeof path == 'string' && path !== '' && fs.existsSync(path))
            this.file = path;
        else
            throw new Error(`Invalid input recived as the file path.`);

        this.Bundle()
        return this;
    }

    /**
     * Set Video Bitrate
     * @param {String} x - Video bitrate 
     * @returns {Streamer}
     */
    public setVideoBitrate(x: string): this
    {
        this.videoBitrate = x;
        this.Bundle()
        return this;
    }

    /**
     * Set Audio Bitrate
     * @param {String} x - Audio bitrate 
     * @returns {Streamer}
     */
    public setAudioBitrate(x: string): this
    {
        this.audioBitrate = x
        this.Bundle()
        return this;
    }

    /**
     * Set Audio Sample rate
     * @param {Number} x - Audio Sample Rate
     * @returns {Streamer}
     */
    public setAudioSampleRate(x: number): this
    {
        this.audioSampleRate = x;
        this.Bundle()
        return this;
    }

    /**
     * Set Stream Frame rate
     * @param {Number} x - Frame Rate 
     * @returns {Streamer}
     */
    public setFrameRate(x: number): this
    {
        this.frameRate = x;
        this.Bundle()
        return this;
    }

    /**
     * Set CPU Cores/threads to use
     * @param {Number} x - CPU Cores
     * @returns {Streamer}
     */
    public setThreads(x: number): this
    {
        this.threads = x;
        this.Bundle()
        return this;
    }

    /**
     * Set Audio Channels
     * @param {Number} x - Audio Channels
     * @returns {Streamer}
     */
    public setAudioChannels(x: number): this
    {
        this.audioChannels = x;
        this.Bundle()
        return this;
    }

    /**
     * Set Content Rate Factor
     * @param {Number} x - Content Rate Factor
     * @returns {Streamer}
     */
    public setCRF(x: number): this
    {
        this.contentRateFactor = x;
        this.BundeledCommand = this.Bundle()
        return this;
    }

    /**
     * Set Streaming process preset
     * @param {ffmpegPreset} x - Process preset
     * @returns {Streamer}
     */
    public setPreset(x: ffmpegPreset): this
    {
        this.preset = x;
        this.Bundle()
        return this;
    }

    /**
     * Bundle the executable FFMPEG Command
     * @returns {String}
     * @private
     */
    private Bundle(): string
    {
        if(typeof this.audioBitrate !== 'string' || typeof this.videoBitrate !== 'string' || typeof this.audioChannels !== 'number' || typeof this.audioSampleRate !== 'number' || typeof this.contentRateFactor !== 'number' || typeof this.threads !== 'number' || typeof this.frameRate !== 'number' || !["ultrafast", "fast", "medium"].includes(this.preset))
            throw new Error(`One of imported values types are not valid.`);
        
        const result = `ffmpeg -re -i ${this.file} -pix_fmt yuvj420p -x264-params keyint=48:min-keyint=48:scenecut=-1 -b:v ${this.videoBitrate} -b:a ${this.audioBitrate} -ar ${this.audioSampleRate} -acodec ${this.audioCodec} -vcodec ${this.videoCodec} -preset ${this.preset} -crf ${this.contentRateFactor} -threads ${this.threads} -f ${this.wrapper} "${this.RTMPServer}"`
        this.BundeledCommand = result;
        return result;
    }

    /**
     * @description Start Streaming with FFMPEG
     * @returns {EventEmitter}
     */
    start()
    {
        const result = new EventEmitter();
        executePromise(this.BundeledCommand)
        .then(log => result.emit("finish", log))
        .catch(err => result.emit("error", err));
        return result;
    }
}