timecut is a Node.js program that records smooth videos of web pages that use JavaScript animations. It uses 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 v6.4.0 or higher, and npm.
To only record screenshots and save them as pictures, see timesnap.
# timecut and timesnap Limitations
timesnap (and timecut by extension) only overwrites JavaScript functions, so pages where changes occur via other means (e.g. through video or transitions/animations from CSS rules) will likely not render as intended.
# timecut Modes
timecut can pass frames to ffmpeg using one of two modes:
- # Cache frame mode stores each frame temporarily before running ffmpeg on all of the images. This mode can use a lot of temporary disk space (hundreds of megabytes per second of recorded time), but takes up less memory and is more stable than pipe mode. This is currently enabled by default, though it may change in the future. To explicitly use this mode, use the
--frame-cache
option from the command line or setconfig.frameCache
from Node.js totrue
or to a directory name. - # Pipe mode (experimental) pipes each frame directly to
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 setconfig.pipeCache
totrue
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.
# From the Command Line
# Global Install and Use
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]
# Local Install and Use
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]
# Command Line url
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.
# Command Line Examples
# 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/truchet-tiles-original/#autoplay=true&switchStyle=random" \
-S "#container" \
--left=20 --top=40 --right=6 --bottom=30 \
--duration=20
Opens https://tungs.github.io/truchet-tiles-original/#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.
# Command Line options
- # Output:
-O
,--output
name- Tells ffmpeg to save the video as name. Its file extension determines encoding if not explicitly specified.
- # Frame Rate:
-R
,--fps
frame rate- Frame rate (in frames per virtual second) of capture (default:
60
).
- Frame rate (in frames per virtual second) of capture (default:
- # Duration:
-d
,--duration
seconds- Duration of capture, in seconds (default:
5
).
- Duration of capture, in seconds (default:
- # Frames:
--frames
count- Number of frames to capture.
- # Selector:
-S
,--selector
"selector"- Crops each frame to the bounding box of the first item found by the CSS selector.
- # Viewport:
-V
,--viewport
dimensions- Viewport dimensions, in pixels. For example
800
(for width) or800,600
(for width and height).
- Viewport dimensions, in pixels. For example
- # Frame Cache:
--frame-cache
[directory]- Saves each frame temporarily to disk before ffmpeg processes it. If directory is not specified, temporarily creates one in the current working directory. Enabled by default. See cache frame mode.
- # Pipe Mode:
--pipe-mode
- Experimental. Pipes frames directly to ffmpeg, without saving to disk. See pipe mode.
- # Start:
-s
,--start
n seconds- Runs code for n virtual seconds before saving any frames (default:
0
).
- Runs code for n virtual seconds before saving any frames (default:
- # X Offset:
-x
,--x-offset
pixels- X offset of capture, in pixels (default:
0
).
- X offset of capture, in pixels (default:
- # Y Offset:
-y
,--y-offset
pixels- Y offset of capture, in pixels (default:
0
).
- Y offset of capture, in pixels (default:
- # Width:
-W
,--width
pixels- Width of capture, in pixels.
- # Height:
-H
,--height
pixels- Height of capture, in pixels.
- # No Even Width Rounding:
--no-round-to-even-width
- Disables automatic rounding of capture width up to the nearest even number.
- # No Even Height Rounding:
--no-round-to-even-height
- Disables automatic rounding of capture height up to the nearest even number.
- # Transparent Background:
--transparent-background
- Allows background to be transparent if there is no background styling. Only works if the output video format supports transparency.
- # Left:
-l
,--left
pixels- Left edge of capture, in pixels. Equivalent to
--x-offset
.
- Left edge of capture, in pixels. Equivalent to
- # Right:
-r
,--right
pixels- Right edge of capture, in pixels. Ignored if
width
is specified.
- Right edge of capture, in pixels. Ignored if
- # Top:
-t
,--top
pixels- Top edge of capture, in pixels. Equivalent to
--y-offset
.
- Top edge of capture, in pixels. Equivalent to
- # Bottom:
-b
,--bottom
pixels- Bottom edge of capture, in pixels. Ignored if
height
is specified.
- Bottom edge of capture, in pixels. Ignored if
- # Start Delay:
--start-delay
n seconds- Waits n real seconds after loading the page before starting to capture.
- # Quiet:
-q
,--quiet
- Suppresses console logging.
- # Extra input options:
-e
,--input-options
options- Extra arguments for ffmpeg input, enclosed in quotes. Example:
--input-options="-framerate 30"
- Extra arguments for ffmpeg input, enclosed in quotes. Example:
- # Extra output options:
-E
,--input-options
options- Extra arguments for ffmpeg output, enclosed in quotes. Example:
--output-options="-vf scale=320:240"
- Extra arguments for ffmpeg output, enclosed in quotes. Example:
- # Pixel Format:
--pix-fmt
pixel format- Pixel format for output video (default:
yuv420p
).
- Pixel format for output video (default:
- # Version:
-v
,--version
- Displays version information. Immediately exits.
- # Help:
-h
,--help
- Displays command line options. Immediately exits.
# From Node.js
timecut can also be included as a library inside Node.js programs.
# Node Install
npm install timecut --save
# Node Examples
# Basic Use:
const timecut = require('timecut');
timecut({
url: 'https://tungs.github.io/truchet-tiles-original/#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 (Requires Node v7.6.0 or higher):
const timecut = require('timecut');
var pages = [
{
url: 'https://tungs.github.io/truchet-tiles-original/#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
});
}
})();
# Node API
The Node API is structured similarly to the command line options.
timecut(config)
- #
config
<Object>- #
url
<string> The url to load. It can be a web url, likehttps://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. Ifconfig.frameCache
is a string, uses that as the directory to save the temporary files. Ifconfig.frameCache
is a booleantrue
, temporarily creates a directory in the current working directory. See cache frame mode. - #
pipeMode
<boolean> Experimental. If set totrue
, 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 themeta 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
).
- #
- #
start
<number> Runs code forconfig.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 toconfig.xOffset
. - #
right
<number> Right edge of capture, in pixels. Ignored ifconfig.width
is specified. - #
top
<number> Top edge of capture, in pixels. Equivalent toconfig.yOffset
. - #
bottom
<number> Bottom edge of capture, in pixels. Ignored ifconfig.height
is specified. - #
startDelay
<number> Waitsconfig.loadDelay
real seconds after loading before starting (default:0
). - #
quiet
<boolean> Suppresses console logging. - #
inputOptions
<Array <string>> Extra arguments for ffmpeg input. Example:['-framerate', '30']
- #
outputOptions
<Array <string>> Extra arguments for ffmpeg output. Example:['-vf', 'scale=320:240']
- #
pixelFormat
<string> Pixel format for output video (default:yuv420p
). - #
logToStdErr
<boolean> Logs to stderr instead of stdout. Doesn't do anything ifconfig.quiet
is set to true.
- #
- # returns: <Promise> resolves after all the frames have been captured.
# How it works
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.