Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
transitive-bullshit committed Apr 4, 2018
0 parents commit 942eaed
Show file tree
Hide file tree
Showing 26 changed files with 5,349 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
5 changes: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": [
"standard"
]
}
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
node_modules

# builds
build
dist

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.cache

npm-debug.log*
yarn-debug.log*
yarn-error.log*
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
media
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
language: node_js
node_js:
- 9
- 8
before_install:
- sudo add-apt-repository ppa:mc3man/trusty-media -y
- sudo apt-get update -q
- sudo apt-get install ffmpeg -y
8 changes: 8 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env node
'use strict'

module.exports = require('./lib')

if (!module.parent) {
require('./lib/cli')(process.argv)
}
58 changes: 58 additions & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node
'use strict'

const concat = require('.')
const fs = require('fs')
const program = require('commander')
const { version } = require('../package')

module.exports = async (argv) => {
program
.version(version)
.usage('[options] <videos...>')
.option('-o, --output <output>', 'path to mp4 file to write', (s) => s, 'out.mp4')
.option('-t, --transition-name <name>', 'name of gl-transition to use', (s) => s, 'fade')
.option('-d, --transition-duration <duration>', 'duration of transition to use in ms', parseInt, 500)
.option('-T, --transitions <file>', 'json file to load transitions from')
.option('-f, --frame-format <format>', 'format to use for temp frame images', /^(raw|png|jpg)$/i, 'raw')
.option('-C, --no-cleanup-frames', 'disables cleaning up temp frame images')
.action(async (videos, opts) => {
})
.parse(argv)

let transitions

if (program.transitions) {
try {
transitions = JSON.parse(fs.readFileSync(program.transitions, 'utf8'))
} catch (err) {
console.error(`error parsing transitions file "${program.transitions}"`, err)
throw err
}
}

try {
const videos = program.args.filter((v) => typeof v === 'string')

await concat({
log: console.log,

videos,
output: program.output,

transition: {
name: program.transitionName,
duration: program.transitionDuration
},
transitions,

frameFormat: program.frameFormat,
cleanupFrames: program.cleanupFerames
})

console.log(program.output)
} catch (err) {
console.error('concat error', err)
throw err
}
}
75 changes: 75 additions & 0 deletions lib/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict'

const GL = require('gl')

const createFrameWriter = require('./frame-writer')
const createTransition = require('./transition')

module.exports = async (opts) => {
const {
frameFormat,
theme
} = opts

const {
width,
height
} = theme

const gl = GL(width, height)

if (!gl) {
throw new Error('failed to create OpenGL context')
}

const frameWriter = await createFrameWriter({
gl,
width,
height,
frameFormat
})

const ctx = {
gl,
width,
height,
frameWriter,
transition: null
}

ctx.setTransition = async ({ name, resizeMode }) => {
if (ctx.transition) {
ctx.transition.dispose()
ctx.transition = null
}

ctx.transition = await createTransition({
gl,
name,
resizeMode
})
}

ctx.capture = ctx.frameWriter.write.bind(ctx.frameWriter)

ctx.render = async (...args) => {
if (ctx.transition) {
return ctx.transition.draw(...args)
}
}

ctx.flush = async () => {
return ctx.frameWriter.flush()
}

ctx.dispose = async () => {
if (ctx.transition) {
ctx.transition.dispose()
ctx.transition = null
}

gl.destroy()
}

return ctx
}
23 changes: 23 additions & 0 deletions lib/extract-video-frames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict'

const ffmpeg = require('fluent-ffmpeg')

module.exports = (opts) => {
const {
videoPath,
framePattern
} = opts

return new Promise((resolve, reject) => {
ffmpeg(videoPath)
.outputOptions([
'-pix_fmt', 'rgba',
'-start_number', '0'
])
.output(framePattern)
.on('start', (cmd) => console.log({ cmd }))
.on('end', () => resolve(framePattern))
.on('error', (err) => reject(err))
.run()
})
}
135 changes: 135 additions & 0 deletions lib/frame-writer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict'

// TODO: this worker pool approach was an experiment that failed to yield any
// performance advantages. we should revert back to the straightforward version
// even though dis ist prettttyyyyyyy codezzzzzz.

const fs = require('fs')
const pRace = require('p-race')
const sharp = require('sharp')
const util = require('util')

const fsOpen = util.promisify(fs.open.bind(fs))
const fsWrite = util.promisify(fs.write.bind(fs))
const fsClose = util.promisify(fs.close.bind(fs))

module.exports = async (opts) => {
const {
concurrency = 1,
frameFormat = 'raw',
gl,
width,
height
} = opts

if (frameFormat !== 'png' && frameFormat !== 'raw') {
throw new Error(`frame writer unsupported format "${frameFormat}"`)
}

let pool = []
let inactive = []
let active = { }

for (let i = 0; i < concurrency; ++i) {
const byteArray = new Uint8Array(width * height * 4)

const worker = {
id: i,
byteArray,
promise: null
}

if (frameFormat === 'png') {
const buffer = Buffer.from(byteArray.buffer)
worker.encoder = sharp(buffer, {
raw: {
width,
height,
channels: 4
}
}).png({
compressionLevel: 0,
adaptiveFiltering: false
})
}

pool.push(worker)
inactive.push(i)
}

const writeFrame = async ({ filePath, worker }) => {
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, worker.byteArray)

try {
if (frameFormat === 'png') {
await new Promise((resolve, reject) => {
worker.encoder.toFile(filePath, (err) => {
if (err) reject(err)
resolve()
})
})
} else {
const { byteArray } = worker
const fd = await fsOpen(filePath, 'w')

/*
// write file in 64k chunks
const chunkSize = 2 ** 17
let offset = 0
while (offset < byteArray.byteLength) {
const length = Math.min(chunkSize, byteArray.length - offset)
await fsWrite(fd, byteArray, offset, length)
offset += length
}
*/

// write file in one large chunk
await fsWrite(fd, byteArray)
await fsClose(fd)
}
} catch (err) {
delete active[worker.id]
inactive.push(worker.id)
throw err
}

delete active[worker.id]
inactive.push(worker.id)

return filePath
}

const reserve = async () => {
if (inactive.length) {
const id = inactive.pop()
const worker = pool[id]
active[id] = worker
return worker
} else {
await pRace(Object.values(active).map(v => v.promise))
return reserve()
}
}

return {
write: async (filePath) => {
const worker = await reserve()
worker.promise = writeFrame({
filePath,
worker
})
},

flush: async () => {
return Promise.all(Object.values(active).map(v => v.promise))
},

dispose: () => {
pool = null
active = null
inactive = null
}
}
}
39 changes: 39 additions & 0 deletions lib/get-file-ext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict'

const parseUrl = require('url-parse')

const extWhitelist = new Set([
// videos
'gif',
'mp4',
'webm',
'mkv',
'mov',
'avi',

// images
'bmp',
'jpg',
'jpeg',
'png',
'tif',
'webp',

// audio
'mp3',
'aac',
'wav',
'flac',
'opus',
'ogg'
])

module.exports = (url, opts = { strict: true }) => {
const { pathname } = parseUrl(url)
const parts = pathname.split('.')
const ext = (parts[parts.length - 1] || '').trim().toLowerCase()

if (!opts.strict || extWhitelist.has(ext)) {
return ext
}
}
Loading

0 comments on commit 942eaed

Please sign in to comment.