etro 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,12 +21,25 @@ jobs:
21
21
  - name: Update npm
22
22
  run: |
23
23
  npm i -g npm@^7.x
24
- - name: npm install, lint, build, and test
24
+ - name: Install npm dependencies
25
25
  run: |
26
26
  npm ci
27
27
  node node_modules/puppeteer/install.js
28
- npm run lint
29
- npm run build
30
- xvfb-run --auto-servernum npm test
28
+ env:
29
+ CI: true
30
+ - name: lint code
31
+ run: npm run lint
32
+ env:
33
+ CI: true
34
+ - name: compile project
35
+ run: npm run build
36
+ env:
37
+ CI: true
38
+ - name: run unit tests
39
+ run: xvfb-run --auto-servernum npm run test:unit
40
+ env:
41
+ CI: true
42
+ - name: run smoke tests
43
+ run: xvfb-run --auto-servernum npm run test:smoke
31
44
  env:
32
45
  CI: true
@@ -22,7 +22,10 @@ jobs:
22
22
  else
23
23
  npm install
24
24
  fi
25
- - run: npx shipjs trigger
25
+ - run: |
26
+ git config --global user.email "16855387+clabe45@users.noreply.github.com"
27
+ git config --global user.name "Caleb Sacks"
28
+ npx shipjs trigger
26
29
  env:
27
30
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28
31
  NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx --no -- commitlint --edit ${1}
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npm run lint
5
+ npm run test:unit
6
+ npm run test:smoke
7
+ npm run test:integration
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # gitmoji as a commit hook
4
+ if npx -v >&/dev/null
5
+ then
6
+ exec < /dev/tty
7
+ npx -c "gitmoji --hook $1 $2"
8
+ else
9
+ exec < /dev/tty
10
+ gitmoji --hook $1 $2
11
+ fi
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [0.11.0] - 2023-08-05
9
+ ### Added
10
+ - `duration` option for `Movie#play` ([#208](https://github.com/etro-js/etro/pull/208)).
11
+
12
+ ### Fixed
13
+ - Audio and video layers going silent after the first time recording the movie ([#106](https://github.com/etro-js/etro/issues/106)).
14
+ - `Failed to set the 'currentTime' property on 'HTMLMediaElement'` error when seeking audio and video layers ([#227](https://github.com/etro-js/etro/pull/227)).
15
+ - Seeking while playing not updating the movie's current time ([#233](https://github.com/etro-js/etro/issues/233)).
16
+
17
+ ### Security
18
+ - Bump word-wrap from 1.2.3 to 1.2.5 ([#222](https://github.com/etro-js/etro/pull/222)).
19
+
8
20
  ## [0.10.1] - 2023-07-16
9
21
  ### Security
10
22
  - Bump engine.io and socket.io.
@@ -280,6 +292,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
280
292
  - Gaussian blur
281
293
  - Transform
282
294
 
295
+ [0.11.0]: https://github.com/etro-js/etro/compare/v0.10.1...v0.11.0
283
296
  [0.10.1]: https://github.com/etro-js/etro/compare/v0.10.0...v0.10.1
284
297
  [0.10.0]: https://github.com/etro-js/etro/compare/v0.9.1...v0.10.0
285
298
  [0.9.1]: https://github.com/etro-js/etro/compare/v0.9.0...v0.9.1
package/CONTRIBUTING.md CHANGED
@@ -10,73 +10,47 @@ Thank you for considering contributing to Etro! There are many ways you can cont
10
10
 
11
11
  ## Setting up your local environment
12
12
 
13
- #### Step 0: Dependencies
14
-
15
13
  - You will need Git, Node, NPM (at least 7.x) and Firefox (for headless functional testing) installed.
16
-
17
- #### Step 1: Fork
18
-
19
- - Create your own fork of Etro. Then run
20
-
14
+ - To get started, create your own fork of Etro. Then run
21
15
  ```
22
16
  git clone https://github.com/YOUR_USERNAME/etro.git
23
17
  cd etro
24
18
  npm install
25
- npm test
19
+ npm run test:unit
20
+ npm run test:smoke
21
+ npm run test:integration
26
22
  ```
27
23
 
28
24
  ## Making your changes
29
25
 
30
- #### Step 2: Code
31
-
32
26
  - Make some changes and update tests
33
27
  - If you are writing code, the linter uses [StandardJS](https://standardjs.com/rules.html) for style conventions
34
28
  - If you're adding or updating an effect:
35
29
  - Add your effect to **scripts/gen-effect-samples.html**
36
30
  - Run `npm run effects`
37
31
  - Briefly review the images in **spec/integration/assets/effect/**
38
- - When you're ready to submit, first run
32
+ - As you work, you can run
39
33
  ```
40
- npm run lint
34
+ npm run fix
41
35
  npm run build
42
- npm test
36
+ npm test:unit
37
+ npm test:smoke
38
+ npm test:integration
43
39
  ```
44
40
 
45
- to lint the code, generate the [dist](dist) files and run unit tests on them. It's helpful to put these commands in a pre-commit hook.
46
-
47
- #### Step 3: Commit
41
+ to lint and compile the code and run the tests on them. Husky will run these commands automatically when you commit.
42
+ - *Note: Unit tests validate the logic of the code in etro, with the DOM and any other external dependencies mocked. Because audio cannot be captured in the GitHub Actions runner, the end-to-end tests are divided into two suites. All end-to-end tests that need to validate audio output should be placed in **spec/integration/**. All end-to-end tests that do **not** require an audio device should be placed in **spec/smoke/**. The integration tests can only be run locally, but the other two suites can be run anywhere.*
48
43
 
49
- - Please follow these commit message guidelines:
50
- - Optionally, prefix each commit message with [an appropriate emoji](https://gitmoji.dev), such as `:bug:` for fixes.
51
- - Write in the imperative tense
52
- - Wrap lines after 72 characters (for Vim add `filetype indent plugin on` to ~/.vimrc, it's enabled by default in Atom).
53
- - Format:
54
- ```
55
- :emoji: One-liner
56
-
57
- Optional description
58
- ```
44
+ - Please commit to a new branch, not master
59
45
 
60
46
  ## Submitting your changes
61
47
 
62
- #### Step 4: Push
63
-
64
- - First, rebase (please avoid merging) to integrate your work with any new changes in the main repository
65
-
48
+ - Before pushing to your fork, rebase (please avoid merging) to integrate your work with any new changes in the main repository
66
49
  ```
67
50
  git fetch upstream
68
51
  git rebase upstream/master
69
52
  ```
70
-
71
- - Push to the fork
72
-
73
- #### Step 5: Pull request
74
-
75
- - Open a pull request from the branch in your fork to the main repository
76
- - If you changed any core functionality, make sure you explain your motives for those changes
77
-
78
- #### Step 6: Feedback
79
-
53
+ - Open a pull request from the branch in your fork to the main repository. If you changed any core functionality, make sure you explain your motives for those changes
80
54
  - A large part of the submission process is receiving feedback on how you can improve you pull request. If you need to change your pull request, feel free to push more commits.
81
55
 
82
56
  ## Code overview
@@ -87,6 +61,8 @@ Check out [the user docs](https://etrojs.dev/docs/intro) for a high-level overvi
87
61
 
88
62
  ### Events
89
63
 
64
+ > Events were deprecated in v0.10.0 in favor of async methods with callbacks.
65
+
90
66
  Events emitted by Etro objects use a [pub/sub system](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern). To emit an event, use `event.publish(target, type, event)`. For instance,
91
67
 
92
68
  ```js
package/README.md CHANGED
@@ -2,14 +2,13 @@
2
2
 
3
3
  [![](https://img.shields.io/npm/v/etro)](https://www.npmjs.com/package/etro)
4
4
  [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fetro-js%2Fetro%2Fbadge&style=flat)](https://actions-badge.atrox.dev/etro-js/etro/goto)
5
+ [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=flat&logo=discord&logoColor=white)](https://discord.gg/myrBsQ8Cht)
5
6
 
6
7
  Etro is a typescript framework for programmatically editing videos. It lets you
7
8
  composite layers and add filters (effects). Etro comes shipped with text, video,
8
9
  audio and image layers, along with a bunch of GLSL effects. You can also define
9
10
  your own layers and effects with javascript and GLSL.
10
11
 
11
- [Join our Discord](https://discord.gg/myrBsQ8Cht)
12
-
13
12
  ## Features
14
13
 
15
14
  - Composite video and audio layers
@@ -0,0 +1,39 @@
1
+ import type {UserConfig} from '@commitlint/types';
2
+ import { RuleConfigSeverity } from "@commitlint/types";
3
+
4
+ const Configuration: UserConfig = {
5
+ /*
6
+ * Resolve and load @commitlint/format from node_modules.
7
+ * Referenced package must be installed
8
+ */
9
+ formatter: '@commitlint/format',
10
+ /*
11
+ * Any rules defined here will override the default ones
12
+ */
13
+ rules: {
14
+ 'header-max-length': [RuleConfigSeverity.Error, 'always', 72],
15
+ 'body-max-line-length': [RuleConfigSeverity.Error, 'always', 72],
16
+ },
17
+ /*
18
+ * Functions that return true if commitlint should ignore the given message.
19
+ */
20
+ ignores: [(commit) => commit === ''],
21
+ /*
22
+ * Whether commitlint uses the default ignore rules.
23
+ */
24
+ defaultIgnores: true,
25
+ /*
26
+ * Custom URL to show upon failure
27
+ */
28
+ helpUrl:
29
+ 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
30
+ /*
31
+ * Custom prompt configs
32
+ */
33
+ prompt: {
34
+ messages: {},
35
+ questions: {},
36
+ },
37
+ };
38
+
39
+ module.exports = Configuration;
package/dist/etro-cjs.js CHANGED
@@ -715,32 +715,15 @@ function AudioSourceMixin(superclass) {
715
715
  _super.prototype.attach.call(this, movie);
716
716
  // TODO: on unattach?
717
717
  subscribe(movie, 'audiodestinationupdate', function (event) {
718
- // Connect to new destination if immediately connected to the existing
719
- // destination.
720
- if (_this._connectedToDestination) {
721
- _this.audioNode.disconnect(movie.actx.destination);
722
- _this.audioNode.connect(event.destination);
723
- }
718
+ _this.audioNode.disconnect(_this._lastAudioDestination);
719
+ _this.audioNode.connect(event.destination);
720
+ _this._lastAudioDestination = event.destination;
724
721
  });
725
722
  // connect to audiocontext
726
723
  this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source);
727
- // Spy on connect and disconnect to remember if it connected to
728
- // actx.destination (for Movie#record).
729
- var oldConnect = this._audioNode.connect.bind(this.audioNode);
730
- this._audioNode.connect = function (destination, outputIndex, inputIndex) {
731
- _this._connectedToDestination = destination === movie.actx.destination;
732
- return oldConnect(destination, outputIndex, inputIndex);
733
- };
734
- var oldDisconnect = this._audioNode.disconnect.bind(this.audioNode);
735
- this._audioNode.disconnect = function (destination, output, input) {
736
- if (_this._connectedToDestination &&
737
- destination === movie.actx.destination) {
738
- _this._connectedToDestination = false;
739
- }
740
- return oldDisconnect(destination, output, input);
741
- };
742
724
  // Connect to actx.destination by default (can be rewired by user)
743
725
  this.audioNode.connect(movie.actx.destination);
726
+ this._lastAudioDestination = movie.actx.destination;
744
727
  };
745
728
  MixedAudioSource.prototype.detach = function () {
746
729
  // Cache dest before super.detach() unsets this.movie
@@ -754,7 +737,12 @@ function AudioSourceMixin(superclass) {
754
737
  };
755
738
  MixedAudioSource.prototype.seek = function (time) {
756
739
  _super.prototype.seek.call(this, time);
757
- this.source.currentTime = this.currentTime + this.sourceStartTime;
740
+ if (isNaN(this.currentTime)) {
741
+ this.source.currentTime = this.sourceStartTime;
742
+ }
743
+ else {
744
+ this.source.currentTime = this.currentTime + this.sourceStartTime;
745
+ }
758
746
  };
759
747
  MixedAudioSource.prototype.render = function () {
760
748
  _super.prototype.render.call(this);
@@ -2843,10 +2831,6 @@ var Movie = /** @class */ (function () {
2843
2831
  // `render`). It's only valid while rendering.
2844
2832
  this._renderingFrame = false;
2845
2833
  this.currentTime = 0;
2846
- // The last time `play` was called, -1 works well in comparisons
2847
- this._lastPlayed = -1;
2848
- // What `currentTime` was when `play` was called
2849
- this._lastPlayedOffset = -1;
2850
2834
  }
2851
2835
  Movie.prototype._whenReady = function () {
2852
2836
  return __awaiter(this, void 0, void 0, function () {
@@ -2868,6 +2852,7 @@ var Movie = /** @class */ (function () {
2868
2852
  *
2869
2853
  * @param [options]
2870
2854
  * @param [options.onStart] Called when the movie starts playing
2855
+ * @param [options.duration] The duration of the movie to play in seconds
2871
2856
  *
2872
2857
  * @return Fulfilled when the movie is done playing, never fails
2873
2858
  */
@@ -2885,8 +2870,8 @@ var Movie = /** @class */ (function () {
2885
2870
  throw new Error('Already playing');
2886
2871
  }
2887
2872
  this._paused = this._ended = false;
2888
- this._lastPlayed = performance.now();
2889
- this._lastPlayedOffset = this.currentTime;
2873
+ this._lastRealTime = performance.now();
2874
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2890
2875
  (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2891
2876
  // For backwards compatibility
2892
2877
  publish(this, 'movie.play', {});
@@ -2894,15 +2879,19 @@ var Movie = /** @class */ (function () {
2894
2879
  return [4 /*yield*/, new Promise(function (resolve) {
2895
2880
  if (!_this.renderingFrame) {
2896
2881
  // Not rendering (and not playing), so play.
2897
- _this._render(true, undefined, resolve);
2882
+ _this._render(undefined, resolve);
2898
2883
  }
2899
2884
  // Stop rendering frame if currently doing so, because playing has higher
2900
2885
  // priority. This will affect the next _render call.
2901
2886
  _this._renderingFrame = false;
2902
- })];
2887
+ })
2888
+ // After we're done playing, clear the last timestamp
2889
+ ];
2903
2890
  case 2:
2904
2891
  // Repeatedly render frames until the movie ends
2905
2892
  _b.sent();
2893
+ // After we're done playing, clear the last timestamp
2894
+ this._lastRealTime = undefined;
2906
2895
  return [2 /*return*/];
2907
2896
  }
2908
2897
  });
@@ -2976,16 +2965,17 @@ var Movie = /** @class */ (function () {
2976
2965
  // Create the stream
2977
2966
  this._currentStream = new MediaStream(tracks);
2978
2967
  // Play the movie
2979
- this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2980
2968
  return [4 /*yield*/, this.play({
2981
2969
  onStart: function () {
2982
2970
  // Call the user's onStart callback
2983
2971
  options.onStart(_this._currentStream);
2984
- }
2972
+ },
2973
+ duration: options.duration
2985
2974
  })
2986
2975
  // Clear the stream after the movie is done playing
2987
2976
  ];
2988
2977
  case 2:
2978
+ // Play the movie
2989
2979
  _a.sent();
2990
2980
  // Clear the stream after the movie is done playing
2991
2981
  this._currentStream.getTracks().forEach(function (track) {
@@ -3120,11 +3110,13 @@ var Movie = /** @class */ (function () {
3120
3110
  return this;
3121
3111
  };
3122
3112
  /**
3113
+ * Processes one frame of the movie and draws it to the canvas
3114
+ *
3123
3115
  * @param [timestamp=performance.now()]
3124
3116
  * @param [done=undefined] - Called when done playing or when the current
3125
3117
  * frame is loaded
3126
3118
  */
3127
- Movie.prototype._render = function (repeat, timestamp, done) {
3119
+ Movie.prototype._render = function (timestamp, done) {
3128
3120
  var _this = this;
3129
3121
  if (timestamp === void 0) { timestamp = performance.now(); }
3130
3122
  if (done === void 0) { done = undefined; }
@@ -3161,8 +3153,6 @@ var Movie = /** @class */ (function () {
3161
3153
  // value and publish a 'imeupdate' event.
3162
3154
  this._currentTime = 0;
3163
3155
  publish(this, 'movie.timeupdate', { movie: this });
3164
- this._lastPlayed = performance.now();
3165
- this._lastPlayedOffset = 0; // this.currentTime
3166
3156
  this._renderingFrame = false;
3167
3157
  // Stop playback or recording if done (except if it's playing and repeat
3168
3158
  // is true)
@@ -3214,18 +3204,19 @@ var Movie = /** @class */ (function () {
3214
3204
  }
3215
3205
  // TODO: Is making a new arrow function every frame bad for performance?
3216
3206
  window.requestAnimationFrame(function () {
3217
- _this._render(repeat, undefined, done);
3207
+ _this._render(undefined, done);
3218
3208
  });
3219
3209
  };
3220
3210
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
3221
3211
  // If we're only frame-rendering (current frame only), it doesn't matter if
3222
3212
  // it's paused or not.
3223
3213
  if (!this._renderingFrame) {
3224
- var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
3225
- var currentTime = this._lastPlayedOffset + sinceLastPlayed;
3226
- if (this.currentTime !== currentTime) {
3214
+ var timestamp = timestampMs / 1000;
3215
+ var delta = timestamp - this._lastRealTime;
3216
+ this._lastRealTime = timestamp;
3217
+ if (delta > 0) {
3227
3218
  // Update the current time (don't use setter)
3228
- this._currentTime = currentTime;
3219
+ this._currentTime += delta;
3229
3220
  // For backwards compatibility, publish a 'movie.timeupdate' event.
3230
3221
  publish(this, 'movie.timeupdate', { movie: this });
3231
3222
  }
@@ -3234,6 +3225,11 @@ var Movie = /** @class */ (function () {
3234
3225
  }
3235
3226
  }
3236
3227
  };
3228
+ /**
3229
+ * Draws the movie's background to the canvas
3230
+ *
3231
+ * @param timestamp The current high-resolution timestamp in milliseconds
3232
+ */
3237
3233
  Movie.prototype._renderBackground = function (timestamp) {
3238
3234
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3239
3235
  // Evaluate background color (since it's a dynamic property)
@@ -3244,7 +3240,7 @@ var Movie = /** @class */ (function () {
3244
3240
  }
3245
3241
  };
3246
3242
  /**
3247
- * @param [timestamp=performance.now()]
3243
+ * Ticks all layers and renders them to the canvas
3248
3244
  */
3249
3245
  Movie.prototype._renderLayers = function () {
3250
3246
  for (var i = 0; i < this.layers.length; i++) {
@@ -3289,6 +3285,12 @@ var Movie = /** @class */ (function () {
3289
3285
  }
3290
3286
  }
3291
3287
  };
3288
+ /**
3289
+ * Applies all of the movie's effects to the canvas
3290
+ *
3291
+ * Note: This method only applies the movie's effects, not the layers'
3292
+ * effects.
3293
+ */
3292
3294
  Movie.prototype._applyEffects = function () {
3293
3295
  for (var i = 0; i < this.effects.length; i++) {
3294
3296
  var effect = this.effects[i];
@@ -3315,7 +3317,7 @@ var Movie = /** @class */ (function () {
3315
3317
  }
3316
3318
  return new Promise(function (resolve) {
3317
3319
  _this._renderingFrame = true;
3318
- _this._render(false, undefined, resolve);
3320
+ _this._render(undefined, resolve);
3319
3321
  });
3320
3322
  };
3321
3323
  /**
@@ -3364,7 +3366,7 @@ var Movie = /** @class */ (function () {
3364
3366
  *
3365
3367
  * Calculated from the end time of the last layer
3366
3368
  */
3367
- // TODO: dirty flag?
3369
+ // TODO: cache
3368
3370
  get: function () {
3369
3371
  return this.layers.reduce(function (end, layer) { return Math.max(layer.startTime + layer.duration, end); }, 0);
3370
3372
  },
@@ -3373,6 +3375,7 @@ var Movie = /** @class */ (function () {
3373
3375
  });
3374
3376
  /**
3375
3377
  * Convenience method for `layers.push()`
3378
+ *
3376
3379
  * @param layer
3377
3380
  * @return The movie
3378
3381
  */
@@ -3382,6 +3385,7 @@ var Movie = /** @class */ (function () {
3382
3385
  };
3383
3386
  /**
3384
3387
  * Convenience method for `effects.push()`
3388
+ *
3385
3389
  * @param effect
3386
3390
  * @return the movie
3387
3391
  */
package/dist/etro-iife.js CHANGED
@@ -716,32 +716,15 @@ var etro = (function () {
716
716
  _super.prototype.attach.call(this, movie);
717
717
  // TODO: on unattach?
718
718
  subscribe(movie, 'audiodestinationupdate', function (event) {
719
- // Connect to new destination if immediately connected to the existing
720
- // destination.
721
- if (_this._connectedToDestination) {
722
- _this.audioNode.disconnect(movie.actx.destination);
723
- _this.audioNode.connect(event.destination);
724
- }
719
+ _this.audioNode.disconnect(_this._lastAudioDestination);
720
+ _this.audioNode.connect(event.destination);
721
+ _this._lastAudioDestination = event.destination;
725
722
  });
726
723
  // connect to audiocontext
727
724
  this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source);
728
- // Spy on connect and disconnect to remember if it connected to
729
- // actx.destination (for Movie#record).
730
- var oldConnect = this._audioNode.connect.bind(this.audioNode);
731
- this._audioNode.connect = function (destination, outputIndex, inputIndex) {
732
- _this._connectedToDestination = destination === movie.actx.destination;
733
- return oldConnect(destination, outputIndex, inputIndex);
734
- };
735
- var oldDisconnect = this._audioNode.disconnect.bind(this.audioNode);
736
- this._audioNode.disconnect = function (destination, output, input) {
737
- if (_this._connectedToDestination &&
738
- destination === movie.actx.destination) {
739
- _this._connectedToDestination = false;
740
- }
741
- return oldDisconnect(destination, output, input);
742
- };
743
725
  // Connect to actx.destination by default (can be rewired by user)
744
726
  this.audioNode.connect(movie.actx.destination);
727
+ this._lastAudioDestination = movie.actx.destination;
745
728
  };
746
729
  MixedAudioSource.prototype.detach = function () {
747
730
  // Cache dest before super.detach() unsets this.movie
@@ -755,7 +738,12 @@ var etro = (function () {
755
738
  };
756
739
  MixedAudioSource.prototype.seek = function (time) {
757
740
  _super.prototype.seek.call(this, time);
758
- this.source.currentTime = this.currentTime + this.sourceStartTime;
741
+ if (isNaN(this.currentTime)) {
742
+ this.source.currentTime = this.sourceStartTime;
743
+ }
744
+ else {
745
+ this.source.currentTime = this.currentTime + this.sourceStartTime;
746
+ }
759
747
  };
760
748
  MixedAudioSource.prototype.render = function () {
761
749
  _super.prototype.render.call(this);
@@ -2844,10 +2832,6 @@ var etro = (function () {
2844
2832
  // `render`). It's only valid while rendering.
2845
2833
  this._renderingFrame = false;
2846
2834
  this.currentTime = 0;
2847
- // The last time `play` was called, -1 works well in comparisons
2848
- this._lastPlayed = -1;
2849
- // What `currentTime` was when `play` was called
2850
- this._lastPlayedOffset = -1;
2851
2835
  }
2852
2836
  Movie.prototype._whenReady = function () {
2853
2837
  return __awaiter(this, void 0, void 0, function () {
@@ -2869,6 +2853,7 @@ var etro = (function () {
2869
2853
  *
2870
2854
  * @param [options]
2871
2855
  * @param [options.onStart] Called when the movie starts playing
2856
+ * @param [options.duration] The duration of the movie to play in seconds
2872
2857
  *
2873
2858
  * @return Fulfilled when the movie is done playing, never fails
2874
2859
  */
@@ -2886,8 +2871,8 @@ var etro = (function () {
2886
2871
  throw new Error('Already playing');
2887
2872
  }
2888
2873
  this._paused = this._ended = false;
2889
- this._lastPlayed = performance.now();
2890
- this._lastPlayedOffset = this.currentTime;
2874
+ this._lastRealTime = performance.now();
2875
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2891
2876
  (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2892
2877
  // For backwards compatibility
2893
2878
  publish(this, 'movie.play', {});
@@ -2895,15 +2880,19 @@ var etro = (function () {
2895
2880
  return [4 /*yield*/, new Promise(function (resolve) {
2896
2881
  if (!_this.renderingFrame) {
2897
2882
  // Not rendering (and not playing), so play.
2898
- _this._render(true, undefined, resolve);
2883
+ _this._render(undefined, resolve);
2899
2884
  }
2900
2885
  // Stop rendering frame if currently doing so, because playing has higher
2901
2886
  // priority. This will affect the next _render call.
2902
2887
  _this._renderingFrame = false;
2903
- })];
2888
+ })
2889
+ // After we're done playing, clear the last timestamp
2890
+ ];
2904
2891
  case 2:
2905
2892
  // Repeatedly render frames until the movie ends
2906
2893
  _b.sent();
2894
+ // After we're done playing, clear the last timestamp
2895
+ this._lastRealTime = undefined;
2907
2896
  return [2 /*return*/];
2908
2897
  }
2909
2898
  });
@@ -2977,16 +2966,17 @@ var etro = (function () {
2977
2966
  // Create the stream
2978
2967
  this._currentStream = new MediaStream(tracks);
2979
2968
  // Play the movie
2980
- this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2981
2969
  return [4 /*yield*/, this.play({
2982
2970
  onStart: function () {
2983
2971
  // Call the user's onStart callback
2984
2972
  options.onStart(_this._currentStream);
2985
- }
2973
+ },
2974
+ duration: options.duration
2986
2975
  })
2987
2976
  // Clear the stream after the movie is done playing
2988
2977
  ];
2989
2978
  case 2:
2979
+ // Play the movie
2990
2980
  _a.sent();
2991
2981
  // Clear the stream after the movie is done playing
2992
2982
  this._currentStream.getTracks().forEach(function (track) {
@@ -3121,11 +3111,13 @@ var etro = (function () {
3121
3111
  return this;
3122
3112
  };
3123
3113
  /**
3114
+ * Processes one frame of the movie and draws it to the canvas
3115
+ *
3124
3116
  * @param [timestamp=performance.now()]
3125
3117
  * @param [done=undefined] - Called when done playing or when the current
3126
3118
  * frame is loaded
3127
3119
  */
3128
- Movie.prototype._render = function (repeat, timestamp, done) {
3120
+ Movie.prototype._render = function (timestamp, done) {
3129
3121
  var _this = this;
3130
3122
  if (timestamp === void 0) { timestamp = performance.now(); }
3131
3123
  if (done === void 0) { done = undefined; }
@@ -3162,8 +3154,6 @@ var etro = (function () {
3162
3154
  // value and publish a 'imeupdate' event.
3163
3155
  this._currentTime = 0;
3164
3156
  publish(this, 'movie.timeupdate', { movie: this });
3165
- this._lastPlayed = performance.now();
3166
- this._lastPlayedOffset = 0; // this.currentTime
3167
3157
  this._renderingFrame = false;
3168
3158
  // Stop playback or recording if done (except if it's playing and repeat
3169
3159
  // is true)
@@ -3215,18 +3205,19 @@ var etro = (function () {
3215
3205
  }
3216
3206
  // TODO: Is making a new arrow function every frame bad for performance?
3217
3207
  window.requestAnimationFrame(function () {
3218
- _this._render(repeat, undefined, done);
3208
+ _this._render(undefined, done);
3219
3209
  });
3220
3210
  };
3221
3211
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
3222
3212
  // If we're only frame-rendering (current frame only), it doesn't matter if
3223
3213
  // it's paused or not.
3224
3214
  if (!this._renderingFrame) {
3225
- var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
3226
- var currentTime = this._lastPlayedOffset + sinceLastPlayed;
3227
- if (this.currentTime !== currentTime) {
3215
+ var timestamp = timestampMs / 1000;
3216
+ var delta = timestamp - this._lastRealTime;
3217
+ this._lastRealTime = timestamp;
3218
+ if (delta > 0) {
3228
3219
  // Update the current time (don't use setter)
3229
- this._currentTime = currentTime;
3220
+ this._currentTime += delta;
3230
3221
  // For backwards compatibility, publish a 'movie.timeupdate' event.
3231
3222
  publish(this, 'movie.timeupdate', { movie: this });
3232
3223
  }
@@ -3235,6 +3226,11 @@ var etro = (function () {
3235
3226
  }
3236
3227
  }
3237
3228
  };
3229
+ /**
3230
+ * Draws the movie's background to the canvas
3231
+ *
3232
+ * @param timestamp The current high-resolution timestamp in milliseconds
3233
+ */
3238
3234
  Movie.prototype._renderBackground = function (timestamp) {
3239
3235
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3240
3236
  // Evaluate background color (since it's a dynamic property)
@@ -3245,7 +3241,7 @@ var etro = (function () {
3245
3241
  }
3246
3242
  };
3247
3243
  /**
3248
- * @param [timestamp=performance.now()]
3244
+ * Ticks all layers and renders them to the canvas
3249
3245
  */
3250
3246
  Movie.prototype._renderLayers = function () {
3251
3247
  for (var i = 0; i < this.layers.length; i++) {
@@ -3290,6 +3286,12 @@ var etro = (function () {
3290
3286
  }
3291
3287
  }
3292
3288
  };
3289
+ /**
3290
+ * Applies all of the movie's effects to the canvas
3291
+ *
3292
+ * Note: This method only applies the movie's effects, not the layers'
3293
+ * effects.
3294
+ */
3293
3295
  Movie.prototype._applyEffects = function () {
3294
3296
  for (var i = 0; i < this.effects.length; i++) {
3295
3297
  var effect = this.effects[i];
@@ -3316,7 +3318,7 @@ var etro = (function () {
3316
3318
  }
3317
3319
  return new Promise(function (resolve) {
3318
3320
  _this._renderingFrame = true;
3319
- _this._render(false, undefined, resolve);
3321
+ _this._render(undefined, resolve);
3320
3322
  });
3321
3323
  };
3322
3324
  /**
@@ -3365,7 +3367,7 @@ var etro = (function () {
3365
3367
  *
3366
3368
  * Calculated from the end time of the last layer
3367
3369
  */
3368
- // TODO: dirty flag?
3370
+ // TODO: cache
3369
3371
  get: function () {
3370
3372
  return this.layers.reduce(function (end, layer) { return Math.max(layer.startTime + layer.duration, end); }, 0);
3371
3373
  },
@@ -3374,6 +3376,7 @@ var etro = (function () {
3374
3376
  });
3375
3377
  /**
3376
3378
  * Convenience method for `layers.push()`
3379
+ *
3377
3380
  * @param layer
3378
3381
  * @return The movie
3379
3382
  */
@@ -3383,6 +3386,7 @@ var etro = (function () {
3383
3386
  };
3384
3387
  /**
3385
3388
  * Convenience method for `effects.push()`
3389
+ *
3386
3390
  * @param effect
3387
3391
  * @return the movie
3388
3392
  */
@@ -62,8 +62,8 @@ export declare class Movie {
62
62
  private _recording;
63
63
  private _currentStream;
64
64
  private _endTime;
65
- private _lastPlayed;
66
- private _lastPlayedOffset;
65
+ /** The timestamp last frame in seconds */
66
+ private _lastRealTime;
67
67
  /**
68
68
  * Creates a new movie.
69
69
  */
@@ -74,11 +74,13 @@ export declare class Movie {
74
74
  *
75
75
  * @param [options]
76
76
  * @param [options.onStart] Called when the movie starts playing
77
+ * @param [options.duration] The duration of the movie to play in seconds
77
78
  *
78
79
  * @return Fulfilled when the movie is done playing, never fails
79
80
  */
80
81
  play(options?: {
81
82
  onStart?: () => void;
83
+ duration?: number;
82
84
  }): Promise<void>;
83
85
  /**
84
86
  * Updates the rendering canvas and audio destination to the visible canvas
@@ -136,17 +138,30 @@ export declare class Movie {
136
138
  */
137
139
  stop(): Movie;
138
140
  /**
141
+ * Processes one frame of the movie and draws it to the canvas
142
+ *
139
143
  * @param [timestamp=performance.now()]
140
144
  * @param [done=undefined] - Called when done playing or when the current
141
145
  * frame is loaded
142
146
  */
143
147
  private _render;
144
148
  private _updateCurrentTime;
149
+ /**
150
+ * Draws the movie's background to the canvas
151
+ *
152
+ * @param timestamp The current high-resolution timestamp in milliseconds
153
+ */
145
154
  private _renderBackground;
146
155
  /**
147
- * @param [timestamp=performance.now()]
156
+ * Ticks all layers and renders them to the canvas
148
157
  */
149
158
  private _renderLayers;
159
+ /**
160
+ * Applies all of the movie's effects to the canvas
161
+ *
162
+ * Note: This method only applies the movie's effects, not the layers'
163
+ * effects.
164
+ */
150
165
  private _applyEffects;
151
166
  /**
152
167
  * Refreshes the screen
@@ -180,12 +195,14 @@ export declare class Movie {
180
195
  get duration(): number;
181
196
  /**
182
197
  * Convenience method for `layers.push()`
198
+ *
183
199
  * @param layer
184
200
  * @return The movie
185
201
  */
186
202
  addLayer(layer: BaseLayer): Movie;
187
203
  /**
188
204
  * Convenience method for `effects.push()`
205
+ *
189
206
  * @param effect
190
207
  * @return the movie
191
208
  */
package/karma.conf.js CHANGED
@@ -3,6 +3,12 @@
3
3
 
4
4
  process.env.CHROME_BIN = require('puppeteer').executablePath()
5
5
 
6
+ // Make sure TEST_SUITE is set
7
+ if (!process.env.TEST_SUITE) {
8
+ console.error('TEST_SUITE environment variable must be set')
9
+ process.exit(1)
10
+ }
11
+
6
12
  module.exports = function (config) {
7
13
  config.set({
8
14
 
@@ -16,8 +22,8 @@ module.exports = function (config) {
16
22
  // list of files / patterns to load in the browser
17
23
  files: [
18
24
  'src/**/*.ts',
19
- 'spec/**/*.ts',
20
- { pattern: 'spec/integration/assets/**/*', included: false }
25
+ `spec/${process.env.TEST_SUITE}/**/*.ts`,
26
+ { pattern: 'spec/assets/**/*', included: false }
21
27
  ],
22
28
 
23
29
  // list of files / patterns to exclude
@@ -57,7 +63,8 @@ module.exports = function (config) {
57
63
  base: 'Firefox',
58
64
  flags: ['-headless'],
59
65
  prefs: {
60
- 'network.proxy.type': 0
66
+ 'network.proxy.type': 0,
67
+ 'media.autoplay.default': 0 // Allow all autoplay
61
68
  }
62
69
  }
63
70
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etro",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "An extendable video-editing framework for the browser",
5
5
  "browser": "dist/etro-cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,6 +10,8 @@
10
10
  "test": "spec"
11
11
  },
12
12
  "devDependencies": {
13
+ "@commitlint/cli": "^17.6.6",
14
+ "@commitlint/format": "^17.4.4",
13
15
  "@rollup/plugin-eslint": "^8.0.2",
14
16
  "@types/jest": "^29.0.0",
15
17
  "@typescript-eslint/eslint-plugin": "^5.30.7",
@@ -25,7 +27,9 @@
25
27
  "eslint-plugin-promise": "^6.0.0",
26
28
  "eslint-plugin-standard": "^5.0.0",
27
29
  "ev": "0.0.7",
30
+ "gitmoji-cli": "^8.4.0",
28
31
  "http-server": "^14.1.1",
32
+ "husky": "^8.0.3",
29
33
  "jasmine": "^3.4.0",
30
34
  "jasmine-ts": "^0.4.0",
31
35
  "karma": "^6.1.1",
@@ -48,14 +52,17 @@
48
52
  "scripts": {
49
53
  "build": "rollup -c",
50
54
  "doc": "rm -rf docs && npx typedoc src/etro.ts --excludePrivate --readme none",
51
- "assets": "git fetch origin example-assets:example-assets && git cherry-pick example-assets && git reset --soft HEAD^ && git reset HEAD examples/assets",
55
+ "prepare": "husky install",
52
56
  "effects": "node scripts/effect/save-effect-samples.js",
53
57
  "lint": "npm run --silent lint:main && npm run --silent lint:test && npm run --silent lint:examples",
54
- "lint:main": "eslint -c eslint.typescript-conf.js --ext .ts --fix src",
55
- "lint:test": "eslint -c eslint.test-conf.js --ext .ts --fix spec",
56
- "lint:examples": "eslint -c eslint.example-conf.js --ext .html --fix examples",
58
+ "fix": "npm run --silent lint:main -- --fix && npm run --silent lint:test -- --fix && npm run --silent lint:examples -- --fix",
59
+ "lint:main": "eslint -c eslint.typescript-conf.js --ext .ts src",
60
+ "lint:test": "eslint -c eslint.test-conf.js --ext .ts spec",
61
+ "lint:examples": "eslint -c eslint.example-conf.js --ext .html examples",
57
62
  "start": "http-server",
58
- "test": "karma start",
63
+ "test:unit": "TEST_SUITE=unit karma start",
64
+ "test:smoke": "TEST_SUITE=smoke karma start",
65
+ "test:integration": "TEST_SUITE=integration karma start",
59
66
  "release": "shipjs prepare"
60
67
  },
61
68
  "repository": {
@@ -39,7 +39,7 @@ function createDirs(filePath) {
39
39
  // remove prefix and save to png
40
40
  const buffer = Buffer.from(item.data.replace(/^data:image\/png;base64,/, ''), 'base64')
41
41
  console.log(`writing ${item.path} ...`)
42
- const path = projectDir + '/spec/integration/assets/effect/' + item.path
42
+ const path = projectDir + '/spec/assets/effect/' + item.path
43
43
  createDirs(path)
44
44
  fs.writeFileSync(path, buffer)
45
45
  })
@@ -50,7 +50,7 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
50
50
  private _unstretchedDuration: number
51
51
  private _playbackRate: number
52
52
  private _initialized: boolean
53
- private _connectedToDestination: boolean
53
+ private _lastAudioDestination: AudioNode
54
54
 
55
55
  /**
56
56
  * @param options
@@ -123,36 +123,18 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
123
123
 
124
124
  // TODO: on unattach?
125
125
  subscribe(movie, 'audiodestinationupdate', event => {
126
- // Connect to new destination if immediately connected to the existing
127
- // destination.
128
- if (this._connectedToDestination) {
129
- this.audioNode.disconnect(movie.actx.destination)
130
- this.audioNode.connect(event.destination)
131
- }
126
+ this.audioNode.disconnect(this._lastAudioDestination)
127
+ this.audioNode.connect(event.destination)
128
+
129
+ this._lastAudioDestination = event.destination
132
130
  })
133
131
 
134
132
  // connect to audiocontext
135
133
  this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source)
136
134
 
137
- // Spy on connect and disconnect to remember if it connected to
138
- // actx.destination (for Movie#record).
139
- const oldConnect = this._audioNode.connect.bind(this.audioNode)
140
- this._audioNode.connect = <T extends AudioDestinationNode>(destination: T, outputIndex?: number, inputIndex?: number): AudioNode => {
141
- this._connectedToDestination = destination === movie.actx.destination
142
- return oldConnect(destination, outputIndex, inputIndex)
143
- }
144
- const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
145
- this._audioNode.disconnect = <T extends AudioDestinationNode>(destination?: T | number, output?: number, input?: number): AudioNode => {
146
- if (this._connectedToDestination &&
147
- destination === movie.actx.destination) {
148
- this._connectedToDestination = false
149
- }
150
-
151
- return oldDisconnect(destination, output, input)
152
- }
153
-
154
135
  // Connect to actx.destination by default (can be rewired by user)
155
136
  this.audioNode.connect(movie.actx.destination)
137
+ this._lastAudioDestination = movie.actx.destination
156
138
  }
157
139
 
158
140
  detach () {
@@ -170,7 +152,11 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
170
152
  seek (time: number): void {
171
153
  super.seek(time)
172
154
 
173
- this.source.currentTime = this.currentTime + this.sourceStartTime
155
+ if (isNaN(this.currentTime)) {
156
+ this.source.currentTime = this.sourceStartTime
157
+ } else {
158
+ this.source.currentTime = this.currentTime + this.sourceStartTime
159
+ }
174
160
  }
175
161
 
176
162
  render () {
@@ -74,8 +74,8 @@ export class Movie {
74
74
  private _recording = false
75
75
  private _currentStream: MediaStream
76
76
  private _endTime: number
77
- private _lastPlayed: number
78
- private _lastPlayedOffset: number
77
+ /** The timestamp last frame in seconds */
78
+ private _lastRealTime: number
79
79
 
80
80
  /**
81
81
  * Creates a new movie.
@@ -111,11 +111,6 @@ export class Movie {
111
111
  // `render`). It's only valid while rendering.
112
112
  this._renderingFrame = false
113
113
  this.currentTime = 0
114
-
115
- // The last time `play` was called, -1 works well in comparisons
116
- this._lastPlayed = -1
117
- // What `currentTime` was when `play` was called
118
- this._lastPlayedOffset = -1
119
114
  }
120
115
 
121
116
  private async _whenReady (): Promise<void> {
@@ -130,11 +125,13 @@ export class Movie {
130
125
  *
131
126
  * @param [options]
132
127
  * @param [options.onStart] Called when the movie starts playing
128
+ * @param [options.duration] The duration of the movie to play in seconds
133
129
  *
134
130
  * @return Fulfilled when the movie is done playing, never fails
135
131
  */
136
132
  async play (options: {
137
133
  onStart?: () => void,
134
+ duration?: number,
138
135
  } = {}): Promise<void> {
139
136
  await this._whenReady()
140
137
 
@@ -143,8 +140,8 @@ export class Movie {
143
140
  }
144
141
 
145
142
  this._paused = this._ended = false
146
- this._lastPlayed = performance.now()
147
- this._lastPlayedOffset = this.currentTime
143
+ this._lastRealTime = performance.now()
144
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration
148
145
 
149
146
  options.onStart?.()
150
147
 
@@ -155,13 +152,16 @@ export class Movie {
155
152
  await new Promise<void>(resolve => {
156
153
  if (!this.renderingFrame) {
157
154
  // Not rendering (and not playing), so play.
158
- this._render(true, undefined, resolve)
155
+ this._render(undefined, resolve)
159
156
  }
160
157
 
161
158
  // Stop rendering frame if currently doing so, because playing has higher
162
159
  // priority. This will affect the next _render call.
163
160
  this._renderingFrame = false
164
161
  })
162
+
163
+ // After we're done playing, clear the last timestamp
164
+ this._lastRealTime = undefined
165
165
  }
166
166
 
167
167
  /**
@@ -244,12 +244,12 @@ export class Movie {
244
244
  this._currentStream = new MediaStream(tracks)
245
245
 
246
246
  // Play the movie
247
- this._endTime = options.duration ? this.currentTime + options.duration : this.duration
248
247
  await this.play({
249
248
  onStart: () => {
250
249
  // Call the user's onStart callback
251
250
  options.onStart(this._currentStream)
252
- }
251
+ },
252
+ duration: options.duration
253
253
  })
254
254
 
255
255
  // Clear the stream after the movie is done playing
@@ -396,11 +396,13 @@ export class Movie {
396
396
  }
397
397
 
398
398
  /**
399
+ * Processes one frame of the movie and draws it to the canvas
400
+ *
399
401
  * @param [timestamp=performance.now()]
400
402
  * @param [done=undefined] - Called when done playing or when the current
401
403
  * frame is loaded
402
404
  */
403
- private _render (repeat, timestamp = performance.now(), done = undefined) {
405
+ private _render (timestamp = performance.now(), done = undefined) {
404
406
  clearCachedValues(this)
405
407
 
406
408
  if (!this.rendering) {
@@ -444,8 +446,6 @@ export class Movie {
444
446
  this._currentTime = 0
445
447
  publish(this, 'movie.timeupdate', { movie: this })
446
448
 
447
- this._lastPlayed = performance.now()
448
- this._lastPlayedOffset = 0 // this.currentTime
449
449
  this._renderingFrame = false
450
450
 
451
451
  // Stop playback or recording if done (except if it's playing and repeat
@@ -505,7 +505,7 @@ export class Movie {
505
505
 
506
506
  // TODO: Is making a new arrow function every frame bad for performance?
507
507
  window.requestAnimationFrame(() => {
508
- this._render(repeat, undefined, done)
508
+ this._render(undefined, done)
509
509
  })
510
510
  }
511
511
 
@@ -513,11 +513,12 @@ export class Movie {
513
513
  // If we're only frame-rendering (current frame only), it doesn't matter if
514
514
  // it's paused or not.
515
515
  if (!this._renderingFrame) {
516
- const sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000
517
- const currentTime = this._lastPlayedOffset + sinceLastPlayed
518
- if (this.currentTime !== currentTime) {
516
+ const timestamp = timestampMs / 1000
517
+ const delta = timestamp - this._lastRealTime
518
+ this._lastRealTime = timestamp
519
+ if (delta > 0) {
519
520
  // Update the current time (don't use setter)
520
- this._currentTime = currentTime
521
+ this._currentTime += delta
521
522
 
522
523
  // For backwards compatibility, publish a 'movie.timeupdate' event.
523
524
  publish(this, 'movie.timeupdate', { movie: this })
@@ -529,7 +530,12 @@ export class Movie {
529
530
  }
530
531
  }
531
532
 
532
- private _renderBackground (timestamp) {
533
+ /**
534
+ * Draws the movie's background to the canvas
535
+ *
536
+ * @param timestamp The current high-resolution timestamp in milliseconds
537
+ */
538
+ private _renderBackground (timestamp: number) {
533
539
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
534
540
 
535
541
  // Evaluate background color (since it's a dynamic property)
@@ -541,7 +547,7 @@ export class Movie {
541
547
  }
542
548
 
543
549
  /**
544
- * @param [timestamp=performance.now()]
550
+ * Ticks all layers and renders them to the canvas
545
551
  */
546
552
  private _renderLayers () {
547
553
  for (let i = 0; i < this.layers.length; i++) {
@@ -595,6 +601,12 @@ export class Movie {
595
601
  }
596
602
  }
597
603
 
604
+ /**
605
+ * Applies all of the movie's effects to the canvas
606
+ *
607
+ * Note: This method only applies the movie's effects, not the layers'
608
+ * effects.
609
+ */
598
610
  private _applyEffects () {
599
611
  for (let i = 0; i < this.effects.length; i++) {
600
612
  const effect = this.effects[i]
@@ -624,7 +636,7 @@ export class Movie {
624
636
 
625
637
  return new Promise(resolve => {
626
638
  this._renderingFrame = true
627
- this._render(false, undefined, resolve)
639
+ this._render(undefined, resolve)
628
640
  })
629
641
  }
630
642
 
@@ -665,13 +677,14 @@ export class Movie {
665
677
  *
666
678
  * Calculated from the end time of the last layer
667
679
  */
668
- // TODO: dirty flag?
680
+ // TODO: cache
669
681
  get duration (): number {
670
682
  return this.layers.reduce((end, layer) => Math.max(layer.startTime + layer.duration, end), 0)
671
683
  }
672
684
 
673
685
  /**
674
686
  * Convenience method for `layers.push()`
687
+ *
675
688
  * @param layer
676
689
  * @return The movie
677
690
  */
@@ -681,6 +694,7 @@ export class Movie {
681
694
 
682
695
  /**
683
696
  * Convenience method for `effects.push()`
697
+ *
684
698
  * @param effect
685
699
  * @return the movie
686
700
  */