Skip to content

Commit

Permalink
Merge remote-tracking branch 'JonFranklin301/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
mint-dewit committed Jun 3, 2021
2 parents 0f77bca + 109cac5 commit 63cb75d
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 99 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ yarn lint
// having a format defined implies a reencode will be done
"width": 1024
}
},
{
"postFix": "_NO-AUDIO",
"discard": {
"audio": true
}
}
]
}
Expand Down Expand Up @@ -134,6 +140,15 @@ export interface EncoderConfig {
postFix: string
/** extension of the new file (e.g. .mp4) */
extension?: string
/** custom options. Ignores all other options */
custom?: string
/** discard streams */
discard?: {
video?: boolean
audio?: boolean
subtitle?: boolean
data?: boolean
}
/** Configures loudnorm filter */
loudness?: {
integrated?: number
Expand Down Expand Up @@ -163,3 +178,43 @@ export interface EncoderConfig {
}
}
```

#### Custom encoder

The custom encoder enables the use of complex ffmpeg encoding/filter settings. It is assumed you know how to use ffmpeg when using the custom encoder.

It takes in a string of ffmpeg args and the output file path, name and extension are automatically appended.

```json
"encoders": [
{
"postFix": "_custom-text-overlay",
"extension": ".mp4",
"custom": "-vf drawtext=\"fontfile=/Windows/Fonts/arial.ttf: text='Custom text overlay': fontcolor=white: fontsize=120: box=1: boxcolor=black: boxborderw=20: x=(w-text_w)/2: y=(h-text_h)/1.4\""
}
]
```

The custom encoder supports handlebar style string replacement for customising file names. If any handlebars are detected the output filename will not be automatically appended. You will need to handle the output file yourself (e.g. `{{dir}}/{{name}}{{postFix}}_Custom-format{{extension}}`).

```json
"encoders": [
{
"postFix": "_Complex-Filter",
"custom": "-an -filter_complex \"[0]pad=iw*2:ih[int];[int][0]overlay=W/2:0[doublewidth];[doublewidth]scale=iw/2:ih/2[scaled];[scaled]split=3[s1][s2][s3];[s1]crop=iw/3:ih:0:0[one];[s2]crop=iw/3:ih:ow:0[two];[s3]crop=iw/3:ih:ow*2:0[three]\" -map \"[one]\" -q:v 1 -sws_flags bicubic \"{{dir}}/{{name}}{{postFix}}_{{date}}_one{{ext}}\" -map \"[two]\" -q:v 1 -sws_flags bicubic \"{{dir}}/{{name}}{{postFix}}_{{date}}_two{{ext}}\" -map \"[three]\" -q:v 1 -sws_flags bicubic \"{{dir}}/{{name}}{{postFix}}_{{date}}_three{{ext}}\""
}
]
```

Available to use:

```ts
postFix // EncoderConfig postfix
extension? // EncoderConfig extension
root // Input file root name
dir // Input file directory
base // Input file name with original extension
ext // Input file extension
name // Input file name
date // Date in ISO format (YYYY-MM-DD)
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"vuex": "^3.4.0"
},
"devDependencies": {
"@types/electron-devtools-installer": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.4.0",
Expand All @@ -34,6 +35,7 @@
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"electron": "^5.0.0",
"electron-devtools-installer": "^3.1.1",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
Expand Down
5 changes: 3 additions & 2 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict'

import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol, installVueDevtools } from 'vue-cli-plugin-electron-builder/lib'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'

