Skip to content

Commit 942eaed

Browse files
init
0 parents  commit 942eaed

26 files changed

+5349
-0
lines changed

Diff for: .editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true

Diff for: .eslintrc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": [
3+
"standard"
4+
]
5+
}

Diff for: .gitignore

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# See https://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# dependencies
4+
node_modules
5+
6+
# builds
7+
build
8+
dist
9+
10+
# misc
11+
.DS_Store
12+
.env
13+
.env.local
14+
.env.development.local
15+
.env.test.local
16+
.env.production.local
17+
.cache
18+
19+
npm-debug.log*
20+
yarn-debug.log*
21+
yarn-error.log*

Diff for: .npmignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
media

Diff for: .travis.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
language: node_js
2+
node_js:
3+
- 9
4+
- 8
5+
before_install:
6+
- sudo add-apt-repository ppa:mc3man/trusty-media -y
7+
- sudo apt-get update -q
8+
- sudo apt-get install ffmpeg -y

Diff for: index.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env node
2+
'use strict'
3+
4+
module.exports = require('./lib')
5+
6+
if (!module.parent) {
7+
require('./lib/cli')(process.argv)
8+
}

Diff for: lib/cli.js

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env node
2+
'use strict'
3+
4+
const concat = require('.')
5+
const fs = require('fs')
6+
const program = require('commander')
7+
const { version } = require('../package')
8+
9+
module.exports = async (argv) => {
10+
program
11+
.version(version)
12+
.usage('[options] <videos...>')
13+
.option('-o, --output <output>', 'path to mp4 file to write', (s) => s, 'out.mp4')
14+
.option('-t, --transition-name <name>', 'name of gl-transition to use', (s) => s, 'fade')
15+
.option('-d, --transition-duration <duration>', 'duration of transition to use in ms', parseInt, 500)
16+
.option('-T, --transitions <file>', 'json file to load transitions from')
17+
.option('-f, --frame-format <format>', 'format to use for temp frame images', /^(raw|png|jpg)$/i, 'raw')
18+
.option('-C, --no-cleanup-frames', 'disables cleaning up temp frame images')
19+
.action(async (videos, opts) => {
20+
})
21+
.parse(argv)
22+
23+
let transitions
24+
25+
if (program.transitions) {
26+
try {
27+
transitions = JSON.parse(fs.readFileSync(program.transitions, 'utf8'))
28+
} catch (err) {
29+
console.error(`error parsing transitions file "${program.transitions}"`, err)
30+
throw err
31+
}
32+
}
33+
34+
try {
35+
const videos = program.args.filter((v) => typeof v === 'string')
36+
37+
await concat({
38+
log: console.log,
39+
40+
videos,
41+
output: program.output,
42+
43+
transition: {
44+
name: program.transitionName,
45+
duration: program.transitionDuration
46+
},
47+
transitions,
48+
49+
frameFormat: program.frameFormat,
50+
cleanupFrames: program.cleanupFerames
51+
})
52+
53+
console.log(program.output)
54+
} catch (err) {
55+
console.error('concat error', err)
56+
throw err
57+
}
58+
}

Diff for: lib/context.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict'
2+
3+
const GL = require('gl')
4+
5+
const createFrameWriter = require('./frame-writer')
6+
const createTransition = require('./transition')
7+
8+
module.exports = async (opts) => {
9+
const {
10+
frameFormat,
11+
theme
12+
} = opts
13+
14+
const {
15+
width,
16+
height
17+
} = theme
18+
19+
const gl = GL(width, height)
20+
21+
if (!gl) {
22+
throw new Error('failed to create OpenGL context')
23+
}
24+
25+
const frameWriter = await createFrameWriter({
26+
gl,
27+
width,
28+
height,
29+
frameFormat
30+
})
31+
32+
const ctx = {
33+
gl,
34+
width,
35+
height,
36+
frameWriter,
37+
transition: null
38+
}
39+
40+
ctx.setTransition = async ({ name, resizeMode }) => {
41+
if (ctx.transition) {
42+
ctx.transition.dispose()
43+
ctx.transition = null
44+
}
45+
46+
ctx.transition = await createTransition({
47+
gl,
48+
name,
49+
resizeMode
50+
})
51+
}
52+
53+
ctx.capture = ctx.frameWriter.write.bind(ctx.frameWriter)
54+
55+
ctx.render = async (...args) => {
56+
if (ctx.transition) {
57+
return ctx.transition.draw(...args)
58+
}
59+
}
60+
61+
ctx.flush = async () => {
62+
return ctx.frameWriter.flush()
63+
}
64+
65+
ctx.dispose = async () => {
66+
if (ctx.transition) {
67+
ctx.transition.dispose()
68+
ctx.transition = null
69+
}
70+
71+
gl.destroy()
72+
}
73+
74+
return ctx
75+
}

Diff for: lib/extract-video-frames.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict'
2+
3+
const ffmpeg = require('fluent-ffmpeg')
4+
5+
module.exports = (opts) => {
6+
const {
7+
videoPath,
8+
framePattern
9+
} = opts
10+
11+
return new Promise((resolve, reject) => {
12+
ffmpeg(videoPath)
13+
.outputOptions([
14+
'-pix_fmt', 'rgba',
15+
'-start_number', '0'
16+
])
17+
.output(framePattern)
18+
.on('start', (cmd) => console.log({ cmd }))
19+
.on('end', () => resolve(framePattern))
20+
.on('error', (err) => reject(err))
21+
.run()
22+
})
23+
}

Diff for: lib/frame-writer.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use strict'
2+
3+
// TODO: this worker pool approach was an experiment that failed to yield any
4+
// performance advantages. we should revert back to the straightforward version
5+
// even though dis ist prettttyyyyyyy codezzzzzz.
6+
7+
const fs = require('fs')
8+
const pRace = require('p-race')
9+
const sharp = require('sharp')
10+
const util = require('util')
11+
12+
const fsOpen = util.promisify(fs.open.bind(fs))
13+
const fsWrite = util.promisify(fs.write.bind(fs))
14+
const fsClose = util.promisify(fs.close.bind(fs))
15+
16+
module.exports = async (opts) => {
17+
const {
18+
concurrency = 1,
19+
frameFormat = 'raw',
20+
gl,
21+
width,
22+
height
23+
} = opts
24+
25+
if (frameFormat !== 'png' && frameFormat !== 'raw') {
26+
throw new Error(`frame writer unsupported format "${frameFormat}"`)
27+
}
28+
29+
let pool = []
30+
let inactive = []
31+
let active = { }
32+
33+
for (let i = 0; i < concurrency; ++i) {
34+
const byteArray = new Uint8Array(width * height * 4)
35+
36+
const worker = {
37+
id: i,
38+
byteArray,
39+
promise: null
40+
}
41+
42+
if (frameFormat === 'png') {
43+
const buffer = Buffer.from(byteArray.buffer)
44+
worker.encoder = sharp(buffer, {
45+
raw: {
46+
width,
47+
height,
48+
channels: 4
49+
}
50+
}).png({
51+
compressionLevel: 0,
52+
adaptiveFiltering: false
53+
})
54+
}
55+
56+
pool.push(worker)
57+
inactive.push(i)
58+
}
59+
60+
const writeFrame = async ({ filePath, worker }) => {
61+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, worker.byteArray)
62+
63+
try {
64+
if (frameFormat === 'png') {
65+
await new Promise((resolve, reject) => {
66+
worker.encoder.toFile(filePath, (err) => {
67+
if (err) reject(err)
68+
resolve()
69+
})
70+
})
71+
} else {
72+
const { byteArray } = worker
73+
const fd = await fsOpen(filePath, 'w')
74+
75+
/*
76+
// write file in 64k chunks
77+
const chunkSize = 2 ** 17
78+
let offset = 0
79+
80+
while (offset < byteArray.byteLength) {
81+
const length = Math.min(chunkSize, byteArray.length - offset)
82+
83+
await fsWrite(fd, byteArray, offset, length)
84+
offset += length
85+
}
86+
*/
87+
88+
// write file in one large chunk
89+
await fsWrite(fd, byteArray)
90+
await fsClose(fd)
91+
}
92+
} catch (err) {
93+
delete active[worker.id]
94+
inactive.push(worker.id)
95+
throw err
96+
}
97+
98+
delete active[worker.id]
99+
inactive.push(worker.id)
100+
101+
return filePath
102+
}
103+
104+
const reserve = async () => {
105+
if (inactive.length) {
106+
const id = inactive.pop()
107+
const worker = pool[id]
108+
active[id] = worker
109+
return worker
110+
} else {
111+
await pRace(Object.values(active).map(v => v.promise))
112+
return reserve()
113+
}
114+
}
115+
116+
return {
117+
write: async (filePath) => {
118+
const worker = await reserve()
119+
worker.promise = writeFrame({
120+
filePath,
121+
worker
122+
})
123+
},
124+
125+
flush: async () => {
126+
return Promise.all(Object.values(active).map(v => v.promise))
127+
},
128+
129+
dispose: () => {
130+
pool = null
131+
active = null
132+
inactive = null
133+
}
134+
}
135+
}

Diff for: lib/get-file-ext.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
3+
const parseUrl = require('url-parse')
4+
5+
const extWhitelist = new Set([
6+
// videos
7+
'gif',
8+
'mp4',
9+
'webm',
10+
'mkv',
11+
'mov',
12+
'avi',
13+
14+
// images
15+
'bmp',
16+
'jpg',
17+
'jpeg',
18+
'png',
19+
'tif',
20+
'webp',
21+
22+
// audio
23+
'mp3',
24+
'aac',
25+
'wav',
26+
'flac',
27+
'opus',
28+
'ogg'
29+
])
30+
31+
module.exports = (url, opts = { strict: true }) => {
32+
const { pathname } = parseUrl(url)
33+
const parts = pathname.split('.')
34+
const ext = (parts[parts.length - 1] || '').trim().toLowerCase()
35+
36+
if (!opts.strict || extWhitelist.has(ext)) {
37+
return ext
38+
}
39+
}

0 commit comments

Comments
 (0)