dizquetv/src/ffmpeg.js
2020-05-15 12:38:37 -04:00

177 lines
8.2 KiB
JavaScript

const spawn = require('child_process').spawn
const events = require('events')
const fs = require('fs')
class FFMPEG extends events.EventEmitter {
constructor(opts, channel) {
super()
this.offset = 0
this.args = []
this.opts = opts
this.channel = channel
this.ffmpegPath = opts.ffmpegPath
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))
}
}
}
// This is used to generate ass subtitles from text subs to be used with the ass filter in ffmpeg.
createSubsFromStream(file, startTime, duration, streamIndex, output) {
if (process.env.DEBUG) console.log('Generating .ass subtitles')
let exe = spawn(this.ffmpegPath, [
'-threads', this.opts.threads,
'-ss', startTime,
'-i', file,
'-t', duration,
'-map', `0:${streamIndex}`,
'-f', 'ass',
output
])
return new Promise((resolve, reject) => {
if (this.opts.logFfmpeg) {
exe.stderr.on('data', (chunk) => {
process.stderr.write(chunk)
})
}
exe.on('close', (code) => {
if (code === 0) {
if (process.env.DEBUG) console.log('Successfully generated .ass subtitles')
resolve()
} else {
console.log('Failed generating .ass subtitles.')
reject()
}
})
})
}
async spawn(lineupItem) {
let videoIndex = lineupItem.opts.videoIndex
let audioIndex = lineupItem.opts.audioIndex
let subtitleIndex = lineupItem.opts.subtitleIndex
let uniqSubFileName = Date.now().valueOf().toString()
for (let i = 0, l = lineupItem.streams.length; i < l; i++) {
if (videoIndex === '-1' && lineupItem.streams[i].streamType === 1)
if (lineupItem.streams[i].default)
videoIndex = i
if (audioIndex === '-1' && lineupItem.streams[i].streamType === 2)
if (lineupItem.streams[i].default || lineupItem.streams[i].selected)
audioIndex = i
if (subtitleIndex === '-1' && lineupItem.streams[i].streamType === 3)
if (lineupItem.streams[i].default || lineupItem.streams[i].forced)
subtitleIndex = i
}
// if for some reason we didn't find a default track, let ffmpeg decide..
if (videoIndex === '-1')
videoIndex = 'v'
if (audioIndex === '-1')
audioIndex = 'a'
let sub = (subtitleIndex === '-1' || subtitleIndex === '-2') ? null : lineupItem.streams[subtitleIndex]
let tmpargs = JSON.parse(JSON.stringify(this.args))
let startTime = tmpargs.indexOf('STARTTIME')
let dur = tmpargs.indexOf('DURATION')
let input = tmpargs.indexOf('INPUTFILE')
let vidStream = tmpargs.indexOf('VIDEOSTREAM')
let output = tmpargs.indexOf('OUTPUTFILE')
let tsoffset = tmpargs.indexOf('TSOFFSET')
let audStream = tmpargs.indexOf('AUDIOSTREAM')
let chanName = tmpargs.indexOf('CHANNELNAME')
tmpargs[startTime] = lineupItem.start / 1000
tmpargs[dur] = lineupItem.duration / 1000
tmpargs[input] = lineupItem.file
tmpargs[audStream] = `0:${audioIndex}`
tmpargs[chanName] = `service_name="${this.channel.name}"`
tmpargs[tsoffset] = this.offset
tmpargs[output] = 'pipe:1'
let iconOverlay = `[0:${videoIndex}]null`
let deinterlace = 'null'
let posAry = [ '20:20', 'W-w-20:20', '20:H-h-20', 'W-w-20:H-h-20'] // top-left, top-right, bottom-left, bottom-right (with 20px padding)
let icnDur = ''
if (this.channel.iconDuration > 0)
icnDur = `:enable='between(t,0,${this.channel.iconDuration})'`
if (this.channel.icon !== '' && this.channel.overlayIcon && lineupItem.type === 'program') {
iconOverlay = `[1:v]scale=${this.channel.iconWidth}:-1[icn];[0:${videoIndex}][icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}`
if (process.env.DEBUG) console.log('Channel Icon Overlay Enabled')
}
if (videoIndex !== 'v') {
if (typeof lineupItem.streams[videoIndex].scanType === 'undefined' || lineupItem.streams[videoIndex].scanType !== 'progressive') {
deinterlace = 'yadif'
if (process.env.DEBUG) console.log('Deinterlacing Video')
}
}
if (sub === null || lineupItem.type === 'commercial') { // No subs or icon overlays for Commercials
tmpargs[vidStream] = '[v]'
tmpargs.splice(vidStream - 1, 0, "-filter_complex", `${iconOverlay}[v1];[v1]${deinterlace}[v]`)
console.log("No Subtitles")
} else if (sub.codec === 'pgs') { // If program has PGS subs
tmpargs[vidStream] = '[v]'
if (typeof sub.index === 'undefined') { // If external subs
console.log("PGS SUBS (external) - Not implemented..")
tmpargs.splice(vidStream - 1, 0, "-filter_complex", `${iconOverlay}[v1];[v1]${deinterlace}[v]`)
} else { // Otherwise, internal/embeded pgs subs
console.log("PGS SUBS (embeded)")
tmpargs.splice(vidStream - 1, 0, "-filter_complex", `${iconOverlay}[v2];[v2]${deinterlace}[v1];[v1][0:${sub.index}]overlay[v]`)
}
} else if (sub.codec === 'srt' || sub.codec === 'ass') {
tmpargs[vidStream] = '[v]'
if (typeof sub.index === 'undefined') {
console.log("SRT SUBS (external)")
await this.createSubsFromStream(sub.key, lineupItem.start / 1000, lineupItem.duration / 1000, 0, `${process.env.DATABASE}/${uniqSubFileName}.ass`)
tmpargs.splice(vidStream - 1, 0, "-filter_complex", `${iconOverlay}[v1];[v1]${deinterlace},ass=${process.env.DATABASE}/${uniqSubFileName}.ass[v]`)
} else {
console.log("SRT SUBS (embeded) - This may take a few seconds..")
await this.createSubsFromStream(lineupItem.file, lineupItem.start / 1000, lineupItem.duration / 1000, sub.index, `${process.env.DATABASE}/${uniqSubFileName}.ass`)
tmpargs.splice(vidStream - 1, 0, "-filter_complex", `${iconOverlay}[v1];[v1]${deinterlace},ass=${process.env.DATABASE}/${uniqSubFileName}.ass[v]`)
}
} else { // Can't do VobSub's as plex only hosts the .idx file, there is no access to the .sub file.. Who the fuck uses VobSubs anyways.. SRT/ASS FTW
tmpargs[vidStream] = '[v]'
tmpargs.splice(vidStream - 1, 0, "-filter_complex", `${iconOverlay}[v1];[v1]${deinterlace}[v]`)
console.log("No Compatible Subtitles")
}
if (this.channel.icon !== '' && this.channel.overlayIcon && lineupItem.type === 'program') // Add the channel icon to ffmpeg input if enabled
tmpargs.splice(vidStream - 1, 0, '-i', this.channel.icon)
this.offset += lineupItem.duration / 1000
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 (fs.existsSync(`${process.env.DATABASE}/${uniqSubFileName}.ass`))
fs.unlinkSync(`${process.env.DATABASE}/${uniqSubFileName}.ass`)
if (code === null)
this.emit('close', code)
else if (code === 0)
this.emit('end')
else if (code === 255)
this.emit('close', code)
else
this.emit('error', { code: code, cmd: `${opts.ffmpegPath} ${tmpargs.join(' ')}` })
})
}
kill() {
this.ffmpeg.kill()
}
}
module.exports = FFMPEG