Files
speckle-server/packages/preview-service/routes/preview.js
T

182 lines
5.3 KiB
JavaScript

/* eslint-disable no-undef */
'use strict'
const express = require('express')
const router = express.Router()
const puppeteer = require('puppeteer')
const { logger } = require('../observability/logging')
const { reduce } = require('lodash')
const shouldBeHeadless = process.env.PREVIEWS_HEADED !== 'true'
const getChromiumExecutablePath = () =>
process.env.CHROMIUM_EXECUTABLE_PATH || '/usr/bin/google-chrome-stable'
const getPuppeteerUserDataDir = () => process.env.USER_DATA_DIR || '/tmp/puppeteer'
async function pageFunction(objectUrl) {
waitForAnimation = async (ms = 70) =>
await new Promise((resolve) => {
setTimeout(resolve, ms)
})
const ret = {
duration: 0,
mem: 0,
scr: {}
}
const t0 = Date.now()
await window.v.init()
try {
await window.v.loadObjectAsync(objectUrl)
} catch {
// Main call failed. Wait some time for other objects to load inside the viewer and generate the preview anyway
await waitForAnimation(1000)
}
window.v.resize()
window.v.zoom(undefined, 0.95, false)
await waitForAnimation(100)
for (let i = 0; i < 24; i++) {
window.v.setView({ azimuth: Math.PI / 12, polar: 0 }, false)
window.v.getRenderer().resetPipeline(true)
/** Not sure what the frame time when running pupeteer is, but it's not 16ms.
* That's why we're allowing more time between frames than probably needed
* In a future update, we'll have the viewer signal when convergence is complete
* regradless of how many frames/time that takes
*/
/** 22.11.2022 Alex: Commenting this out for now */
// await waitForAnimation(2500)
await waitForAnimation()
ret.scr[i + ''] = await window.v.screenshot()
}
ret.duration = (Date.now() - t0) / 1000
ret.mem = {
total: performance.memory.totalJSHeapSize,
used: performance.memory.usedJSHeapSize
}
ret.userAgent = navigator.userAgent
return ret
}
async function getScreenshot(objectUrl, boundLogger = logger) {
const launchParams = {
headless: shouldBeHeadless,
userDataDir: getPuppeteerUserDataDir(),
executablePath: getChromiumExecutablePath(),
protocolTimeout: 3600_000,
// we trust the web content that is running, so can disable the sandbox
// disabling the sandbox allows us to run the docker image without linux kernel privileges
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
}
const browser = await puppeteer.launch(launchParams)
const page = await browser.newPage()
const wrapperPromise = (async () => {
await page.goto('http://127.0.0.1:3001/render/')
boundLogger.info('Page loaded')
// Handle page crash (oom?)
page.on('error', (err) => {
throw err
})
return await page.evaluate(pageFunction, objectUrl)
})()
let ret = null
try {
ret = await wrapperPromise
} catch (err) {
boundLogger.error(err, 'Error generating preview.')
ret = {
error: err
}
}
// Don't await for cleanup
browser.close()
if (ret.error) {
return null
}
boundLogger.info(
{
durationSeconds: ret.duration,
totalMemoryMB: ret.mem.total / 1000000,
resultingImages: {
count: Object.keys(ret.scr || {}).length,
totalStringSize: reduce(ret.scr || {}, (acc, val) => acc + val.length, 0)
}
},
`Generated preview.`
)
return ret.scr
// return `
// <html><body>
// <div>Generated by: ${ret.userAgent}</div>
// <div>Duration in seconds: ${ret.duration}</div>
// <div>Memory in MB: ${ret.mem.total / 1000000}</div>
// <div>Used Memory in MB: ${ret.mem.used / 1000000}</div>
// <img height="200px" src="${ret.scr['-2']}" /><br />
// <img height="200px" src="${ret.scr['-1']}" /><br />
// <img height="200px" src="${ret.scr['0']}" /><br />
// <img height="200px" src="${ret.scr['1']}" /><br />
// <img height="200px" src="${ret.scr['2']}" /><br />
// </body></html>
// `
// const imageBuffer = new Buffer.from(
// b64Image.replace(/^data:image\/\w+;base64,/, ''),
// 'base64'
// )
// // await page.waitForTimeout(500);
// //var response = await page.screenshot({
// // type: 'png',
// // clip: {x: 0, y: 0, width: 800, height: 800}
// //});
// return imageBuffer
}
router.get('/:streamId/:objectId', async function (req, res) {
const safeParamRgx = /^[\w]+$/i
const { streamId, objectId } = req.params || {}
const boundLogger = logger.child({ streamId, objectId })
if (!safeParamRgx.test(streamId) || !safeParamRgx.test(objectId)) {
return res.status(400).json({ error: 'Invalid streamId or objectId!' })
}
const objectUrl = `http://127.0.0.1:3001/streams/${req.params.streamId}/objects/${req.params.objectId}`
/*
let authToken = ''
let authorizationHeader = req.header( 'Authorization' )
if ( authorizationHeader && authorizationHeader.toLowerCase().startsWith( 'bearer ' ) ) {
authToken = authorizationHeader.Substring( 'Bearer '.Length ).Trim()
}
// useful for testing (not the recommended way of passing the auth token)
if ( req.query.authToken ) {
authToken = req.query.authToken
}
*/
boundLogger.info('Requesting screenshot.')
const scr = await getScreenshot(objectUrl, boundLogger)
if (!scr) {
return res.status(500).end()
}
// res.setHeader( 'content-type', 'image/png' )
res.send(scr)
})
module.exports = router