Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add HTML Renderer #193

Merged
merged 30 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8610b6e
feat: add HTML Renderer
nytamin Jul 11, 2024
7d36627
fix: fix issue in HelpfulEventEmitter where it logged false errors
nytamin Jul 11, 2024
92c575f
fix: allow baseUrl to be optional for HTTP Accessor
nytamin Jul 11, 2024
e98854c
fix: add option to HTTP Accessor to either send a HEAD or GET to retr…
nytamin Jul 11, 2024
7ee0aa7
chore: use prerelease R50.4 Core types
nytamin Jul 11, 2024
3f14443
chore: bug fix in regex
nytamin Jul 11, 2024
0546c72
chore: minor fixes after code review
nytamin Aug 20, 2024
468e3f5
chore: rename RenderHTML.ts
nytamin Aug 20, 2024
56c1b1b
chore: rename renderHTML.ts
nytamin Aug 20, 2024
c3775a7
fix: html-renderer: add support for transparent backgrounds
nytamin Aug 20, 2024
b7031b2
chore: doc
nytamin Aug 20, 2024
0ad92d1
chore: refactor RengerHTML code
nytamin Aug 20, 2024
e3e0242
fix: html-renderer: bug in background color and cropping
nytamin Aug 20, 2024
8057950
chore: expectedPackage.json
nytamin Aug 20, 2024
c75757e
fix: rename supperHEAD to useGETinsteadOfHead
nytamin Aug 21, 2024
0e589bf
fix: allow stringified JSON object as storeObject value. Also add som…
nytamin Aug 21, 2024
7fcdf70
fix: change how render width, height and scale works for html_templat…
nytamin Aug 21, 2024
b40bf2d
chore: update inputAPI definitions
nytamin Aug 21, 2024
43e9887
fix: remove outputPrefix for html-template expectations
nytamin Aug 23, 2024
9a2d6fd
chore: move some helper functions so that they can be shared with Sof…
nytamin Aug 23, 2024
d342bc7
chore: lint
nytamin Aug 23, 2024
e964533
Merge branch 'master' into feat/html-rendering
nytamin Aug 23, 2024
77a7a28
chore: update code dep
nytamin Aug 23, 2024
1b06fbb
fix: pin electron version. fix build of html-renderer on linux
Julusian Aug 23, 2024
40e12d3
fix: html-renderer lookup paths on linux
Julusian Aug 23, 2024
f3164b4
fix: run HTMLRenderer using `yarn start` script when in development mode
nytamin Aug 26, 2024
9bc6a52
fix: default to png as screenshot
nytamin Aug 26, 2024
ca54b28
chore: yarn.lock
nytamin Aug 26, 2024
5817e25
chore: refactor to DRY and fix an issue in unit tests
nytamin Aug 26, 2024
5fba682
chore: refactor to avoid regex
nytamin Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ ffprobe.exe
apps/single-app/app/expectedPackages.json_smartbull
.ffmpeg/
signtool.exe
apps/single-app/app/tmp/
apps/single-app/app/tmpRenderHTML/
3 changes: 2 additions & 1 deletion apps/appcontainer-node/packages/generic/src/appContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,8 @@ export class AppContainer {
availableApp: AvailableAppInfo,
useCriticalOnlyMode: boolean
): cp.ChildProcess {
const cwd = process.execPath.match(/node.exe$/)
const isRunningInDevelopmentMode = process.execPath.endsWith('node.exe') || process.execPath.endsWith('node')
const cwd = isRunningInDevelopmentMode
? undefined // Process runs as a node process, we're probably in development mode.
: path.dirname(process.execPath) // Process runs as a node process, we're probably in development mode.

Expand Down
4 changes: 4 additions & 0 deletions apps/html-renderer/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ffmpeg.exe
ffprobe.exe
log.log
deploy/*
73 changes: 73 additions & 0 deletions apps/html-renderer/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# HTML Renderer

## How to run

### Default CasparCG template

_This is used for a HTML file that follows the typical CasparCG lifespan (`update(data); play(); stop()`)_

```bash
html-renderer.exe -- --url=file://C:/templates/mytemplate.html outputPath="C:\\rendered" --screenshots=true --recording=true --recording-cropped=true --casparData='{"name":"John Doe"}' --casparDelay=1000
```

### Generic HTML template

_This is used for a HTML file that doesn't require any additional input during its run._

```bash
html-renderer.exe -- --url=https://bouncingdvdlogo.com outputPath="C:\\rendered" --screenshots=true --recording=true --recording-cropped=false --genericWaitIdle=1000 --genericWaitPlay=1000 --genericWaitStop=1000 --width=480 --height=320 --zoom=0.25
```

### Interactive mode

_This is used for HTML templates that require manual handling (like, external API calls need to be made)_

```bash
html-renderer.exe -- --url=https://bouncingdvdlogo.com outputPath=C:\\rendered --interactive=1
```

In interactive mode, commands are sent to the renderer via the console. The following commands are available:

```json

// Wait for this message before sending interactive messages.
{ "status": "ready" }


// Wait for the load event to be fired
{ "do": "waitForLoad" }
// Reply:
{ "reply": "waitForLoad" }

// Take a screenshot and save it as PNG
{ "do": "takeScreenshot", "fileName": "screenshot.png" }
// Reply:
{ "reply": "takeScreenshot" }
{ "reply": "takeScreenshot", "error": "Unable to write file \"screenshot.png\"" }

// Start recording
{ "do": "startRecording", "fileName": "recording.webm" }
// Reply:
{ "reply": "startRecording" }
{ "reply": "startRecording", "error": "Recording already started" }


// Stop recording
{ "do": "stopRecording" }
// Reply:
{ "reply": "stopRecording"}
{ "reply": "stopRecording", "error": "Unable to write file \"recording.webm\"" }

// Analyze the recording and crop it to only include the region with content
{ "do": "cropRecording", "fileName": "recording-cropped.webm" }
// Reply:
{ "reply": "cropRecording" }
{ "reply": "cropRecording", "error": "No recording found" }

// Execute javascript in the renderer
{ "do": "executeJs", "js": "update(\"myData\")" }
// Reply:
{ "reply": "executeJs" }
{ "reply": "executeJs", "error": "Some error" }

```
7 changes: 7 additions & 0 deletions apps/html-renderer/app/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const base = require('../../../jest.config.base')
const packageJson = require('./package')

module.exports = {
...base,
displayName: packageJson.name,
}
76 changes: 76 additions & 0 deletions apps/html-renderer/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "@html-renderer/app",
"version": "1.50.5",
"description": "HTML-renderer",
"private": true,
"main": "dist/index.js",
"scripts": {
"build": "yarn rimraf dist && yarn build:main",
"build:main": "tsc -p tsconfig.json",
"build-win32": "yarn prepare-build-win32 && electron-builder && yarn post-build-win32",
"prepare-build-win32": "node scripts/prepare_build.js",
"post-build-win32": "node scripts/post_build.js",
"__test": "jest",
"start": "electron dist/index.js"
},
"prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
"engines": {
"node": ">=18"
},
"lint-staged": {
"*.{js,css,json,md,scss}": [
"prettier"
],
"*.{ts,tsx}": [
"eslint"
]
},
"peerDependencies": {
"ws": "*"
},
"dependencies": {
"@html-renderer/generic": "1.50.5",
"@sofie-automation/shared-lib": "1.51.0-nightly-fix-pm-types-html-template-20240823-061828-d90c220.0",
"@sofie-package-manager/api": "1.50.6",
"portfinder": "^1.0.32",
"tslib": "^2.1.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/ws": "^8.5.4",
"archiver": "^7.0.1",
"electron": "30.0.6",
"electron-builder": "^24.13.3",
"lerna": "^6.6.1",
"rimraf": "^5.0.5"
},
"build": {
"productName": "html-renderer",
"appId": "no.nrk.sofie.html-renderer",
"win": {
"extraFiles": [],
"target": [
{
"target": "portable",
"arch": [
"x64"
]
}
]
},
"linux": {
"target": "dir",
"executableName": "html-renderer",
"extraFiles": []
},
"files": [
"dist/**/*"
],
"portable": {
"artifactName": "html-renderer.exe"
},
"directories": {
"output": "deploy"
}
}
}
46 changes: 46 additions & 0 deletions apps/html-renderer/app/scripts/post_build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable no-console, node/no-unpublished-require */
const fs = require('fs')
const path = require('path')
const archiver = require('archiver')

