Node.js program to record smooth movies of web pages with JavaScript animations
BSD-3-CLAUSE License
timecut is a Node.js program that records smooth videos of web pages that use JavaScript animations. It uses timeweb, timesnap, and puppeteer to open a web page, overwrite its time-handling functions, take snapshots of the web page, and then passes the results to ffmpeg to encode those frames into a video. This allows for slower-than-realtime and/or virtual high-fps capture of frames, while the resulting video is smooth.
You can run timecut from the command line or as a Node.js library. It requires ffmpeg, Node v8.9.0 or higher, and npm.
To only record screenshots and save them as pictures, see timesnap. For using virtual time in browser, see timeweb.
timeweb (and timesnap and timecut by extension) only overwrites JavaScript functions and video playback, so pages where changes occur via other means (e.g. through transitions/animations from CSS rules) will likely not render as intended.
To install:
Due to an issue in puppeteer with permissions, timecut is not supported for global installation for root. You can configure npm
to install global packages for a specific user following this guide: https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-two-change-npms-default-directory
After configuring, run:
npm install -g timecut
To use:
timecut "url" [options]
To install:
cd /path/to/installation/directory
npm install timecut
To use:
node /path/to/installation/directory/node_modules/timecut/cli.js "url" [options]
Alternatively:
To install:
cd /path/to/installation/directory
git clone https://github.com/tungs/timecut.git
cd timecut
npm install
To use:
node /path/to/installation/directory/timecut/cli.js "url" [options]
The url can be a web url (e.g. https://github.com
) or a file path, with relative paths resolving in the current working directory. If no url is specified, defaults to index.html
. Remember to enclose urls that contain special characters (like #
and &
) with quotes.
# Default behavior:
timecut
Opens index.html
in the current working directory, sets the viewport to 800x600, captures at 60 frames per second for 5 virtual seconds (temporarily saving each frame), and saves video.mp4
with the yuv420p
pixel format in the current working directory. The defaults may change in the future, so for long-term scripting, it's a good idea to explicitly pass these options, like in the following example.
# Setting viewport size, frames per second, duration, mode, and output:
timecut index.html --viewport="800,600" --fps=60 --duration=5 \
--frame-cache --pix-fmt=yuv420p --output=video.mp4
Equivalent to the current default timecut
invocation, but with explicit options. Opens index.html
in the current working directory, sets the viewport to 800x600, captures at 60 frames per second for 5 virtual seconds (temporarily saving each frame), and saves the resulting video using the pixel format yuv420p
as video.mp4
.
# Using a selector:
timecut drawing.html -S "canvas,svg"
Opens drawing.html
in the current working directory, crops each frame to the bounding box of the first canvas or svg element, and captures frames using default settings (5 seconds @ 60fps saving to video.mp4
).
# Using offsets:
timecut "https://tungs.github.io/amuse/truchet-tiles/#autoplay=true&switchStyle=random" \
-S "#container" \
--left=20 --top=40 --right=6 --bottom=30 \
--duration=20
Opens https://tungs.github.io/amuse/truchet-tiles/#autoplay=true&switchStyle=random (note the quotes in the url and selector are necessary because of the #
and &
). Crops each frame to the #container
element, with an additional crop of 20px, 40px, 6px, and 30px for the left, top, right, and bottom, respectively. Captures frames for 20 virtual seconds at 60fps to video.mp4
in the current working directory.
-O
, --output
name
-R
, --fps
frame rate
60
).-d
, --duration
seconds
5
).--frames
count
-S
, --selector
"selector"
-V
, --viewport
dimensions
800
(for width), or "800,600"
(for width and height), or "800,600,deviceScaleFactor=2"
for (width, height, and deviceScaleFactor). When running in Windows, quotes may be necessary for parsing commas. For a list of optional keys, see config.viewport
.--frame-cache
[directory]
--pipe-mode
--canvas-capture-mode
[format]
png
), otherwise it uses the saved image's extension, or defaults to png
if the format is not specified or supported. Can prefix the format with immediate:
(e.g. immediate:png
) to immediately capture pixel data after rendering, which is sometimes needed for some WebGL renderers. Specify the canvas using the --selector
option, otherwise it defaults to the first canvas in the document.-s
, --start
n seconds
0
).-x
, --x-offset
pixels
0
).-y
, --y-offset
pixels
0
).-W
, --width
pixels
-H
, --height
pixels
--no-round-to-even-width
--no-round-to-even-height
--transparent-background
-l
, --left
pixels
--x-offset
.-r
, --right
pixels
width
is specified.-t
, --top
pixels
--y-offset
.-b
, --bottom
pixels
height
is specified.-u
, --unrandomize
[seeds]
Math.random
with a seeded pseudorandom number generator. Can provide optional seeds as up to four comma separated integers (e.g. --unrandomize 2,3,5,7
or --unrandomize 42
). If seeds
is random-seed
(i.e. --unrandomize random-seed
), a random seed will be generated, displayed (if not in quiet mode), and used. If seeds
is not provided, it uses the seeds 10,0,20,0
.--executable-path
path
--ffmpeg-path
path
-L
, --launch-arguments
arguments
--launch-arguments="--single-process"
. A list of arguments can be found here.--no-headless
--screenshot-type
type
png
is used. jpeg
is also available.--screenshot-quality
number
-e
, --input-options
options
--input-options="-framerate 30"
-E
, --output-options
options
--output-options="-vf scale=320:240"
--pix-fmt
pixel format
yuv420p
).--start-delay
n seconds
--keep-frames
-q
, --quiet
--stop-function-name
function name
--stop-function-name=stopCapture
could be called in the client, via stopCapture()
.-v
, --version
-h
, --help
timecut can also be included as a library inside Node.js programs.
npm install timecut --save
# Basic Use:
const timecut = require('timecut');
timecut({
url: 'https://tungs.github.io/amuse/truchet-tiles/#autoplay=true&switchStyle=random',
viewport: {
width: 800, // sets the viewport (window size) to 800x600
height: 600
},
selector: '#container', // crops each frame to the bounding box of '#container'
left: 20, top: 40, // further crops the left by 20px, and the top by 40px
right: 6, bottom: 30, // and the right by 6px, and the bottom by 30px
fps: 30, // saves 30 frames for each virtual second
duration: 20, // for 20 virtual seconds
output: 'video.mp4' // to video.mp4 of the current working directory
}).then(function () {
console.log('Done!');
});
# Multiple pages:
const timecut = require('timecut');
var pages = [
{
url: 'https://tungs.github.io/amuse/truchet-tiles/#autoplay=true',
output: 'truchet-tiles.mp4',
selector: '#container'
}, {
url: 'https://breathejs.org/examples/Drawing-US-Counties.html',
output: 'counties.mp4',
selector: null // with no selector, it defaults to the viewport dimensions
}
];
(async () => {
for (let page of pages) {
await timecut({
url: page.url,
output: page.output,
selector: page.selector,
viewport: {
width: 800,
height: 600
},
duration: 20
});
}
})();
The Node API is structured similarly to the command line options, but there are a few options for the Node API that are not accessible through the command line interface: config.logToStdErr
, config.navigatePageToURL
, config.preparePage
, config.preparePageForScreenshot
, config.outputStream
, config.logger
, and certain config.viewport
properties.
timecut(config)
config
<Object>
url
<string> The url to load. It can be a web url, like https://github.com
or a file path, with relative paths resolving in the current working directory (default: index.html
).output
<string> Tells ffmpeg to save the video as name. Its file extension determines encoding if not explicitly specified. Default name: video.mp4
.fps
<number> frame rate, in frames per virtual second, of capture (default: 60
).duration
<number> Duration of capture, in seconds (default: 5
).frames
<number> Number of frames to capture. Overrides default fps or default duration.selector
<string> Crops each frame to the bounding box of the first item found by the specified CSS selector.frameCache
<string|boolean> Saves each frame temporarily to disk before ffmpeg processes it. If config.frameCache
is a string, uses that as the directory to save the temporary files. If config.frameCache
is a boolean true
, temporarily creates a directory in the current working directory. See cache frame mode.pipeMode
<boolean> Experimental. If set to true
, pipes frames directly to ffmpeg, without saving to disk. See pipe mode.viewport
<Object>
width
<number> Width of viewport, in pixels (default: 800
).height
<number> Height of viewport, in pixels (default: 600
).deviceScaleFactor
<number> Device scale factor (default: 1
).isMobile
<boolean> Specifies whether the meta viewport
tag should be used (default: false
).hasTouch
<boolean> Specifies whether the viewport supports touch (default: false
).isLandscape
<boolean> Specifies whether the viewport is in landscape mode (default: false
).canvasCaptureMode
<boolean | string>
png
), otherwise it uses the saved image's extension, or defaults to png
if the format is not specified or supported. Can prefix the format with immediate:
(e.g. immediate:png
) to immediately capture pixel data after rendering, which is sometimes needed for some WebGL renderers. Specify the canvas by setting config.selector
, otherwise it defaults to the first canvas in the document.start
<number> Runs code for config.start
virtual seconds before saving any frames (default: 0
).xOffset
<number> X offset of capture, in pixels (default: 0
).yOffset
<number> Y offset of capture, in pixels (default: 0
).width
<number> Width of capture, in pixels.height
<number> Height of capture, in pixels.transparentBackground
<boolean> Allows background to be transparent if there is no background styling. Only works if the output video format supports transparency.roundToEvenWidth
<boolean> Rounds capture width up to the nearest even number (default: true
).roundToEvenHeight
<boolean> Rounds capture height up to the nearest even number (default: true
).left
<number> Left edge of capture, in pixels. Equivalent to config.xOffset
.right
<number> Right edge of capture, in pixels. Ignored if config.width
is specified.top
<number> Top edge of capture, in pixels. Equivalent to config.yOffset
.bottom
<number> Bottom edge of capture, in pixels. Ignored if config.height
is specified.unrandomize
<boolean | string | number | Array<number>> Overwrites Math.random
with a seeded pseudorandom number generator. If it is a number, an array of up to four numbers, or a string of up to four comma separated numbers, then those values are used as the initial seeds. If it is true, then the default seed is used. If it is the string 'random-seed', a random seed will be generated, displayed (if quiet mode is not enabled), and used.executablePath
<string> Uses the Chromium/Chrome instance at config.executablePath
for puppeteer.ffmpegPath
<string> Uses the ffmpeg path for running ffmpeg.launchArguments
<Array <string>> Extra arguments for Puppeteer/Chromium. Example: ['--single-process']
. A list of arguments can be found here.headless
<boolean> Runs puppeteer in headless (nonwindowed) mode (default: true
).screenshotType
<string> Output image format for the screenshots. By default, 'png'
is used. 'jpeg'
is also available.screenshotQuality
<number> Quality level between 0 to 1 for lossy screenshots. Defaults to 0.92 when in canvas capture mode and 0.8 otherwise.inputOptions
<Array <string>> Extra arguments for ffmpeg input. Example: ['-framerate', '30']
outputOptions
<Array <string>> Extra arguments for ffmpeg output. Example: ['-vf', 'scale=320:240']
pixFmt
<string> Pixel format for output video (default: yuv420p
).startDelay
<number> Waits config.startDelay
real seconds after loading before starting (default: 0
).keepFrames
<boolean> If set to true, doesn't delete frames after processing them. Doesn't do anything in pipe mode.quiet
<boolean> Suppresses console logging.logger
<function(...Object)> Replaces console logging with a particular function. The passed arguments are the same as those to console.log
(in this case, usually one string).logToStdErr
<boolean> Logs to stderr instead of stdout. Doesn't do anything if config.quiet
is set to true.stopFunctionName
<string> function name that the client web page can call to stop capturing. For instance, 'stopCapture'
could be called in the client, via stopCapture()
.navigatePageToURL
<function(Object)> A function that navigates a puppeteer page to a URL, overriding the default navigation to a URL. The function should return a promise that resolves once the page is finished navigating. The function is passed the following object:
preparePage
<function(Page)> A setup function that will be called one time before taking screenshots. If it returns a promise, capture will be paused until the promise resolves.
page
<Page> The puppeteer instance of the page being captured.preparePageForScreenshot
<function(Page, number, number)> A setup function that will be called before each screenshot. If it returns a promise, capture will be paused until the promise resolves.
outputStream
<stream()> A node stream to write data to from ffmpegoutputStreamOptions
<Object> Optional configuration object when using config.outputStream
timecut can capture frames to using one of two modes:
--canvas-capture-mode
option from the command line or set config.canvasCaptureMode
from Node.js. Also specify the canvas using a css selector, using the --selector
option from the command line or setting config.selector
from Node.js, otherwise it uses the first canvas element.timecut can pass frames to ffmpeg using one of two modes:
--frame-cache
option from the command line or set config.frameCache
from Node.js to true
or to a directory name.ffmpeg
, without saving each frame. This takes up less temporary space than cache frame mode, but it currently has some observed stability issues. To use this mode, use the --pipe-mode
option from the command line or set config.pipeCache
to true
from Node.js. If you run into issues, you may want to try cache frame mode or to install and use timesnap and pipe it directly to ffmpeg. Both alternative implementations seem more stable than the current pipe mode.timecut uses timesnap to record frames to send to ffmpeg
. timesnap uses puppeteer's page.evaluateOnNewDocument
feature to automatically overwrite a page's native time-handling JavaScript functions and objects (new Date()
, Date.now
, performance.now
, requestAnimationFrame
, setTimeout
, setInterval
, cancelAnimationFrame
, cancelTimeout
, and cancelInterval
) to custom ones that use a virtual timeline, allowing for JavaScript computation to complete before taking a screenshot.
This work was inspired by a talk by Noah Veltman, who described altering a document's Date.now
and performance.now
functions to refer to a virtual time and using puppeteer
to change that virtual time and take snapshots.