Skip plex media without a duration. FFMPEG Raw arguments added. Audio track selection by language using ffprobe.

This commit is contained in:
Dan Ferguson 2020-05-01 07:23:11 -04:00
parent da4ff9fe1f
commit f34e5f524c
9 changed files with 193 additions and 48 deletions

View File

@ -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()

View File

@ -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)
})

View File

@ -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() {

View File

@ -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))
})

View File

@ -52,7 +52,6 @@ function video(db) {
ffmpeg2.spawn(lineup.shift()) // Spawn the ffmpeg process, fire this bitch up
})
return router
}

BIN
test.ts Normal file

Binary file not shown.

View File

@ -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`
}
}
}
}

View File

@ -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>

View File

@ -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}`,