Comparing version
153
index.js
'use strict'; | ||
const os = require('os'); | ||
const util = require('util'); | ||
const {debuglog} = require('util'); | ||
const path = require('path'); | ||
@@ -10,4 +10,6 @@ const execa = require('execa'); | ||
const electronUtil = require('electron-util/node'); | ||
const delay = require('delay'); | ||
const debuglog = util.debuglog('aperture'); | ||
const log = debuglog('aperture'); | ||
const getRandomId = () => Math.random().toString(36).slice(2, 15); | ||
@@ -18,4 +20,7 @@ // Workaround for https://github.com/electron/electron/issues/9459 | ||
const supportsHevcHardwareEncoding = (() => { | ||
if (!macosVersion.isGreaterThanOrEqualTo('10.13')) { | ||
return false; | ||
const cpuModel = os.cpus()[0].model; | ||
// All Apple silicon Macs support HEVC hardware encoding. | ||
if (cpuModel.startsWith('Apple ')) { // Source string example: `'Apple M1'` | ||
return true; | ||
} | ||
@@ -25,6 +30,9 @@ | ||
// More info: https://www.intel.com/content/www/us/en/processors/processor-numbers.html | ||
const result = /Intel.*Core.*i(?:7|5)-(\d)/.exec(os.cpus()[0].model); | ||
// Example strings: | ||
// - `Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz` | ||
// - `Intel(R) Core(TM) i7-4850HQ CPU @ 2.30GHz` | ||
const result = /Intel.*Core.*i\d+-(\d)/.exec(cpuModel); | ||
// Intel Core generation 6 or higher supports HEVC hardware encoding | ||
return result && Number(result[1]) >= 6; | ||
return result && Number.parseInt(result[1], 10) >= 6; | ||
})(); | ||
@@ -34,3 +42,3 @@ | ||
constructor() { | ||
macosVersion.assertGreaterThanOrEqualTo('10.12'); | ||
macosVersion.assertGreaterThanOrEqualTo('10.13'); | ||
} | ||
@@ -47,2 +55,4 @@ | ||
} = {}) { | ||
this.processId = getRandomId(); | ||
return new Promise((resolve, reject) => { | ||
@@ -60,13 +70,15 @@ if (this.recorder !== undefined) { | ||
if (typeof cropArea === 'object') { | ||
if (typeof cropArea.x !== 'number' || | ||
typeof cropArea.y !== 'number' || | ||
typeof cropArea.width !== 'number' || | ||
typeof cropArea.height !== 'number') { | ||
reject(new Error('Invalid `cropArea` option object')); | ||
return; | ||
} | ||
if (typeof cropArea === 'object' && | ||
( | ||
typeof cropArea.x !== 'number' || | ||
typeof cropArea.y !== 'number' || | ||
typeof cropArea.width !== 'number' || | ||
typeof cropArea.height !== 'number' | ||
) | ||
) { | ||
reject(new Error('Invalid `cropArea` option object')); | ||
return; | ||
} | ||
const recorderOpts = { | ||
const recorderOptions = { | ||
destination: fileUrl(this.tmpPath), | ||
@@ -81,3 +93,3 @@ framesPerSecond: fps, | ||
if (cropArea) { | ||
recorderOpts.cropRect = [ | ||
recorderOptions.cropRect = [ | ||
[cropArea.x, cropArea.y], | ||
@@ -104,7 +116,19 @@ [cropArea.width, cropArea.height] | ||
recorderOpts.videoCodec = codecMap.get(videoCodec); | ||
recorderOptions.videoCodec = codecMap.get(videoCodec); | ||
} | ||
this.recorder = execa(BIN, [JSON.stringify(recorderOpts)]); | ||
this.recorder = execa( | ||
BIN, [ | ||
'record', | ||
'--process-id', | ||
this.processId, | ||
JSON.stringify(recorderOptions) | ||
] | ||
); | ||
this.isFileReady = (async () => { | ||
await this.waitForEvent('onFileReady'); | ||
return this.tmpPath; | ||
})(); | ||
const timeout = setTimeout(() => { | ||
@@ -116,8 +140,8 @@ // `.stopRecording()` was called already | ||
const err = new Error('Could not start recording within 5 seconds'); | ||
err.code = 'RECORDER_TIMEOUT'; | ||
const error = new Error('Could not start recording within 5 seconds'); | ||
error.code = 'RECORDER_TIMEOUT'; | ||
this.recorder.kill(); | ||
delete this.recorder; | ||
reject(err); | ||
}, 10000); | ||
reject(error); | ||
}, 5000); | ||
@@ -131,22 +155,83 @@ this.recorder.catch(error => { | ||
this.recorder.stdout.setEncoding('utf8'); | ||
this.recorder.stdout.on('data', data => { | ||
debuglog(data); | ||
this.recorder.stdout.on('data', log); | ||
if (data.trim() === 'R') { | ||
// `R` is printed by Swift when the recording **actually** starts | ||
(async () => { | ||
try { | ||
await this.waitForEvent('onStart'); | ||
clearTimeout(timeout); | ||
resolve(this.tmpPath); | ||
setTimeout(resolve, 1000); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
}); | ||
})(); | ||
}); | ||
} | ||
async stopRecording() { | ||
async waitForEvent(name, parse) { | ||
const {stdout} = await execa( | ||
BIN, [ | ||
'events', | ||
'listen', | ||
'--process-id', | ||
this.processId, | ||
'--exit', | ||
name | ||
] | ||
); | ||
if (parse) { | ||
return parse(stdout.trim()); | ||
} | ||
} | ||
async sendEvent(name, parse) { | ||
const {stdout} = await execa( | ||
BIN, [ | ||
'events', | ||
'send', | ||
'--process-id', | ||
this.processId, | ||
name | ||
] | ||
); | ||
if (parse) { | ||
return parse(stdout.trim()); | ||
} | ||
} | ||
throwIfNotStarted() { | ||
if (this.recorder === undefined) { | ||
throw new Error('Call `.startRecording()` first'); | ||
} | ||
} | ||
async pause() { | ||
this.throwIfNotStarted(); | ||
await this.sendEvent('pause'); | ||
} | ||
async resume() { | ||
this.throwIfNotStarted(); | ||
await this.sendEvent('resume'); | ||
// It takes about 1s after the promise resolves for the recording to actually start | ||
await delay(1000); | ||
} | ||
async isPaused() { | ||
this.throwIfNotStarted(); | ||
return this.sendEvent('isPaused', value => value === 'true'); | ||
} | ||
async stopRecording() { | ||
this.throwIfNotStarted(); | ||
this.recorder.kill(); | ||
await this.recorder; | ||
delete this.recorder; | ||
delete this.isFileReady; | ||
@@ -160,7 +245,7 @@ return this.tmpPath; | ||
module.exports.screens = async () => { | ||
const stderr = await execa.stderr(BIN, ['list-screens']); | ||
const {stderr} = await execa(BIN, ['list', 'screens']); | ||
try { | ||
return JSON.parse(stderr); | ||
} catch (_) { | ||
} catch { | ||
return stderr; | ||
@@ -171,7 +256,7 @@ } | ||
module.exports.audioDevices = async () => { | ||
const stderr = await execa.stderr(BIN, ['list-audio-devices']); | ||
const {stderr} = await execa(BIN, ['list', 'audio-devices']); | ||
try { | ||
return JSON.parse(stderr); | ||
} catch (_) { | ||
} catch { | ||
return stderr; | ||
@@ -178,0 +263,0 @@ } |
{ | ||
"name": "aperture", | ||
"version": "5.2.0", | ||
"version": "6.0.0", | ||
"description": "Record the screen on macOS", | ||
@@ -8,7 +8,7 @@ "license": "MIT", | ||
"engines": { | ||
"node": ">=8" | ||
"node": ">=10" | ||
}, | ||
"scripts": { | ||
"test": "xo && ava", | ||
"build": "swift build --configuration=release --static-swift-stdlib -Xswiftc '-target' -Xswiftc 'x86_64-apple-macosx10.12' && mv .build/release/aperture .", | ||
"test": "xo && ava && tsd", | ||
"build": "swift build --configuration=release && mv .build/release/aperture .", | ||
"prepublish": "npm run build" | ||
@@ -18,17 +18,19 @@ }, | ||
"index.js", | ||
"aperture" | ||
"aperture", | ||
"index.d.ts" | ||
], | ||
"dependencies": { | ||
"electron-util": "^0.11.0", | ||
"execa": "^1.0.0", | ||
"file-url": "^2.0.2", | ||
"macos-version": "^5.0.0", | ||
"tempy": "^0.2.1" | ||
"electron-util": "^0.14.2", | ||
"execa": "^5.0.0", | ||
"file-url": "^3.0.0", | ||
"macos-version": "^5.2.1", | ||
"tempy": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"ava": "^1.2.1", | ||
"delay": "^4.1.0", | ||
"file-type": "^10.8.0", | ||
"read-chunk": "^3.0.0", | ||
"xo": "^0.24.0" | ||
"ava": "^2.4.0", | ||
"delay": "^5.0.0", | ||
"file-type": "^12.0.0", | ||
"read-chunk": "^3.2.0", | ||
"tsd": "^0.14.0", | ||
"xo": "^0.38.2" | ||
}, | ||
@@ -35,0 +37,0 @@ "xo": { |
@@ -1,6 +0,5 @@ | ||
# aperture-node [](https://travis-ci.org/wulkano/aperture-node) | ||
# aperture-node | ||
> Record the screen on macOS from Node.js | ||
## Install | ||
@@ -12,5 +11,4 @@ | ||
*Requires macOS 10.12 or later.* | ||
*Requires macOS 10.13 or later.* | ||
## Usage | ||
@@ -42,3 +40,2 @@ | ||
## API | ||
@@ -74,3 +71,3 @@ | ||
Get a list of available video codecs. The key is the `videoCodec` option name and the value is the codec name. It only returns `hevc` if you're on macOS 10.13 or newer and your computer supports HEVC hardware encoding. | ||
Get a list of available video codecs. The key is the `videoCodec` option name and the value is the codec name. It only returns `hevc` if your computer supports HEVC hardware encoding. | ||
@@ -90,8 +87,30 @@ Example: | ||
#### recorder.startRecording([[options]](#options)) | ||
#### recorder.startRecording([options?](#options)) | ||
Returns a `Promise` for the path to the screen recording file. | ||
Returns a `Promise` that fullfills when the recording starts or rejects if the recording didn't start after 5 seconds. | ||
Fullfills when the recording starts or rejects if the recording didn't start after 5 seconds. | ||
#### recorder.isFileReady | ||
`Promise` that fullfills with the path to the screen recording file when it's ready. This will never reject. | ||
Only available while a recording is happening, `undefined` otherwise. | ||
Usually, this resolves around 1 second before the recording starts, but that's not guaranteed. | ||
#### recorder.pause() | ||
Pauses the recording. To resume, call `recorder.resume()`. | ||
Returns a `Promise` that fullfills when the recording has been paused. | ||
#### recorder.resume() | ||
Resumes the recording if it's been paused. | ||
Returns a `Promise` that fullfills when the recording has been resumed. | ||
#### recorder.isPaused() | ||
Returns a `Promise` that resolves with a boolean indicating whether or not the recording is currently paused. | ||
#### recorder.stopRecording() | ||
@@ -103,5 +122,7 @@ | ||
Type: `object` | ||
#### fps | ||
Type: `number`<br> | ||
Type: `number`\ | ||
Default: `30` | ||
@@ -113,3 +134,3 @@ | ||
Type: `Object`<br> | ||
Type: `object`\ | ||
Default: `undefined` | ||
@@ -121,3 +142,3 @@ | ||
Type: `boolean`<br> | ||
Type: `boolean`\ | ||
Default: `true` | ||
@@ -129,3 +150,3 @@ | ||
Type: `boolean`<br> | ||
Type: `boolean`\ | ||
Default: `false` | ||
@@ -139,3 +160,3 @@ | ||
Type: `number`<br> | ||
Type: `number`\ | ||
Default: `aperture.screens()[0]` *(Primary screen)* | ||
@@ -147,3 +168,3 @@ | ||
Type: `string`<br> | ||
Type: `string`\ | ||
Default: `undefined` | ||
@@ -155,11 +176,10 @@ | ||
Type: `string`<br> | ||
Default: `h264`<br> | ||
Values: `hevc` `h264` `proRes422` `proRes4444` | ||
Type: `string`\ | ||
Default: `'h264'`\ | ||
Values: `'hevc' | 'h264' | 'proRes422' | 'proRes4444'` | ||
The `hevc` codec requires macOS 10.13 or newer. A computer with Intel 6th generation processor or newer is strongly recommended, as otherwise it will use software encoding, which only produces 3 FPS fullscreen recording. | ||
A computer with Intel 6th generation processor or newer is strongly recommended for the `hevc` codec, as otherwise it will use software encoding, which only produces 3 FPS fullscreen recording. | ||
The [`proRes422` and `proRes4444`](https://documentation.apple.com/en/finalcutpro/professionalformatsandworkflows/index.html#chapter=10%26section=2%26tasks=true) codecs are uncompressed data. They will create huge files. | ||
## Why | ||
@@ -181,10 +201,4 @@ | ||
## Related | ||
- [Aperture](https://github.com/wulkano/Aperture) - The Swift framework used in this package | ||
## License | ||
MIT |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
6
20%343
127.15%193
7.82%1839187
-82.93%6
20%2
100%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated