Skip to content

Instantly share code, notes, and snippets.

@demogoran
Created July 1, 2020 12:52
Show Gist options
  • Save demogoran/04dfa15f018f8937399af55f7e7ffed6 to your computer and use it in GitHub Desktop.
Save demogoran/04dfa15f018f8937399af55f7e7ffed6 to your computer and use it in GitHub Desktop.
// Modules to control application life and create native browser window
const {app, BrowserWindow, protocol} = require('electron')
const path = require("path");
const fs = require("fs");
const fsPromises = require('fs').promises
const url = require("url");
const { Transform, Stream } = require('stream');
var Koa = require('koa');
var route = require('koa-route');
// MODIFICATION PART
class ModifyFSStream extends Transform{
_transform(chunk, enc, callback){
console.log(chunk.length);
// JUST MODIFY CHUNK AS REQUIRED
callback(null, chunk);
}
}
function rangeParse (str) {
const token = str.split('=')
if (!token || token.length !== 2 || token[0] !== 'bytes') {
return null
}
return token[1]
.split(',')
.map((range) => {
return range.split('-').map((value) => {
if (value === '') {
return Infinity
}
return Number(value)
})
})
.filter((range) => {
return !isNaN(range[0]) && !isNaN(range[1]) && range[0] <= range[1]
})
}
var koa = new Koa();
koa.use(route.get('/videos/:fileName', async function (ctx, fileName) {
let range = ctx.header.range
if (!range) {
return
}
const p = path.join(__dirname, fileName);
console.log('p', p);
const ranges = rangeParse(range)
let [start, end] = ranges[0]
const stats = await fsPromises.lstat(p)
const fileSize = stats.size
ctx.status = 206
end = end === Infinity ? fileSize - 1 : end;
ctx.set('Content-Type', 'video/mp4')
ctx.set('Accept-Ranges', 'bytes')
ctx.set('Content-Range', `bytes ${start}-${end}/${fileSize}`)
ctx.set('Content-Length', end - start + 1)
// highWaterMark is chunk size
ctx.body = fs.createReadStream(p, { start, end, highWaterMark: 1024 }).pipe(new ModifyFSStream());
}));
koa.listen(55123);
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
protocol.registerSchemesAsPrivileged([
{scheme: 'custom', privileges: {standard: true, secure: true}},
{scheme: 'customfile', privileges: {standard: true, secure: true}},
]);
function createWindow () {
protocol.registerStreamProtocol("custom", (request, callback) => {
let requestUrl = url.parse(request.url, /*parseQueryString*/ true, /*slashesDenoteHost*/ true);
let filePath = requestUrl.pathname.replace(/^\/static/, path.join(__dirname).replace(/[?#&].*/, ''));
if (filePath.endsWith(".mp4")) {
let options = {};
let contentLength = 0;
const range = request.headers.Range.replace('bytes=', '').split('-');
if (range.length !== 2) {
// No range header or unknown range header format, read whole file
contentLength = fs.statSync(filePath).size;
} else {
let start = +range[0];
// Generate options object to tell createReadStream which parts to read
if (range[1].length > 0) {
options = {start: start, end: +range[1]};
contentLength = +range[1] - start;
} else {
options = {start: start};
contentLength = fs.statSync(filePath).size - start;
}
}
let stream = fs.createReadStream(filePath, options);
// Here, the stream could be modified, e.g. via stream.pipe(...)
// without having to process the whole file before starting to play it.
// However, even when only passing the stream to the browser,
// the video cannot be viewed anymore
// This _did_ work in electron 6, but does not in electron 7/8/9.
// You can try it by replacing the electron version with ^6.0.0 in package.json
callback({
statusCode: 200,
headers: {
'Content-Type': 'video/mp4',
'Content-Length': '' + contentLength,
},
data: stream
})
} else if (filePath.endsWith(".png")) {
callback({
statusCode: 200,
headers: {
'Content-Type': 'image/png',
},
data: fs.createReadStream(filePath)
})
} else {
callback({
statusCode: 200,
headers: {
'Content-Type': 'text/html',
},
data: fs.createReadStream(filePath)
})
}
})
protocol.registerFileProtocol('customfile', (request, callback) => {
let requestUrl = url.parse(request.url, /*parseQueryString*/ true, /*slashesDenoteHost*/ true);
let filePath = requestUrl.pathname.replace(/^\/static/, path.join(__dirname).replace(/[?#&].*/, ''));
callback({
path: filePath
});
})
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
mainWindow.loadURL('custom://test/static/index.html')
// Open the DevTools.
mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment