diff --git a/index.js b/index.js index e719f2d..e083ae3 100644 --- a/index.js +++ b/index.js @@ -108,6 +108,7 @@ function initDB(db) { if (ffmpegSettings.length === 0) { db['ffmpeg-settings'].save({ ffmpegPath: "/usr/bin/ffmpeg", + ffprobePath: "/usr/bin/ffprobe", offset: 0, threads: '4', videoEncoder: 'mpeg2video', @@ -118,7 +119,35 @@ function initDB(db) { audioChannels: '2', audioRate: '48000', bufSize: '1000k', - audioEncoder: 'ac3' + audioEncoder: 'ac3', + preferAudioLanguage: 'false', + audioLanguage: 'eng', + deinterlace: true, + logFfmpeg: true, + args: `-threads 4 +-ss STARTTIME +-t DURATION +-re +-i INPUTFILE +-vf yadif +-map 0:v +-map AUDIOSTREAM +-c:v mpeg2video +-c:a ac3 +-ac 2 +-ar 48000 +-b:a 192k +-b:v 10000k +-s 1280x720 +-r 30 +-flags cgop+ilme +-sc_threshold 1000000000 +-minrate:v 10000k +-maxrate:v 10000k +-bufsize:v 1000k +-f mpegts +-output_ts_offset TSOFFSET +OUTPUTFILE` }) } let xmltvSettings = db['xmltv-settings'].find() diff --git a/src/api.js b/src/api.js index 8d17890..d475835 100644 --- a/src/api.js +++ b/src/api.js @@ -64,6 +64,7 @@ function api(db, xmltvInterval) { router.post('/api/ffmpeg-settings', (req, res) => { // RESET db['ffmpeg-settings'].update({ _id: req.body._id }, { ffmpegPath: req.body.ffmpegPath, + ffprobePath: req.body.ffprobePath, offset: 0, threads: '4', videoEncoder: 'mpeg2video', @@ -74,7 +75,35 @@ function api(db, xmltvInterval) { audioChannels: '2', audioRate: '48000', bufSize: '1000k', - audioEncoder: 'ac3' + audioEncoder: 'ac3', + preferAudioLanguage: 'false', + audioLanguage: 'eng', + deinterlace: true, + logFfmpeg: true, + args: `-threads 4 +-ss STARTTIME +-t DURATION +-re +-i INPUTFILE +-vf yadif +-map 0:v +-map AUDIOSTREAM +-c:v mpeg2video +-c:a ac3 +-ac 2 +-ar 48000 +-b:a 192k +-b:v 10000k +-s 1280x720 +-r 30 +-flags cgop+ilme +-sc_threshold 1000000000 +-minrate:v 10000k +-maxrate:v 10000k +-bufsize:v 1000k +-f mpegts +-output_ts_offset TSOFFSET +OUTPUTFILE` }) let ffmpeg = db['ffmpeg-settings'].find()[0] res.send(ffmpeg) @@ -135,13 +164,14 @@ function api(db, xmltvInterval) { res.send(fs.readFileSync(xmltvSettings.file)) }) + // CHANNELS.M3U Download router.get('/api/channels.m3u', (req, res) => { res.type('text') let channels = db['channels'].find() var data = "#EXTM3U\n" for (var i = 0; i < channels.length; i++) { data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}",${channels[i].name}\n` - data += `http://${process.env.HOST}:${process.env.PORT}/video?channel=${channels[i].number}|User-Agent=ffmpeg\n` + data += `http://${process.env.HOST}:${process.env.PORT}/video?channel=${channels[i].number}\n` } res.send(data) }) diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 03cd1b8..0376ac9 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -5,45 +5,78 @@ class ffmpeg extends events.EventEmitter { constructor(opts) { super() this.offset = 0 + this.args = [] this.opts = opts + this.ffmpegPath = opts.ffmpegPath + this.ffprobePath = opts.ffprobePath + let lines = opts.args.split('\n') + for (let i = 0, l = lines.length; i < l; i++) { + let x = lines[i].indexOf(' ') + if (x === -1) + this.args.push(lines[i]) + else { + this.args.push(lines[i].substring(0, x)) + this.args.push(lines[i].substring(x + 1, lines[i].length)) + } + } } - spawn(lineupItem) { - let args = [ - '-headers', 'User-Agent: ffmpeg', - '-threads', this.opts.threads, - '-ss', lineupItem.start / 1000, - '-t', lineupItem.duration / 1000, - '-re', - '-i', lineupItem.file, - '-c:v', this.opts.videoEncoder, - '-c:a', this.opts.audioEncoder, - '-ac', this.opts.audioChannels, - '-ar', this.opts.audioRate, - '-b:a', this.opts.audioBitrate, - '-b:v', this.opts.videoBitrate, - '-s', this.opts.videoResolution, - '-r', this.opts.videoFrameRate, - '-flags', 'cgop+ilme', // Dont know if this does fuck all - '-sc_threshold', '1000000000', // same here... - '-minrate:v', this.opts.videoBitrate, - '-maxrate:v', this.opts.videoBitrate, - '-bufsize:v', this.opts.bufSize, - '-f', 'mpegts', - '-output_ts_offset', this.offset, // This actually helped.. VLC still shows "TS discontinuity" errors tho.. - 'pipe:1' - ] + getStreams(file) { + return new Promise((resolve, reject) => { + let ffprobe = spawn(this.ffprobePath, [ '-v', 'quiet', '-show_streams', '-of', 'json', file ]) + let str = "" + ffprobe.stdout.on('data', (chunk) => { + str += chunk + }) + ffprobe.on('close', () => { + resolve(str) + }) + }) + } + async spawn(lineupItem) { + let audioIndex = -1 + if (this.opts.preferAudioLanguage === 'true') { + let streams = JSON.parse(await this.getStreams(lineupItem.file)).streams + for (let i = 0, l = streams.length; i < l; i++) { + if (streams[i].codec_type === 'audio') { + if (streams[i].tags.language === this.opts.audioLanguage) { + audioIndex = i + break; + } + } + } + } + + let tmpargs = JSON.parse(JSON.stringify(this.args)) + let startTime = tmpargs.indexOf('STARTTIME') + let dur = tmpargs.indexOf('DURATION') + let input = tmpargs.indexOf('INPUTFILE') + let output = tmpargs.indexOf('OUTPUTFILE') + let tsoffset = tmpargs.indexOf('TSOFFSET') + let audStream = tmpargs.indexOf('AUDIOSTREAM') + + tmpargs[startTime] = lineupItem.start / 1000 + tmpargs[dur] = lineupItem.duration / 1000 + tmpargs[input] = lineupItem.file + tmpargs[audStream] = `0:${audioIndex === -1 ? 'a' : audioIndex}` + tmpargs[tsoffset] = this.offset + tmpargs[output] = 'pipe:1' this.offset += lineupItem.duration / 1000 - this.ffmpeg = spawn(this.opts.ffmpegPath, args) + this.ffmpeg = spawn(this.ffmpegPath, tmpargs) this.ffmpeg.stdout.on('data', (chunk) => { this.emit('data', chunk) }) + if (this.opts.logFfmpeg) { + this.ffmpeg.stderr.on('data', (chunk) => { + process.stderr.write(chunk) + }) + } this.ffmpeg.on('close', (code) => { if (code === null) this.emit('close', code) else if (code === 0) this.emit('end') else - this.emit('error', { code: code, cmd: `${args.join(' ')}` }) + this.emit('error', { code: code, cmd: `${tmpargs.join(' ')}` }) }) } kill() { diff --git a/src/hdhr.js b/src/hdhr.js index 9ee767e..34f7935 100644 --- a/src/hdhr.js +++ b/src/hdhr.js @@ -48,7 +48,7 @@ function hdhr(db) { var lineup = [] var channels = db['channels'].find() for (let i = 0, l = channels.length; i < l; i++) - lineup.push({ GuideNumber: channels[i].number.toString(), GuideName: channels[i].name, URL: `http://${process.env.HOST}:${process.env.PORT}/video?channel=${channels[i].number}|User-Agent=ffmpeg` }) + lineup.push({ GuideNumber: channels[i].number.toString(), GuideName: channels[i].name, URL: `http://${process.env.HOST}:${process.env.PORT}/video?channel=${channels[i].number}` }) res.send(JSON.stringify(lineup)) }) diff --git a/src/video.js b/src/video.js index 525d946..ea60d33 100644 --- a/src/video.js +++ b/src/video.js @@ -52,7 +52,6 @@ function video(db) { ffmpeg2.spawn(lineup.shift()) // Spawn the ffmpeg process, fire this bitch up - }) return router } \ No newline at end of file diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..4ed4f2c Binary files /dev/null and b/test.ts differ diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index fe3ad02..c59a83b 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -1,4 +1,4 @@ -module.exports = function (pseudotv) { + module.exports = function (pseudotv) { return { restrict: 'E', templateUrl: 'templates/ffmpeg-settings.html', @@ -8,17 +8,49 @@ module.exports = function (pseudotv) { link: function (scope, element, attrs) { pseudotv.getFfmpegSettings().then((settings) => { scope.settings = settings + if (typeof scope.settings.args === 'undefined') + scope.createArgString() }) scope.updateSettings = (settings) => { pseudotv.updateFfmpegSettings(settings).then((_settings) => { scope.settings = _settings + if (typeof scope.settings.args === 'undefined') + scope.createArgString() }) } scope.resetSettings = (settings) => { pseudotv.resetFfmpegSettings(settings).then((_settings) => { scope.settings = _settings + if (typeof scope.settings.args === 'undefined') + scope.createArgString() }) } + scope.createArgString = () => { + scope.settings.args = `-threads ${ scope.settings.threads } +-ss STARTTIME +-t DURATION +-re +-i INPUTFILE${ scope.settings.deinterlace ? `\n-vf yadif` : `` } +-map 0:v +-map AUDIOSTREAM +-c:v ${ scope.settings.videoEncoder} +-c:a ${ scope.settings.audioEncoder} +-ac ${ scope.settings.audioChannels} +-ar ${ scope.settings.audioRate} +-b:a ${ scope.settings.audioBitrate} +-b:v ${ scope.settings.videoBitrate} +-s ${ scope.settings.videoResolution} +-r ${ scope.settings.videoFrameRate} +-flags cgop+ilme +-sc_threshold 1000000000 +-minrate:v ${ scope.settings.videoBitrate} +-maxrate:v ${ scope.settings.videoBitrate} +-bufsize:v ${ scope.settings.bufSize} +-f mpegts +-output_ts_offset TSOFFSET +OUTPUTFILE` + + } } } } \ No newline at end of file diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 457c2b7..3d84b8f 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -8,18 +8,24 @@ Reset Options -
FFMPEG Path
+
FFMPEG Executable Path (eg: C:\ffmpeg\bin\ffmpeg.exe || /usr/bin/ffmpeg)
+
FFPROBE Executable Path (eg: C:\ffmpeg\bin\ffprobe.exe || /usr/bin/ffprobe)
+
Miscellaneous Options
-
+
- +
-
+
- + +
+
+
+

@@ -27,25 +33,36 @@
Video Options
- + - + - + - + +
+
Audio Options
- + - + - + - + +
+ + +
- +
+
+
Raw FFMPEG Arguments Modifying options above will reset the raw arguments. Therefore edit these last...
+ +
+
\ No newline at end of file diff --git a/web/services/plex.js b/web/services/plex.js index 8ef93e0..7f4c042 100644 --- a/web/services/plex.js +++ b/web/services/plex.js @@ -45,7 +45,12 @@ module.exports = function () { return client.Get(key).then(function (res) { var nested = [] for (let i = 0, l = typeof res.Metadata !== 'undefined' ? res.Metadata.length : 0; i < l; i++) { - var program = { + // Skip any videos (movie or episode) without a duration set... + if (typeof res.Metadata[i].duration === 'undefined' && (res.Metadata[i].type === "episode" || res.Metadata[i].type === "movie")) + continue + if (res.Metadata[i].duration <= 0 && (res.Metadata[i].type === "episode" || res.Metadata[i].type === "movie")) + continue + var program = { // can be show, season or playlist too. title: res.Metadata[i].title, key: res.Metadata[i].key, icon: `${server.protocol}://${server.host}:${server.port}${res.Metadata[i].thumb}?X-Plex-Token=${server.token}`,