Skip to content

Commit

Permalink
Merge pull request #55 from jordankoehn/ffmpeg_concat_demuxer
Browse files Browse the repository at this point in the history
Ffmpeg concat demuxer
  • Loading branch information
jordankoehn committed Jun 20, 2020
2 parents 9cb4b53 + 0fe29e7 commit 0175594
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 162 deletions.
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
FROM node:12.16
RUN apt-get -y update && \
apt-get -y upgrade && \
apt-get install -y ffmpeg && \
rm -rf /var/lib/apt/lists/*
FROM node:12.18-alpine3.12
# Should be ffmpeg v4.2.3
RUN apk add --no-cache ffmpeg && ffmpeg -version
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install
Expand Down
3 changes: 1 addition & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ function initDB(db) {
videoResolutionHeight: 'unchanged',
videoBitrate: 10000,
videoBufSize: 2000,
enableAutoPlay: true,
breakStreamOnCodecChange: true,
concatMuxDelay: '0',
logFfmpeg: true
})
}
Expand Down
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ function api(db, xmltvInterval) {
videoResolutionHeight: 'unchanged',
videoBitrate: 10000,
videoBufSize: 2000,
enableAutoPlay: true,
breakStreamOnCodecChange: true,
concatMuxDelay: '0',
logFfmpeg: true
})
let ffmpeg = db['ffmpeg-settings'].find()[0]
Expand Down
84 changes: 51 additions & 33 deletions src/ffmpeg.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ class FFMPEG extends events.EventEmitter {
this.channel = channel
this.ffmpegPath = opts.ffmpegPath
}
async spawn(streamUrl, duration, enableIcon, videoResolution) {
async spawn(streamUrl, streamStats, duration, enableIcon, type, isConcatPlaylist) {
let ffmpegArgs = [`-threads`, this.opts.threads,
`-t`, duration,
`-re`,
`-fflags`, `+genpts`,
`-i`, streamUrl];
`-re`,
`-fflags`, `+genpts+discardcorrupt+igndts`];

if (duration > 0)
ffmpegArgs.push(`-t`, duration)

if (isConcatPlaylist == true)
ffmpegArgs.push(`-f`, `concat`,
`-safe`, `0`,
`-protocol_whitelist`, `file,http,tcp,https,tcp,tls`)

if (enableIcon == true) {
ffmpegArgs.push(`-i`, streamUrl)

// Overlay icon
if (enableIcon && type === 'program') {
if (process.env.DEBUG) console.log('Channel Icon Overlay Enabled')

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)
Expand All @@ -27,39 +36,48 @@ class FFMPEG extends events.EventEmitter {

let iconOverlay = `[1:v]scale=${this.channel.iconWidth}:-1[icn];[0:v][icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[outv]`
// Only scale video if specified, don't upscale video
if (this.opts.videoResolutionHeight != "unchanged" && parseInt(this.opts.videoResolutionHeight, 10) < parseInt(videoResolution, 10)) {
if (this.opts.videoResolutionHeight != "unchanged" && streamStats.videoHeight != `undefined` && parseInt(this.opts.videoResolutionHeight, 10) < parseInt(streamStats.videoHeight, 10)) {
iconOverlay = `[0:v]scale=-2:${this.opts.videoResolutionHeight}[scaled];[1:v]scale=${this.channel.iconWidth}:-1[icn];[scaled][icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[outv]`
}

ffmpegArgs.push(`-i`, `${this.channel.icon}`,
`-filter_complex`, iconOverlay,
`-map`, `[outv]`,
`-c:v`, this.opts.videoEncoder,
`-flags`, `cgop+ilme`,
`-sc_threshold`, `1000000000`,
`-b:v`, `${this.opts.videoBitrate}k`,
`-minrate:v`, `${this.opts.videoBitrate}k`,
`-maxrate:v`, `${this.opts.videoBitrate}k`,
`-bufsize:v`, `${this.opts.videoBufSize}k`,
`-map`, `0:a`,
`-c:a`, `copy`);
} else {
ffmpegArgs.push(`-c`, `copy`);
}
`-filter_complex`, iconOverlay,
`-map`, `[outv]`,
`-c:v`, this.opts.videoEncoder,
`-flags`, `cgop+ilme`,
`-sc_threshold`, `1000000000`,
`-b:v`, `${this.opts.videoBitrate}k`,
`-minrate:v`, `${this.opts.videoBitrate}k`,
`-maxrate:v`, `${this.opts.videoBitrate}k`,
`-bufsize:v`, `${this.opts.videoBufSize}k`,
`-map`, `0:a`,
`-c:a`, `copy`,
`-muxdelay`, `0`,
`-muxpreload`, `0`);
} else if (enableIcon && streamStats.videoCodec != this.opts.videoEncoder) { // Encode commercial if video codec does not match
ffmpegArgs.push(`-map`, `0`,
`-c:v`, this.opts.videoEncoder,
`-flags`, `cgop+ilme`,
`-sc_threshold`, `1000000000`,
`-b:v`, `${this.opts.videoBitrate}k`,
`-minrate:v`, `${this.opts.videoBitrate}k`,
`-maxrate:v`, `${this.opts.videoBitrate}k`,
`-bufsize:v`, `${this.opts.videoBufSize}k`,
`-c:a`, `copy`,
`-muxdelay`, `0`,
`-muxpreload`, `0`);
} else
ffmpegArgs.push(`-map`, `0`,
`-c`, `copy`,
`-muxdelay`, this.opts.concatMuxDelay,
`-muxpreload`, this.opts.concatMuxDelay);

ffmpegArgs.push(`-metadata`,
`service_provider="PseudoTV"`,
`-metadata`,
`service_name="${this.channel.name}"`,
`-f`,
`mpegts`,
`-output_ts_offset`,
`0`,
`-muxdelay`,
`0`,
`-muxpreload`,
`0`,
`pipe:1`);
`service_provider="PseudoTV"`,
`-metadata`,
`service_name="${this.channel.name}`,
`-f`, `mpegts`,
`pipe:1`)

this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs)
this.ffmpeg.stdout.on('data', (chunk) => {
Expand Down
6 changes: 5 additions & 1 deletion src/helperFuncs.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
timeElapsed -= program.duration
}
}

if (currentProgramIndex === -1)
throw new Error("No program found; find algorithm fucked up")

return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
}

Expand Down Expand Up @@ -104,5 +106,7 @@ function createLineup(obj) {
}

function isChannelIconEnabled(enableChannelOverlay, icon, overlayIcon, type) {
return enableChannelOverlay == true && icon !== '' && overlayIcon && type === 'program'
if (typeof type === `undefined`)
return enableChannelOverlay == true && icon !== '' && overlayIcon
return enableChannelOverlay == true && icon !== '' && overlayIcon
}
53 changes: 29 additions & 24 deletions src/plexTranscoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ class PlexTranscoder {
this.playState = "stopped"
}

async getStream(deinterlace) {
let stream = {}
stream.streamUrl = await this.getStreamUrl(deinterlace);
stream.streamStats = this.getVideoStats();
return stream;
}

async getStreamUrl(deinterlace) {
// Set transcoding parameters based off direct stream params
this.setTranscodingArgs(true, deinterlace)
Expand All @@ -45,7 +52,7 @@ class PlexTranscoder {
let mediaBufferSize = (directStream == true) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize
let subtitles = (this.settings.enableSubtitles == true) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar
let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing

let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set
let audioBoost=`100` // only applies when downmixing to stereo I believe, add option later?
let profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra
Expand All @@ -54,6 +61,7 @@ class PlexTranscoder {

let clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${this.settings.videoCodecs}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\
add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&CopyMatroskaAttachments=true)+\
add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&BreakNonKeyframes=true)+\
add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.width&value=${resolutionArr[0]})+\
add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.height&value=${resolutionArr[1]})`

Expand Down Expand Up @@ -110,36 +118,25 @@ lang=en`
return this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"][0]["height"];
}

getVideoStats(channelIconEnabled, ffmpegEncoderName) {
let ret = []
getVideoStats() {
let ret = {}
let streams = this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"]

streams.forEach(function (stream) {
// Video
if (stream["streamType"] == "1") {
ret.push(stream["width"],
stream["height"],
Math.round(stream["frameRate"]))
// Rounding framerate avoids scenarios where
// 29.9999999 & 30 don't match. Probably close enough
// to continue the stream as is.

// Implies future transcoding
if (channelIconEnabled == true)
if (ffmpegEncoderName.includes('mpeg2'))
ret.push("mpeg2video")
else if (ffmpegEncoderName.includes("264"))
ret.push("h264")
else if (ffmpegEncoderName.includes("hevc") || ffmpegEncoderName.includes("265"))
ret.push("hevc")
else
ret.push("unknown")
else
ret.push(stream["codec"])
ret.videoCodec = stream["codec"];
ret.videoWidth = stream["width"];
ret.videoHeight = stream["height"];
ret.videoFramerate = Math.round(stream["frameRate"]);
// Rounding framerate avoids scenarios where
// 29.9999999 & 30 don't match.
}
// Audio. Only look at stream being used
if (stream["streamType"] == "2" && stream["selected"] == "1")
ret.push(stream["channels"], stream["codec"])
if (stream["streamType"] == "2" && stream["selected"] == "1") {
ret.audioChannels = stream["channels"];
ret.audioCodec = stream["codec"];
}
})

return ret
Expand All @@ -151,6 +148,14 @@ lang=en`
})
.then((res) => {
this.decisionJson = res.data;

// Print error message if transcode not possible
// TODO: handle failure better
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode
if (transcodeDecisionCode != "1001") {
console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
}
})
.catch((err) => {
console.log(err);
Expand Down
Loading

0 comments on commit 0175594

Please sign in to comment.