import { API } from './background/api'
Expand Down Expand Up @@ -67,7 +68,7 @@ app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installVueDevtools()
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
Expand Down
4 changes: 2 additions & 2 deletions src/background/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ export class API {

private _createRenderSteps(file: string, analysis: Analysis) {
return this.config!.encoders.map((encoderConfig) => {
const p = path.parse(file)
const p: path.ParsedPath = path.parse(file)
const output = `${p.dir}/${p.name}${encoderConfig.postFix}${encoderConfig.extension || p.ext}`
return new RenderWorkstep(file, output, encoderConfig, analysis)
return new RenderWorkstep(file, output, p, encoderConfig, analysis)
})
}
}
240 changes: 146 additions & 94 deletions src/background/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,118 +73,169 @@ export class Renderer extends EventEmitter {
private _getProcessArgs(step: RenderWorkstep) {
const args = ['-y', '-i', `"${step.input}"`]

if (step.encoderConfig.videoEncoder) {
const videoConfig = step.encoderConfig.videoEncoder

args.push('-codec:v', videoConfig.encoder || 'libx264')

if (videoConfig.encoderOptions) args.push(...videoConfig.encoderOptions)
else args.push('-crf', '18')

const videoFilter: Array<string> = []
const inputFieldOrder = step.analysis.info.field_order

if (step.encoderConfig.format) {
const f = step.encoderConfig.format
if (f.width || f.height) {
videoFilter.push(
`scale=w=${f.width || '-1'}:h=${f.height || -1}:interl=${
inputFieldOrder !== FieldOrder.Progressive ? 1 : 0
}:${f.width && f.height ? 'force_original_aspect_ratio=decrease' : ''}`
)
if (f.width && f.height && f.width > 0 && f.height > 0) {
videoFilter.push(`pad=${f.width || '-1'}:${f.height}:-1:-1`)
}
}
if (inputFieldOrder !== FieldOrder.Progressive && f.interlaced === undefined) {
// input is interlaced, output is progressive

// export 1 frame per frame or 1 frame per field:
const mode = (f.frameRate || 25) >= 50 ? 1 : 0
// if we know fieldorder instruct filter, otherwise autodetect:
const parity =
inputFieldOrder === FieldOrder.BFF ? 1 : inputFieldOrder === FieldOrder.TFF ? 0 : -1
// if we know fieldorder always deinterlace, otherwise autodetect:
const deint = inputFieldOrder && inputFieldOrder !== FieldOrder.Unknown ? 0 : -1

videoFilter.push(`bwdif=mode=${mode}:parity=${parity}:deint=${deint}`)
if (step.encoderConfig.custom) {
let stringHasMatchData: boolean = false
// data available for use in handlebars
const customEncoderMatchData: { [key: string]: string } = {
...step.outputParse,
postFix: step.encoderConfig.postFix,
extension: step.encoderConfig.extension
? step.encoderConfig.extension
: step.outputParse.ext,
date: new Date().toISOString().slice(0, 10)
}
// replace handlebars with data
const customEncoderString = step.encoderConfig.custom.replace(
/\{\{([^}]+)\}\}/g,
(match: string) => {
match = match.slice(2, -2)
if (!customEncoderMatchData[match]) return '{{' + match + '}}'
stringHasMatchData = true
return customEncoderMatchData[match]
}
if (f.interlaced) {
// output is interlaced
if (inputFieldOrder) {
// input has metadata
if (inputFieldOrder !== (f.interlaced as unknown)) {
// input !== output
if (inputFieldOrder === FieldOrder.Progressive) {
// input is progressive
const modes: { [key: string]: string } = {
[FieldOrder.TFF]: 'interleave_top',
[FieldOrder.BFF]: 'interleave_bottom'
}
const mode = modes[f.interlaced] as string
)

videoFilter.push('fps=' + (f.frameRate || 25) * 2) // make sure input has appropriate amount of frames
videoFilter.push('tinterlace=mode=' + mode)
}
if (stringHasMatchData) {
// handlebars are used. do not append the output file name
args.push(customEncoderString)
} else {
// handlebars are not used. append the output file name
args.push(step.encoderConfig.custom)
args.push(`"${step.output}"`)
}
return args
}

const discard = step.encoderConfig.discard || {}

if (discard.video) {
args.push('-vn')
} else {
if (step.encoderConfig.videoEncoder) {
const videoConfig = step.encoderConfig.videoEncoder

args.push('-codec:v', videoConfig.encoder || 'libx264')

if (videoConfig.encoderOptions) args.push(...videoConfig.encoderOptions)
else args.push('-crf', '18')

const videoFilter: Array<string> = []
const inputFieldOrder = step.analysis.info.field_order

if (step.encoderConfig.format) {
const f = step.encoderConfig.format
if (f.width || f.height) {
videoFilter.push(
`scale=w=${f.width || '-1'}:h=${f.height || -1}:interl=${
inputFieldOrder !== FieldOrder.Progressive ? 1 : 0
}:${f.width && f.height ? 'force_original_aspect_ratio=decrease' : ''}`
)
if (f.width && f.height && f.width > 0 && f.height > 0) {
videoFilter.push(`pad=${f.width || '-1'}:${f.height}:-1:-1`)
}
} else {
// TODO - is there a ffmpeg filter that can interlace based on decoder field metadata?
}
if (inputFieldOrder !== FieldOrder.Progressive && f.interlaced === undefined) {
// input is interlaced, output is progressive

// export 1 frame per frame or 1 frame per field:
const mode = (f.frameRate || 25) >= 50 ? 1 : 0
// if we know fieldorder instruct filter, otherwise autodetect:
const parity =
inputFieldOrder === FieldOrder.BFF ? 1 : inputFieldOrder === FieldOrder.TFF ? 0 : -1
// if we know fieldorder always deinterlace, otherwise autodetect:
const deint = inputFieldOrder && inputFieldOrder !== FieldOrder.Unknown ? 0 : -1

videoFilter.push(`bwdif=mode=${mode}:parity=${parity}:deint=${deint}`)
}
if (f.interlaced) {
// output is interlaced
if (inputFieldOrder) {
// input has metadata
if (inputFieldOrder !== (f.interlaced as unknown)) {
// input !== output
if (inputFieldOrder === FieldOrder.Progressive) {
// input is progressive
const modes: { [key: string]: string } = {
[FieldOrder.TFF]: 'interleave_top',
[FieldOrder.BFF]: 'interleave_bottom'
}
const mode = modes[f.interlaced] as string

videoFilter.push('fps=' + (f.frameRate || 25) * 2) // make sure input has appropriate amount of frames
videoFilter.push('tinterlace=mode=' + mode)
}
}
} else {
// TODO - is there a ffmpeg filter that can interlace based on decoder field metadata?
}

// set fieldorder (this will correctly set tff/bff)
videoFilter.push(`fieldorder=${f.interlaced}`)
} else if (f.frameRate) {
videoFilter.push(`fps=${f.frameRate || '25'}`)
}
// note that input needs a color space to be set for use to do useful things
const hasInpColorSpace = step.analysis.info.colorSpace
if (hasInpColorSpace && f.colorspace) {
videoFilter.push(`colorspace=${f.colorspace}`)
} else if (hasInpColorSpace && f.height) {
const cSpace = f.height <= 576 ? 'bt601-6-625' : 'bt709'
videoFilter.push(`colorspace=${cSpace}`)
// set fieldorder (this will correctly set tff/bff)
videoFilter.push(`fieldorder=${f.interlaced}`)
} else if (f.frameRate) {
videoFilter.push(`fps=${f.frameRate || '25'}`)
}
// note that input needs a color space to be set for use to do useful things
const hasInpColorSpace = step.analysis.info.colorSpace
if (hasInpColorSpace && f.colorspace) {
videoFilter.push(`colorspace=${f.colorspace}`)
} else if (hasInpColorSpace && f.height) {
const cSpace = f.height <= 576 ? 'bt601-6-625' : 'bt709'
videoFilter.push(`colorspace=${cSpace}`)
}
}

if (videoFilter.length) args.push('-filter:v', videoFilter.join(','))
} else {
args.push('-codec:v', 'copy')
}
}

if (videoFilter.length) args.push('-filter:v', videoFilter.join(','))
if (discard.audio) {
args.push('-an')
} else {
args.push('-codec:v', 'copy')
}
if (step.encoderConfig.audioEncoder) {
const audioConfig = step.encoderConfig.audioEncoder

if (step.encoderConfig.audioEncoder) {
const audioConfig = step.encoderConfig.audioEncoder
args.push('-codec:a', audioConfig.encoder || 'aac')

args.push('-codec:a', audioConfig.encoder || 'aac')
if (audioConfig.encoderOptions) args.push(...audioConfig.encoderOptions)

if (audioConfig.encoderOptions) args.push(...audioConfig.encoderOptions)
let audioFilter = ''

let audioFilter = ''
if (step.encoderConfig.loudness) {
let measured = ''
if (step.analysis.info.loudness) {
// pass in measure values
const loudness = step.analysis.info.loudness
measured =
`measured_i=${loudness.integrated}:` +
`measured_lra=${loudness.LRA}:` +
`measured_tp=${loudness.truePeak}:` +
`measured_thresh=${loudness.threshold}:`
}
const lConfig = step.encoderConfig.loudness
audioFilter += `loudnorm=${measured}i=${lConfig.integrated || -23}:lra=${lConfig.LRA ||
13}:tp=${lConfig.truePeak || -1}:dual_mono=${lConfig.dualMono || 'false'}`
}

if (audioFilter) args.push('-filter:a', audioFilter)

if (step.encoderConfig.loudness) {
let measured = ''
if (step.analysis.info.loudness) {
// pass in measure values
const loudness = step.analysis.info.loudness
measured =
`measured_i=${loudness.integrated}:` +
`measured_lra=${loudness.LRA}:` +
`measured_tp=${loudness.truePeak}:` +
`measured_thresh=${loudness.threshold}:`
if (step.encoderConfig.format && step.encoderConfig.format.audioRate) {
args.push('-ar', step.encoderConfig.format.audioRate)
} else if (step.encoderConfig.loudness) {
args.push('-ar', '48k')
}
const lConfig = step.encoderConfig.loudness
audioFilter += `loudnorm=${measured}i=${lConfig.integrated || -23}:lra=${lConfig.LRA ||
13}:tp=${lConfig.truePeak || -1}:dual_mono=${lConfig.dualMono || 'false'}`
} else {
args.push('-codec:a', 'copy')
}
}

if (audioFilter) args.push('-filter:a', audioFilter)
if (discard.subtitle) {
args.push('-sn')
}

if (step.encoderConfig.format && step.encoderConfig.format.audioRate) {
args.push('-ar', step.encoderConfig.format.audioRate)
} else if (step.encoderConfig.loudness) {
args.push('-ar', '48k')
}
} else {
args.push('-codec:a', 'copy')
if (discard.data) {
args.push('-dn')
}

if (step.encoderConfig.format && step.encoderConfig.format.format) {
Expand All @@ -193,6 +244,7 @@ export class Renderer extends EventEmitter {

// pass the interlacing flags
if (
!discard.video &&
step.encoderConfig.videoEncoder &&
step.encoderConfig.format &&
step.encoderConfig.format.interlaced
Expand Down
Loading

0 comments on commit 63cb75d

Please sign in to comment.