-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.mjs
157 lines (136 loc) · 3.89 KB
/
server.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import express from 'express'
import fs from 'fs/promises'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { spawn } from 'child_process'
import c from 'chalk'
import os from 'os'
import multer from 'multer'
const getDirname = (importMetaUrl) => dirname(fileURLToPath(importMetaUrl))
const app = express()
const port = 3000
const upload = multer({
limits: {
// 50MB per field
fieldSize: 50 * 1024 * 1024,
// Can handle up to 10000 frames (about 5-6 minutes at 30fps)
fieldsLimit: 10000,
fieldNameSize: 100,
},
})
const log = (...args) => {
console.info(
...args.map((arg) => (typeof arg === 'string' ? c.cyan(arg) : arg)),
)
}
app.use(express.static('src'))
app.use('/modules', express.static('node_modules'))
app.use('/assets', express.static('assets'))
app.use(
express.json({
limit: 1e9,
}),
)
app.get('/', (req, res) => {
res.sendFile(`${getDirname(import.meta.url)}/src/index.html`)
})
app.get('/sketches', async (req, res) => {
res.send(
(await fs.readdir('./src/sketches')).filter((file) =>
file.endsWith('.mjs'),
),
)
})
app.post('/upload-frames', upload.none(), async (req, res) => {
try {
const { name, frameRate } = req.body
const frames = Object.keys(req.body)
.filter((key) => key.startsWith('frame-'))
.map((key) => req.body[key])
if (frames.length === 0) {
return res.status(400).send({ message: 'No frames received' })
}
const timestamp = prettyDate(new Date())
const dirPath = `${os.homedir()}/Movies/p5/${name}-${timestamp}`
await fs.mkdir(dirPath, { recursive: true })
const totalFrames = frames.length
const padding = String(totalFrames).length // Calculate padding length based on total frames
res.send({
message: 'Frames uploaded. View server logs for status',
path: dirPath,
})
log('writing files')
await Promise.all(
frames.map(async (frameDataUrl, index) => {
const base64Data = frameDataUrl.replace(/^data:image\/png;base64,/, '')
const paddedIndex = String(index).padStart(padding, '0')
const filePath = `${dirPath}/frame-${paddedIndex}.png`
await fs.writeFile(filePath, base64Data, 'base64')
}),
)
log('Files written')
log('Converting to mp4')
await convertImagesToMP4({
imagesPath: dirPath,
totalFrames,
frameRate,
})
} catch (error) {
console.error('Error saving frames:', error)
res.status(500).send({
message: 'Error saving frames',
error: error.toString(),
})
}
})
async function convertImagesToMP4({ imagesPath, totalFrames, frameRate }) {
const mp4Pathname = `${imagesPath}.mp4`
return new Promise((resolve, reject) => {
const childProcess = spawn(
'ffmpeg',
[
'-framerate',
// Set the desired frame rate (adjust as needed)
frameRate,
'-i',
// Input image sequence
`${imagesPath}/frame-%0${String(totalFrames).length}d.png`,
'-vf',
// Set the output video resolution (adjust as needed)
'scale=1080x1080',
'-vcodec',
// Use H.264 video codec
'libx264',
'-pix_fmt',
// Ensures compatibility with most players
'yuv420p',
// Output MP4 file
mp4Pathname,
],
{
stdio: 'inherit',
shell: true,
},
)
childProcess.on('close', (code) => {
if (code === 0) {
log('Done. File available at', mp4Pathname)
resolve(mp4Pathname)
} else {
reject(
new Error(`FFmpeg process closed with a non-zero exit code ${code}`),
)
}
})
})
}
app.listen(port, () => {
log(`app listening at http://localhost:${port}`)
})
function prettyDate(date) {
const iso = date.toISOString()
let [d, t] = iso.split('T')
d = d.replace(/-/g, '')
t = t.slice(0, t.indexOf('.')).replace(/:/g, '')
return `${d}-${t}`
}