-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
227 lines (189 loc) · 7.67 KB
/
server.js
File metadata and controls
227 lines (189 loc) · 7.67 KB
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/**
* @file This is the back-end server side code. Currently, implemented API are listed below:
* Stream API: /stream?id=<youtubeId>
* @author NanKe
*/
const express = require('express');
const ytdl = require('ytdl-core');
const https = require('https');
const { installHandler } = require('./graphQL_handler.js');
const { connectToDb } = require('./db.js');
const app = express();
// handle env variable here
const PORT = process.env.PORT || 7777;
/**
* Route for stream API, return a readable stream to client
*/
app.get('/stream', (req, res) => {
let youtubeId = req.query.id;
let ip = req.ip;
let resLock = false;
// default filter
let filter = 'audioonly';
// this filter drop the result that is dashMPD, which doesn't have response event
// let filter = format => format.isDashMPD === false ;
let ua = req.get('User-Agent');
let isSafari = ua.indexOf('Safari') > -1 && ua.indexOf('Chrome') === -1;
// dectect safari, then change the filter
if (isSafari) {
console.log(`request by safari`);
filter = format => format.container === 'mp4';
}
if (youtubeId) {
console.log(`${new Date()}: Get request for video ${youtubeId} from ${ip}`);
let stream;
let start, end;
let range = req.get('Range');
// if request contain Range option in header, handle range option
if (range) {
// ! debug, logging req range
console.log(`Range ${range}`);
let positions = range.replace(/bytes=/, "").split("-");
start = parseInt(positions[0], 10);
if (positions.length > 1) end = parseInt(positions[1], 10);
if (!Number.isNaN(start) && end) {
stream = ytdl(`https://www.youtube.com/watch?v=${youtubeId}`, { quality: 'highestaudio', range: { start, end }, filter });
} else {
stream = ytdl(`https://www.youtube.com/watch?v=${youtubeId}`, { quality: 'highestaudio', range: { start }, filter });
}
} else {
stream = ytdl(`https://www.youtube.com/watch?v=${youtubeId}`, { quality: 'highestaudio', filter });
}
// there is a possible bug for response event, it can emitted twice for a single request
// set up header when Event:reaponse emitted(video response has been found)
stream.on('response', Ytbres => {
// set content length to indicate server support range header
// use the header from youtbe in response
// if this is the first response event(we don't want to double response to client)
if (!resLock) {
// lock, prevent double response
resLock = true;
res.set(Ytbres.headers);
res.status(Ytbres.statusCode);
stream.pipe(res);
}
// ! Debug code for header/statusCode
console.log(`----Response Header for request ${youtubeId}----`);
console.log(`StatusCode: ${Ytbres.statusCode}`);
console.log(Ytbres.headers);
console.log(`----Response Header End----`);
});
// error handling for readable stream
stream.on('error', err => {
console.log(`${new Date()}: Youtube readable stream error, video id=${youtubeId}`);
console.log('----Error Info----');
console.log(err);
console.log('----Error Info End----');
});
// error handling for writable stream (response)
res.on('error', err => {
console.log(`${new Date()}: Error in response stream, video id=${youtubeId}`);
});
// log video info, handle the DashMPD format(no response event)
stream.on('info', function (info, format) {
console.log('----Audio information----');
console.log(`Title=${info.videoDetails.title}`);
console.log(`audioQuality=${format.audioQuality} audioBitrate=${format.audioBitrate} audioCodec=${format.audioCodec}`);
console.log(`container=${format.container} isDashMPD=${format.isDashMPD} isLive=${format.live}`)
console.log('----Audio info end----')
// if is DashMPD format and not live video, we should set our header
if (format.isDashMPD && !format.live) {
// get url from info, match the itag
let resFormat = info.player_response.streamingData.adaptiveFormats;
let url, contentLength, lastModified;
resFormat.forEach(element => {
if (element.itag == format.itag) {
// !debug, log format info
// console.log(element)
url = element.url;
contentLength = element.contentLength;
lastModified = element.lastModified;
}
});
if (url) {
// if we have range request, and not from 0, we should specified it on headers
if (start !== 0) {
let option = {
headers: {
'if-range': new Date(parseInt(lastModified, 10) / 1000).toUTCString(),
}
};
// check if we have end range
if (!Number.isNaN(end)) {
option.headers['Range'] = `bytes=${start}-${end}`;
} else {
// we should fill it with contentlength
option.headers['Range'] = `bytes=${start}-${parseInt(contentLength, 10) - 1}`;
}
// !debug, log request header
// console.log(option.headers);
// shot the request
https.get(url, option, yRes => {
// error handling for response from youtube (readable stream)
yRes.on('error', e => {
console.log(`${new Date()}: Error on response from youtube`);
});
// log respose for debug
console.log(`----Response Header for request ${youtubeId}----`)
console.log(`StatusCode: ${yRes.headers}`);
console.log(yRes.statusCode);
console.log(`----Response Header End----`)
res.set(yRes.headers);
res.status(yRes.statusCode);
// all set, pipe youtube response to client
yRes.pipe(res);
}).on('error', e => {
console.log(`${new Date()}: Error on sending get request to youtube`);
});
} else {
// if no range request, send request directly
https.get(url, (yRes) => {
// error handling for response from youtube (readable stream)
yRes.on('error', e => {
console.log(`${new Date()}: Error on response from youtube`);
});
// log respose for debug
console.log(`----Response Header for request ${youtubeId}----`)
console.log(`StatusCode: ${yRes.headers}`);
console.log(yRes.statusCode);
console.log(`----Response Header End----`)
res.set(yRes.headers);
res.status(yRes.statusCode);
yRes.pipe(res);
}).on('error', e => {
console.log(`${new Date()}: Error on sending get request to youtube`);
});
}
} else {
// if no url get, we cannot offer content
res.status(400).send('Bad Request');
}
}
})
// stream.on('progress', (chunkLength, downloaded, total) => {
// console.log(chunkLength);
// console.log(total);
// });
// !debug, log when stream end
stream.on('end', () => {
console.log(`${new Date()}: streaming to ${ip} end, video id=${youtubeId}`);
});
// !debug, log when stream close
stream.on('close', () => {
console.log(`${new Date()}: streaming to ${ip} closed, video id=${youtubeId}`);
});
} else {
res.status(400).send('Bad Request');
}
});
installHandler(app);
(async function start() {
try {
await connectToDb();
app.listen(PORT, () => {
console.log(`${new Date()}: API server started on port ${PORT}`);
});
} catch (err) {
console.log('ERROR:', err);
}
}());