Skip plex media without a duration. FFMPEG Raw arguments added. Audio track selection by language using ffprobe.
This commit is contained in:
parent
da4ff9fe1f
commit
f34e5f524c
31
index.js
31
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()
|
||||
|
||||
34
src/api.js
34
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)
|
||||
})
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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))
|
||||
})
|
||||
|
||||
|
||||
@ -52,7 +52,6 @@ function video(db) {
|
||||
|
||||
ffmpeg2.spawn(lineup.shift()) // Spawn the ffmpeg process, fire this bitch up
|
||||
|
||||
|
||||
})
|
||||
return router
|
||||
}
|
||||
@ -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`
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,18 +8,24 @@
|
||||
Reset Options
|
||||
</button>
|
||||
</h5>
|
||||
<h6>FFMPEG Path</h6>
|
||||
<h6>FFMPEG Executable Path (eg: C:\ffmpeg\bin\ffmpeg.exe || /usr/bin/ffmpeg)</h6>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.ffmpegPath"/>
|
||||
<h6>FFPROBE Executable Path (eg: C:\ffmpeg\bin\ffprobe.exe || /usr/bin/ffprobe)</h6>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.ffprobePath" ng-disabled="settings.preferAudioLanguage === 'false'"/>
|
||||
<hr/>
|
||||
<h6>Miscellaneous Options</h6>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-4">
|
||||
<label>Threads</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.threads"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.threads" ng-change="createArgString()"/>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-4">
|
||||
<label>Buffer Size</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.bufSize"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.bufSize" ng-change="createArgString()"/>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label>Log FFMPEG</label><br/>
|
||||
<input id="logFfmpeg" type="checkbox" ng-model="settings.logFfmpeg"/> <label for="logFfmpeg">Log FFMPEG to console</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
@ -27,25 +33,36 @@
|
||||
<div class="col-sm-6">
|
||||
<h6>Video Options</h6>
|
||||
<label>Video Encoder</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoEncoder"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoEncoder" ng-change="createArgString()"/>
|
||||
<label>Video Bitrate</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoBitrate"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoBitrate" ng-change="createArgString()"/>
|
||||
<label>Video Resolution</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoResolution"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoResolution" ng-change="createArgString()"/>
|
||||
<label>Video Framerate</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoFrameRate"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoFrameRate" ng-change="createArgString()"/>
|
||||
<label>Deinterlace</label><br/>
|
||||
<input id="deinterlace" type="checkbox" ng-model="settings.deinterlace" ng-change="createArgString()"/> <label for="deinterlace">Deinterlace Video</label>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h6>Audio Options</h6>
|
||||
<label>Audio Encoder</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioEncoder"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioEncoder" ng-change="createArgString()"/>
|
||||
<label>Audio Channels</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioChannels"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioChannels" ng-change="createArgString()"/>
|
||||
<label>Audio Bitrate</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioBitrate"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioBitrate" ng-change="createArgString()"/>
|
||||
<label>Audio Rate</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioRate"/>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioRate" ng-change="createArgString()"/>
|
||||
<label>Audio Stream Preference</label><br/>
|
||||
<input id="preferAudioLanguage1" type="radio" ng-model="settings.preferAudioLanguage" value="false"/> <label for="preferAudioLanguage1">Default track</label>
|
||||
<input id="preferAudioLanguage2" type="radio" ng-model="settings.preferAudioLanguage" value="true"/> <label for="preferAudioLanguage2">Prefer Select Language <small>(uses ffprobe)</small></label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioLanguage" ng-disabled="settings.preferAudioLanguage === 'false'"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<h6>Raw FFMPEG Arguments <small>Modifying options above will reset the raw arguments. Therefore edit these last...</small></h6>
|
||||
<textarea class="form-control" style="height: 350px" ng-model="settings.args"></textarea>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
@ -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}`,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user