/*
* This script gathers the built files from electron-builder and zips them into a zip file
*/

async function main() {
const myDir = path.resolve('.')
const deployDir = path.join(myDir, 'deploy')
const archiveDir = path.join(deployDir, 'win-unpacked')

const zipFile = path.join(deployDir, 'html-renderer.zip')

await new Promise((resolve, reject) => {
const output = fs.createWriteStream(zipFile)
const archive = archiver('zip', {
zlib: { level: 5 }, // Sets the compression level.
})
output.on('close', function () {
console.log(archive.pointer() + ' total bytes')
resolve()
})
archive.on('warning', function (err) {
if (err.code === 'ENOENT') console.log(`WARNING: ${err}`)
else reject(err)
})
archive.on('error', reject)
archive.pipe(output)

console.log(`Archiving ${archiveDir}`)
archive.directory(archiveDir, false)

archive.finalize()
})
console.log('Zipping done, removing temporary artifacts...')

// Remove the archived directory
await fs.promises.rm(archiveDir, { recursive: true })
await fs.promises.rm(path.join(deployDir, 'html-renderer.exe'), { recursive: true })
await fs.promises.rm(path.join(myDir, 'node_modules'), { recursive: true })
}

main().catch(console.error)
32 changes: 32 additions & 0 deletions apps/html-renderer/app/scripts/prepare_build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const fs = require('fs')
const path = require('path')
/* eslint-disable no-console */

/*
* This script copies some dependencies from the main node_modules folder to the node_modules folder of this project.
* So that electron-builder includes them when building the executable.

*/

async function main() {
// Things to copy:

const baseDir = path.resolve('../../..')
const myDir = path.resolve('.')

const libsToCopy = ['tslib', '@sofie-automation']

// Create node_modules folder
await fs.promises.mkdir(path.join(myDir, 'node_modules'), { recursive: true })

for (const lib of libsToCopy) {
const src = path.join(baseDir, `node_modules/${lib}`)
const target = path.join(myDir, `node_modules/${lib}`)
console.log(`Copying ${src} to ${target}`)
await fs.promises.cp(src, target, {
recursive: true,
})
}
}

main().catch(console.error)
7 changes: 7 additions & 0 deletions apps/html-renderer/app/src/__tests__/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
describe('tmp', () => {
test('tmp', () => {
// Note: To enable tests in this package, ensure that the "test" script is present in package.json
expect(1).toEqual(1)
})
})
export {}
117 changes: 117 additions & 0 deletions apps/html-renderer/app/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import yargs = require('yargs/yargs')
import {
ProcessConfig,
getProcessConfig,
processOptions,
defineArguments,
getProcessArgv,
} from '@sofie-package-manager/api'

/*
* This file contains various CLI argument definitions, used by the various processes that together constitutes the Package Manager
*/

/** Generic CLI-argument-definitions for any process */
const htmlRendererOptions = defineArguments({
url: { type: 'string', describe: 'URL or path to the file to be rendered' },
width: { type: 'number', describe: 'Width of the HTML renderer (default: 1920)' },
height: { type: 'number', describe: 'Width of the HTML renderer (default: 1080)' },
zoom: { type: 'number', describe: 'Zoom factor of the HTML renderer (default: 1)' },
background: {
type: 'string',
describe: 'Background color, #RRGGBB, CSS-string or "transparent" or "default" (defaults: "default")',
},
outputPath: { type: 'string', describe: 'File path to where the output files will be saved' },
tempPath: { type: 'string', describe: 'File path to where temporary files will be saved (default: "tmp")' },
screenshots: { type: 'boolean', describe: 'When true, will capture screenshots' },
recording: { type: 'boolean', describe: 'When true, will capture recording' },
'recording-cropped': {
type: 'boolean',
describe: 'When true, will capture a recording cropped to the non-black area',
},
casparData: {
type: 'string',
describe:
'(JSON) data to send into the update() function of a CasparCG Template. This needs to be a stringified JSON object, a string (in quotes) etc..',
},
casparDelay: {
type: 'number',
describe: 'How long to wait between each action in a CasparCG template (default: 1000ms)',
},
genericWaitIdle: {
type: 'number',
describe: 'For a generic HTML template, how long to wait before considering it idle',
},
genericWaitPlay: {
type: 'number',
describe: 'For a generic HTML template, how long to wait before considering it playing',
},
genericWaitStop: {
type: 'number',
describe: 'For a generic HTML template, how long to wait before considering it stopped',
},
interactive: {
type: 'boolean',
describe: 'When true, will start the process in interactive mode. See Readme for docs.',
},
test: {
type: 'boolean',
describe: 'When true, will simply trace the version and then close.',
},
})

export interface HTMLRendererOptionsConfig {
url: string | undefined
width: number | undefined
height: number | undefined
zoom: number | undefined
background: string | undefined
outputPath: string | undefined
tempPath: string | undefined
screenshots: boolean | undefined
recording: boolean | undefined
'recording-cropped': boolean | undefined
casparData: string | undefined
casparDelay: number | undefined
genericWaitIdle: number | undefined
genericWaitPlay: number | undefined
genericWaitStop: number | undefined
interactive: boolean | undefined
test: boolean | undefined
}
export async function getHTMLRendererConfig(): Promise<{
process: ProcessConfig
htmlRenderer: HTMLRendererOptionsConfig
}> {
const argv = await Promise.resolve(
yargs(getProcessArgv()).options({
...processOptions,
...htmlRendererOptions,
}).argv
)

return {
process: getProcessConfig(argv),
htmlRenderer: {
url: argv.url,
width: argv.width,
height: argv.height,
zoom: argv.zoom,
background: argv.background,
outputPath: argv.outputPath,
tempPath: argv.tempPath,
screenshots: argv.screenshots,
recording: argv.recording,
'recording-cropped': argv['recording-cropped'],
casparData: argv.casparData,
casparDelay: argv.casparDelay,
genericWaitIdle: argv.genericWaitIdle,
genericWaitPlay: argv.genericWaitPlay,
genericWaitStop: argv.genericWaitStop,
interactive: argv.interactive,
test: argv.test,
},
}
}

// ---------------------------------------------------------------------------------
Loading