Home

Awesome

timecut

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.

<a name="limitations" href="#limitations">#</a> timeweb, timecut, and timesnap Limitations

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.

Read Me Contents

<a name="from-cli" href="#from-cli">#</a> From the Command Line

<a name="cli-global-install" href="#cli-global-install">#</a> 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]

<a name="cli-local-install" href="#cli-local-install">#</a> 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]

<a name="cli-url-use" href="#cli-url-use">#</a> 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.

<a name="cli-examples" href="#cli-examples">#</a> Command Line Examples

<a name="cli-example-default" href="#cli-example-default">#</a> 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.

<a name="cli-example-viewport-fps-duration-mode-output" href="#cli-example-viewport-fps-duration-mode-output">#</a> 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.

<a name="cli-example-selector" href="#cli-example-selector">#</a> 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).

<a name="cli-example-offsets" href="#cli-example-offsets">#</a> 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.

<a name="cli-options" href="#cli-options">#</a> Command Line options

<a name="from-node" href="#from-node">#</a> From Node.js

timecut can also be included as a library inside Node.js programs.

<a name="node-install" href="#node-install">#</a> Node Install

npm install timecut --save

<a name="node-examples" href="#node-examples">#</a> Node Examples

<a name="node-example-basic" href="#node-example-basic">#</a> 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!');
});

<a name="node-example-multiple" href="#node-example-multiple">#</a> 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
    });
  }
})();

<a name="node-api" href="#node-api">#</a> Node API

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)

<a name="modes" href="#modes">#</a> timecut Modes

<a name="capture-modes" href="#capture-modes">#</a> Capture Modes

timecut can capture frames to using one of two modes:

<a name="frame-transfer-modes" href="#frame-transfer-modes">#</a> Frame Transfer Modes

timecut can pass frames to ffmpeg using one of two modes:

<a name="how-it-works" href="#how-it-works">#</a> 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.