Skip to content

Commit 4535f36

Browse files
committed
added video upload
1 parent 8124714 commit 4535f36

File tree

1 file changed

+245
-27
lines changed

1 file changed

+245
-27
lines changed

server/endpoints/video.ts

Lines changed: 245 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,265 @@
1+
import 'dotenv/config';
12
import express from 'express';
3+
import fileUpload from 'express-fileupload';
24
import fs from 'fs';
3-
import {getDb} from '../db/mongo';
5+
import {spawn} from 'child_process';
6+
import {ObjectId} from 'mongodb';
7+
import {getDb} from '../db/mongo.js';
48

9+
/* eslint new-cap: ["error", { "capIsNewExceptions": ["Router"] }] */
510
const video = express.Router();
611

7-
video.get('/:videoId/info', function (req, res) {
12+
video.get('/:videoId/info', function(req, res) {
813
const db = getDb();
914

10-
const uuid = req.params.videoId;
15+
const videoId = req.params.videoId;
1116

1217
db.collection('videos')
13-
.findOne({ uuid: uuid })
14-
.then((result: any) => {
15-
res.json(result);
16-
});
18+
.findOne({_id: new ObjectId(videoId)})
19+
.then((result: any) => {
20+
res.json(result);
21+
});
1722
});
1823

19-
video.get('/:videoId', function (req, res) {
20-
let range = req.headers.range;
24+
video.get('/:videoId', function(req, res) {
25+
const range = req.headers.range;
2126

2227
if (range === undefined) {
2328
res.status(400).send('Requires Range header');
2429
return;
2530
}
2631

32+
const db = getDb();
2733
const videoId = req.params.videoId;
28-
let filename = videoId;
29-
30-
const videoPath = 'store/videos/files/' + filename;
31-
const videoSize = fs.statSync('store/videos/files/' + filename).size;
32-
const CHUNK_SIZE = 10 ** 6;
33-
const start = Number(range.replace(/\D/g, ''));
34-
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
35-
const contentLength = end - start + 1;
36-
const headers = {
37-
'Content-Range': `bytes ${start}-${end}/${videoSize}`,
38-
'Accept-Ranges': 'bytes',
39-
'Content-Length': contentLength,
40-
'Content-Type': 'video/mp4',
41-
};
42-
res.writeHead(206, headers);
43-
const videoStream = fs.createReadStream(videoPath, { start, end });
44-
videoStream.pipe(res);
34+
35+
db.collection('videos')
36+
.findOne({_id: new ObjectId(videoId)})
37+
.then((result: any) => {
38+
const videoPath =
39+
`${process.env.STORAGE}/videos${result.path}/videos/${videoId}`;
40+
41+
const videoSize = fs.statSync(videoPath).size;
42+
const CHUNK_SIZE = 10 ** 6;
43+
const start = Number(range.replace(/\D/g, ''));
44+
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
45+
const contentLength = end - start + 1;
46+
const headers = {
47+
'Content-Range': `bytes ${start}-${end}/${videoSize}`,
48+
'Accept-Ranges': 'bytes',
49+
'Content-Length': contentLength,
50+
'Content-Type': 'video/mp4',
51+
};
52+
res.writeHead(206, headers);
53+
const videoStream = fs.createReadStream(videoPath, {start, end});
54+
videoStream.pipe(res);
55+
});
56+
});
57+
58+
video.post('/upload', function(req, res) {
59+
try {
60+
if (!req.files) {
61+
res.send({
62+
status: false,
63+
message: 'No file uploaded',
64+
});
65+
} else {
66+
const video = req.files.myFile as fileUpload.UploadedFile;
67+
const tempDir = process.env.STORAGE + '/temp';
68+
69+
/** Check if temp dir exists, if it doesn't, create one */
70+
if (!fs.existsSync(tempDir)) {
71+
fs.mkdirSync(tempDir);
72+
}
73+
74+
const timestamp = new Date();
75+
76+
/* Check and create storage directories */
77+
const year = timestamp.getUTCFullYear();
78+
const month = timestamp.getUTCMonth() + 1;
79+
const day = timestamp.getUTCDate();
80+
let path = `${process.env.STORAGE}/videos/${year}`;
81+
82+
if (!fs.existsSync(path)) fs.mkdirSync(path);
83+
path += `/${month}`;
84+
if (!fs.existsSync(path)) fs.mkdirSync(path);
85+
path += `/${day}`;
86+
if (!fs.existsSync(path)) fs.mkdirSync(path);
87+
88+
/* Videos directory */
89+
if (!fs.existsSync(path + '/videos')) fs.mkdirSync(path + '/videos');
90+
/* Thumbs directory */
91+
if (!fs.existsSync(path + '/thumbs')) fs.mkdirSync(path + '/thumbs');
92+
93+
const relPath = `/${year}/${month}/${day}`;
94+
95+
/** Insert video in database */
96+
const db = getDb();
97+
98+
video.mv(tempDir + '/' + video.name).then(() => {
99+
const args = [
100+
'-v',
101+
'quiet',
102+
'-print_format',
103+
'json',
104+
'-show_streams',
105+
'-select_streams',
106+
'v:0',
107+
tempDir + '/' + video.name,
108+
];
109+
110+
const proc = spawn(process.env.FFPROBE as string, args);
111+
let output = '';
112+
113+
proc.stdout.setEncoding('utf8');
114+
proc.stdout.on('data', function(data) {
115+
output += data;
116+
});
117+
118+
proc.on('close', function() {
119+
const json = JSON.parse(output);
120+
const fpsArray = json.streams[0].avg_frame_rate.split('/');
121+
const fps = parseInt(fpsArray[0]) / parseInt(fpsArray[1]);
122+
123+
db.collection('videos')
124+
.insertOne({
125+
name: video.name,
126+
path: relPath,
127+
ts: timestamp,
128+
width: json.streams[0].width,
129+
height: json.streams[0].height,
130+
fps: fps.toFixed(2),
131+
duration: json.streams[0].duration,
132+
bitrate: json.streams[0].bit_rate,
133+
status: 1,
134+
})
135+
.then((object: any) => {
136+
const videoId = object.insertedId.toString();
137+
encodeVideo(tempDir, video.name, videoId, path);
138+
139+
res.send({
140+
status: true,
141+
message: 'File is uploaded',
142+
});
143+
});
144+
});
145+
});
146+
}
147+
} catch (err) {
148+
res.status(500).send(err);
149+
}
45150
});
46151

47-
export default video;
152+
/**
153+
*
154+
* @param {string} tempDir Temporary directory where file is
155+
* @param {string} filename Video filename
156+
* @param {string} videoId Id of video in the database
157+
* @param {string} path Path where the output file should go
158+
*/
159+
function encodeVideo(
160+
tempDir: string,
161+
filename: string,
162+
videoId: string,
163+
path: string,
164+
) {
165+
console.log('encoding video...');
166+
const args = [
167+
'-y',
168+
'-i',
169+
tempDir + '/' + filename,
170+
'-codec:a',
171+
'aac',
172+
'-b:a',
173+
'44.1k',
174+
'-c:v',
175+
'libx264',
176+
'-preset',
177+
'slow',
178+
'-crf',
179+
'22',
180+
'-f',
181+
'mp4',
182+
path + '/videos/' + videoId,
183+
];
184+
185+
const proc = spawn(process.env.FFMPEG as string, args);
186+
187+
proc.stdout.on('data', function(data) {
188+
// console.log(data);
189+
});
190+
191+
proc.stderr.setEncoding('utf8');
192+
proc.stderr.on('data', function(data) {
193+
console.log(data);
194+
});
195+
196+
proc.on('close', function() {
197+
console.log('encoding finished');
198+
199+
fs.unlink(tempDir + '/' + filename, (err) => {
200+
if (err) {
201+
console.error(err);
202+
return;
203+
}
204+
205+
console.log('Temp file removed');
206+
createThumbnail(videoId, path);
207+
});
208+
});
209+
}
210+
211+
/**
212+
*
213+
* @param {string} videoId Id of video in the database
214+
* @param {string} path path where to store thumbnail
215+
*/
216+
function createThumbnail(videoId: string, path: string) {
217+
console.log('creating thumbnail');
218+
219+
const args = [
220+
'-y',
221+
'-i',
222+
path + '/videos/' + videoId,
223+
'-vf',
224+
'thumbnail,scale=320:180:force_original_aspect_ratio=increase,crop=320:180',
225+
'-frames:v',
226+
'1',
227+
'-f',
228+
'image2',
229+
'-c',
230+
'png',
231+
path + '/thumbs/' + videoId,
232+
];
233+
234+
const proc = spawn(process.env.FFMPEG as string, args);
235+
236+
proc.stdout.on('data', function(data) {
237+
console.log(data);
238+
});
239+
240+
proc.stderr.setEncoding('utf8');
241+
proc.stderr.on('data', function(data) {
242+
console.log(data);
243+
});
244+
245+
proc.on('close', function() {
246+
console.log('thumbnail created');
247+
248+
const db = getDb();
249+
250+
db.collection('videos')
251+
.updateOne(
252+
{
253+
_id: new ObjectId(videoId),
254+
},
255+
{
256+
$set: {status: 2},
257+
},
258+
)
259+
.then(() => {
260+
console.log('Status changed to 2');
261+
});
262+
});
263+
}
264+
265+
export default video;

0 commit comments

Comments
 (0)