etro-node
Version:
A wrapper to run Etro in Node
117 lines (108 loc) • 3.78 kB
JavaScript
const util = require('util')
const path = require('path')
const fs = require('fs')
const puppeteer = require('puppeteer')
const mime = require('mime')
const redirectConsoleOutput = page => {
// Redirect page's console output and errors to terminal
page.on('console', consoleObj => {
if (consoleObj.type() in console) {
console[consoleObj.type()](consoleObj.text())
} else {
console.log(consoleObj.text())
}
})
page.on('pageerror', error => {
throw util.inspect(error)
})
}
var exec = require('child_process').exec;
/**
* Retrieve the URL and type of a media source
* @param {string|RawSource} source
*/
const getMetadata = async (page, id, source) => {
let data
let type
if (typeof source === 'string') {
// Source is a path. Read into a new buffer.
data = fs.readFileSync(source, 'base64')
const extension = source.substring(source.indexOf('.') + 1)
type = mime.getType(extension)
} else {
// Source is an object of the form {type: string, data: Buffer}
data = source.data.toString('base64')
type = source.type
}
// Prepend base64 header
url = `data:${type};base64,${data}`
return { url, type }
}
/**
* Runs etroFunction in a browser with inputSources loaded as html elements.
* @param {function} etroFunction
* @param {Object.<string, <string|RawSource>>} inputSources - the input assets, mapped from id to path or raw data
* @param {string|function} resultCallbackOrPath the - output path or callback
* @param {Page} [page] - an existing puppeteer page to use
* @return {Promise} a promise that resolves after the exported video has been written or processed
*
* @typedef {Object} RawSource
* @property {string} type - the MIME type of the media
* @property {Buffer} data
*/
const etroNode = async (etroFunction, inputSources, resultCallbackOrPath, page=undefined) => {
// Set up page
let browser
if (!page) {
browser = await puppeteer.launch()
page = page || await browser.newPage()
}
redirectConsoleOutput(page)
await page.goto(`file://${path.join(__dirname, 'page.html')}`)
await page.evaluate(() => {
window._doneExporting = false
})
// Send assets using data urls
for (let id in inputSources) {
const source = inputSources[id]
const { url, type } = await getMetadata(page, id, source)
await page.evaluate(async (id, type, url) => {
const waitUntilLoaded = () => new Promise((resolve, reject) => {
let tagname = type.substring(0, type.indexOf('/'))
if (tagname === 'image')
tagname = 'img'
const el = document.createElement(tagname)
el.id = id
el.src = url
if (el instanceof window.HTMLMediaElement)
el.onloadeddata = resolve
else if (el instanceof window.HTMLImageElement)
el.onload = resolve
else
reject(`unhandled asset type: ${el.tagName}`)
document.body.appendChild(el)
})
await waitUntilLoaded().catch(e => {
throw e
})
}, id, type, url)
}
// Define `done` function (to be called when the video is done exporting)
// I wish I could avoid using globals for this, but I don't know how to pass it to etroFunction
// correctly (without causing problems).
await page.exposeFunction('done', byteArray => {
const buffer = Buffer.from(byteArray)
if (typeof resultCallbackOrPath === 'function')
resultCallbackOrPath(buffer)
else
fs.writeFileSync(resultCallbackOrPath, buffer, { encoding: 'binary' })
page.evaluate(() => {
window._doneExporting = true
})
})
await page.evaluate(etroFunction)
await page.waitFor(() => window._doneExporting, { timeout: 0 })
if (browser)
await browser.close()
}
module.exports = etroNode