etro 0.10.0 → 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,23 @@ 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
+
20
+ ## [0.10.1] - 2023-07-16
21
+ ### Security
22
+ - Bump engine.io and socket.io.
23
+ - Bump socket.io-parser from 4.2.1 to 4.2.3.
24
+
8
25
  ## [0.10.0] - 2023-04-18
9
26
  ### Added
10
27
  - `Movie#stream()` to stream the movie to a `MediaStream`.
@@ -275,6 +292,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
275
292
  - Gaussian blur
276
293
  - Transform
277
294
 
295
+ [0.11.0]: https://github.com/etro-js/etro/compare/v0.10.1...v0.11.0
296
+ [0.10.1]: https://github.com/etro-js/etro/compare/v0.10.0...v0.10.1
278
297
  [0.10.0]: https://github.com/etro-js/etro/compare/v0.9.1...v0.10.0
279
298
  [0.9.1]: https://github.com/etro-js/etro/compare/v0.9.0...v0.9.1
280
299
  [0.9.0]: https://github.com/etro-js/etro/compare/v0.8.5...v0.9.0
package/CONTRIBUTING.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Contributing
2
2
 
3
+ > If you would like to update the docs, please see [the docs repo](https://github.com/etro-js/etro-js.github.io).
4
+
3
5
  ## Introduction
4
6
 
5
7
  Thank you for considering contributing to Etro! There are many ways you can contribute to Etro, like creating issues for features or bugs, improving the docs or wiki, or writing the code for the library. This page covers how to make changes to the repository files (either code or jsdocs).
@@ -8,73 +10,47 @@ Thank you for considering contributing to Etro! There are many ways you can cont
8
10
 
9
11
  ## Setting up your local environment
10
12
 
11
- #### Step 0: Dependencies
12
-
13
13
  - You will need Git, Node, NPM (at least 7.x) and Firefox (for headless functional testing) installed.
14
-
15
- #### Step 1: Fork
16
-
17
- - Create your own fork of Etro. Then run
18
-
14
+ - To get started, create your own fork of Etro. Then run
19
15
  ```
20
16
  git clone https://github.com/YOUR_USERNAME/etro.git
21
17
  cd etro
22
18
  npm install
23
- npm test
19
+ npm run test:unit
20
+ npm run test:smoke
21
+ npm run test:integration
24
22
  ```
25
23
 
26
24
  ## Making your changes
27
25
 
28
- #### Step 2: Code
29
-
30
26
  - Make some changes and update tests
31
27
  - If you are writing code, the linter uses [StandardJS](https://standardjs.com/rules.html) for style conventions
32
28
  - If you're adding or updating an effect:
33
29
  - Add your effect to **scripts/gen-effect-samples.html**
34
30
  - Run `npm run effects`
35
31
  - Briefly review the images in **spec/integration/assets/effect/**
36
- - When you're ready to submit, first run
32
+ - As you work, you can run
37
33
  ```
38
- npm run lint
34
+ npm run fix
39
35
  npm run build
40
- npm test
36
+ npm test:unit
37
+ npm test:smoke
38
+ npm test:integration
41
39
  ```
42
40
 
43
- 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.
44
-
45
- #### Step 3: Commit
46
-
47
- - Please follow these commit message guidelines:
48
- - Optionally, prefix each commit message with [an appropriate emoji](https://gitmoji.dev), such as `:bug:` for fixes.
49
- - Write in the imperative tense
50
- - Wrap lines after 72 characters (for Vim add `filetype indent plugin on` to ~/.vimrc, it's enabled by default in Atom).
51
- - Format:
52
- ```
53
- :emoji: One-liner
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.*
54
43
 
55
- Optional description
56
- ```
44
+ - Please commit to a new branch, not master
57
45
 
58
46
  ## Submitting your changes
59
47
 
60
- #### Step 4: Push
61
-
62
- - First, rebase (please avoid merging) to integrate your work with any new changes in the main repository
63
-
48
+ - Before pushing to your fork, rebase (please avoid merging) to integrate your work with any new changes in the main repository
64
49
  ```
65
50
  git fetch upstream
66
51
  git rebase upstream/master
67
52
  ```
68
-
69
- - Push to the fork
70
-
71
- #### Step 5: Pull request
72
-
73
- - Open a pull request from the branch in your fork to the main repository
74
- - If you changed any core functionality, make sure you explain your motives for those changes
75
-
76
- #### Step 6: Feedback
77
-
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
78
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.
79
55
 
80
56
  ## Code overview
@@ -85,6 +61,8 @@ Check out [the user docs](https://etrojs.dev/docs/intro) for a high-level overvi
85
61
 
86
62
  ### Events
87
63
 
64
+ > Events were deprecated in v0.10.0 in favor of async methods with callbacks.
65
+
88
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,
89
67
 
90
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
@@ -42,6 +41,9 @@ movie.record({ frameRate: 24 }) // or just `play` if you don't need to save it
42
41
  The blob could then be downloaded as a video file or displayed using a `<video>`
43
42
  element.
44
43
 
44
+ See the [documentation](https://etrojs.dev/docs/category/layers) for a list of
45
+ all built-in layers.
46
+
45
47
  ## Effects
46
48
 
47
49
  Effects can transform the output of a layer or movie:
@@ -50,6 +52,9 @@ var layer = new etro.layer.Video({ startTime: 0, source: videoElement })
50
52
  .addEffect(new etro.effect.Brightness({ brightness: +100) }))
51
53
  ```
52
54
 
55
+ See the [documentation](https://etrojs.dev/docs/category/effects) for a list of
56
+ all built-in effects.
57
+
53
58
  ## Dynamic Properties
54
59
 
55
60
  Most properties also support keyframes and functions:
@@ -64,6 +69,9 @@ layer.effects[0].brightness = new etro.KeyFrame(
64
69
  layer.effects[0].brightness = () => 100 * Math.random() - 50
65
70
  ```
66
71
 
72
+ See the [documentation](https://etrojs.dev/docs/reference/dynamic-properties)
73
+ for more info.
74
+
67
75
  ## Using in Node
68
76
 
69
77
  To use Etro in Node, see the [wrapper](https://github.com/etro-js/etro-node):
@@ -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);
@@ -1054,17 +1042,12 @@ var Audio = /** @class */ (function (_super) {
1054
1042
  * Creates an audio layer
1055
1043
  */
1056
1044
  function Audio(options) {
1057
- var _this = this;
1058
1045
  if (typeof options.source === 'string') {
1059
1046
  var audio = document.createElement('audio');
1060
1047
  audio.src = options.source;
1061
1048
  options.source = audio;
1062
1049
  }
1063
- _this = _super.call(this, options) || this;
1064
- if (_this.duration === undefined) {
1065
- _this.duration = (_this).source.duration - _this.sourceStartTime;
1066
- }
1067
- return _this;
1050
+ return _super.call(this, options) || this;
1068
1051
  }
1069
1052
  /**
1070
1053
  * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
@@ -2848,10 +2831,6 @@ var Movie = /** @class */ (function () {
2848
2831
  // `render`). It's only valid while rendering.
2849
2832
  this._renderingFrame = false;
2850
2833
  this.currentTime = 0;
2851
- // The last time `play` was called, -1 works well in comparisons
2852
- this._lastPlayed = -1;
2853
- // What `currentTime` was when `play` was called
2854
- this._lastPlayedOffset = -1;
2855
2834
  }
2856
2835
  Movie.prototype._whenReady = function () {
2857
2836
  return __awaiter(this, void 0, void 0, function () {
@@ -2873,6 +2852,7 @@ var Movie = /** @class */ (function () {
2873
2852
  *
2874
2853
  * @param [options]
2875
2854
  * @param [options.onStart] Called when the movie starts playing
2855
+ * @param [options.duration] The duration of the movie to play in seconds
2876
2856
  *
2877
2857
  * @return Fulfilled when the movie is done playing, never fails
2878
2858
  */
@@ -2890,8 +2870,8 @@ var Movie = /** @class */ (function () {
2890
2870
  throw new Error('Already playing');
2891
2871
  }
2892
2872
  this._paused = this._ended = false;
2893
- this._lastPlayed = performance.now();
2894
- this._lastPlayedOffset = this.currentTime;
2873
+ this._lastRealTime = performance.now();
2874
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2895
2875
  (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2896
2876
  // For backwards compatibility
2897
2877
  publish(this, 'movie.play', {});
@@ -2899,15 +2879,19 @@ var Movie = /** @class */ (function () {
2899
2879
  return [4 /*yield*/, new Promise(function (resolve) {
2900
2880
  if (!_this.renderingFrame) {
2901
2881
  // Not rendering (and not playing), so play.
2902
- _this._render(true, undefined, resolve);
2882
+ _this._render(undefined, resolve);
2903
2883
  }
2904
2884
  // Stop rendering frame if currently doing so, because playing has higher
2905
2885
  // priority. This will affect the next _render call.
2906
2886
  _this._renderingFrame = false;
2907
- })];
2887
+ })
2888
+ // After we're done playing, clear the last timestamp
2889
+ ];
2908
2890
  case 2:
2909
2891
  // Repeatedly render frames until the movie ends
2910
2892
  _b.sent();
2893
+ // After we're done playing, clear the last timestamp
2894
+ this._lastRealTime = undefined;
2911
2895
  return [2 /*return*/];
2912
2896
  }
2913
2897
  });
@@ -2981,16 +2965,17 @@ var Movie = /** @class */ (function () {
2981
2965
  // Create the stream
2982
2966
  this._currentStream = new MediaStream(tracks);
2983
2967
  // Play the movie
2984
- this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2985
2968
  return [4 /*yield*/, this.play({
2986
2969
  onStart: function () {
2987
2970
  // Call the user's onStart callback
2988
2971
  options.onStart(_this._currentStream);
2989
- }
2972
+ },
2973
+ duration: options.duration
2990
2974
  })
2991
2975
  // Clear the stream after the movie is done playing
2992
2976
  ];
2993
2977
  case 2:
2978
+ // Play the movie
2994
2979
  _a.sent();
2995
2980
  // Clear the stream after the movie is done playing
2996
2981
  this._currentStream.getTracks().forEach(function (track) {
@@ -3125,11 +3110,13 @@ var Movie = /** @class */ (function () {
3125
3110
  return this;
3126
3111
  };
3127
3112
  /**
3113
+ * Processes one frame of the movie and draws it to the canvas
3114
+ *
3128
3115
  * @param [timestamp=performance.now()]
3129
3116
  * @param [done=undefined] - Called when done playing or when the current
3130
3117
  * frame is loaded
3131
3118
  */
3132
- Movie.prototype._render = function (repeat, timestamp, done) {
3119
+ Movie.prototype._render = function (timestamp, done) {
3133
3120
  var _this = this;
3134
3121
  if (timestamp === void 0) { timestamp = performance.now(); }
3135
3122
  if (done === void 0) { done = undefined; }
@@ -3166,8 +3153,6 @@ var Movie = /** @class */ (function () {
3166
3153
  // value and publish a 'imeupdate' event.
3167
3154
  this._currentTime = 0;
3168
3155
  publish(this, 'movie.timeupdate', { movie: this });
3169
- this._lastPlayed = performance.now();
3170
- this._lastPlayedOffset = 0; // this.currentTime
3171
3156
  this._renderingFrame = false;
3172
3157
  // Stop playback or recording if done (except if it's playing and repeat
3173
3158
  // is true)
@@ -3219,18 +3204,19 @@ var Movie = /** @class */ (function () {
3219
3204
  }
3220
3205
  // TODO: Is making a new arrow function every frame bad for performance?
3221
3206
  window.requestAnimationFrame(function () {
3222
- _this._render(repeat, undefined, done);
3207
+ _this._render(undefined, done);
3223
3208
  });
3224
3209
  };
3225
3210
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
3226
3211
  // If we're only frame-rendering (current frame only), it doesn't matter if
3227
3212
  // it's paused or not.
3228
3213
  if (!this._renderingFrame) {
3229
- var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
3230
- var currentTime = this._lastPlayedOffset + sinceLastPlayed;
3231
- if (this.currentTime !== currentTime) {
3214
+ var timestamp = timestampMs / 1000;
3215
+ var delta = timestamp - this._lastRealTime;
3216
+ this._lastRealTime = timestamp;
3217
+ if (delta > 0) {
3232
3218
  // Update the current time (don't use setter)
3233
- this._currentTime = currentTime;
3219
+ this._currentTime += delta;
3234
3220
  // For backwards compatibility, publish a 'movie.timeupdate' event.
3235
3221
  publish(this, 'movie.timeupdate', { movie: this });
3236
3222
  }
@@ -3239,6 +3225,11 @@ var Movie = /** @class */ (function () {
3239
3225
  }
3240
3226
  }
3241
3227
  };
3228
+ /**
3229
+ * Draws the movie's background to the canvas
3230
+ *
3231
+ * @param timestamp The current high-resolution timestamp in milliseconds
3232
+ */
3242
3233
  Movie.prototype._renderBackground = function (timestamp) {
3243
3234
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3244
3235
  // Evaluate background color (since it's a dynamic property)
@@ -3249,7 +3240,7 @@ var Movie = /** @class */ (function () {
3249
3240
  }
3250
3241
  };
3251
3242
  /**
3252
- * @param [timestamp=performance.now()]
3243
+ * Ticks all layers and renders them to the canvas
3253
3244
  */
3254
3245
  Movie.prototype._renderLayers = function () {
3255
3246
  for (var i = 0; i < this.layers.length; i++) {
@@ -3294,6 +3285,12 @@ var Movie = /** @class */ (function () {
3294
3285
  }
3295
3286
  }
3296
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
+ */
3297
3294
  Movie.prototype._applyEffects = function () {
3298
3295
  for (var i = 0; i < this.effects.length; i++) {
3299
3296
  var effect = this.effects[i];
@@ -3320,7 +3317,7 @@ var Movie = /** @class */ (function () {
3320
3317
  }
3321
3318
  return new Promise(function (resolve) {
3322
3319
  _this._renderingFrame = true;
3323
- _this._render(false, undefined, resolve);
3320
+ _this._render(undefined, resolve);
3324
3321
  });
3325
3322
  };
3326
3323
  /**
@@ -3369,7 +3366,7 @@ var Movie = /** @class */ (function () {
3369
3366
  *
3370
3367
  * Calculated from the end time of the last layer
3371
3368
  */
3372
- // TODO: dirty flag?
3369
+ // TODO: cache
3373
3370
  get: function () {
3374
3371
  return this.layers.reduce(function (end, layer) { return Math.max(layer.startTime + layer.duration, end); }, 0);
3375
3372
  },
@@ -3378,6 +3375,7 @@ var Movie = /** @class */ (function () {
3378
3375
  });
3379
3376
  /**
3380
3377
  * Convenience method for `layers.push()`
3378
+ *
3381
3379
  * @param layer
3382
3380
  * @return The movie
3383
3381
  */
@@ -3387,6 +3385,7 @@ var Movie = /** @class */ (function () {
3387
3385
  };
3388
3386
  /**
3389
3387
  * Convenience method for `effects.push()`
3388
+ *
3390
3389
  * @param effect
3391
3390
  * @return the movie
3392
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);
@@ -1055,17 +1043,12 @@ var etro = (function () {
1055
1043
  * Creates an audio layer
1056
1044
  */
1057
1045
  function Audio(options) {
1058
- var _this = this;
1059
1046
  if (typeof options.source === 'string') {
1060
1047
  var audio = document.createElement('audio');
1061
1048
  audio.src = options.source;
1062
1049
  options.source = audio;
1063
1050
  }
1064
- _this = _super.call(this, options) || this;
1065
- if (_this.duration === undefined) {
1066
- _this.duration = (_this).source.duration - _this.sourceStartTime;
1067
- }
1068
- return _this;
1051
+ return _super.call(this, options) || this;
1069
1052
  }
1070
1053
  /**
1071
1054
  * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
@@ -2849,10 +2832,6 @@ var etro = (function () {
2849
2832
  // `render`). It's only valid while rendering.
2850
2833
  this._renderingFrame = false;
2851
2834
  this.currentTime = 0;
2852
- // The last time `play` was called, -1 works well in comparisons
2853
- this._lastPlayed = -1;
2854
- // What `currentTime` was when `play` was called
2855
- this._lastPlayedOffset = -1;
2856
2835
  }
2857
2836
  Movie.prototype._whenReady = function () {
2858
2837
  return __awaiter(this, void 0, void 0, function () {
@@ -2874,6 +2853,7 @@ var etro = (function () {
2874
2853
  *
2875
2854
  * @param [options]
2876
2855
  * @param [options.onStart] Called when the movie starts playing
2856
+ * @param [options.duration] The duration of the movie to play in seconds
2877
2857
  *
2878
2858
  * @return Fulfilled when the movie is done playing, never fails
2879
2859
  */
@@ -2891,8 +2871,8 @@ var etro = (function () {
2891
2871
  throw new Error('Already playing');
2892
2872
  }
2893
2873
  this._paused = this._ended = false;
2894
- this._lastPlayed = performance.now();
2895
- this._lastPlayedOffset = this.currentTime;
2874
+ this._lastRealTime = performance.now();
2875
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2896
2876
  (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2897
2877
  // For backwards compatibility
2898
2878
  publish(this, 'movie.play', {});
@@ -2900,15 +2880,19 @@ var etro = (function () {
2900
2880
  return [4 /*yield*/, new Promise(function (resolve) {
2901
2881
  if (!_this.renderingFrame) {
2902
2882
  // Not rendering (and not playing), so play.
2903
- _this._render(true, undefined, resolve);
2883
+ _this._render(undefined, resolve);
2904
2884
  }
2905
2885
  // Stop rendering frame if currently doing so, because playing has higher
2906
2886
  // priority. This will affect the next _render call.
2907
2887
  _this._renderingFrame = false;
2908
- })];
2888
+ })
2889
+ // After we're done playing, clear the last timestamp
2890
+ ];
2909
2891
  case 2:
2910
2892
  // Repeatedly render frames until the movie ends
2911
2893
  _b.sent();
2894
+ // After we're done playing, clear the last timestamp
2895
+ this._lastRealTime = undefined;
2912
2896
  return [2 /*return*/];
2913
2897
  }
2914
2898
  });
@@ -2982,16 +2966,17 @@ var etro = (function () {
2982
2966
  // Create the stream
2983
2967
  this._currentStream = new MediaStream(tracks);
2984
2968
  // Play the movie
2985
- this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2986
2969
  return [4 /*yield*/, this.play({
2987
2970
  onStart: function () {
2988
2971
  // Call the user's onStart callback
2989
2972
  options.onStart(_this._currentStream);
2990
- }
2973
+ },
2974
+ duration: options.duration
2991
2975
  })
2992
2976
  // Clear the stream after the movie is done playing
2993
2977
  ];
2994
2978
  case 2:
2979
+ // Play the movie
2995
2980
  _a.sent();
2996
2981
  // Clear the stream after the movie is done playing
2997
2982
  this._currentStream.getTracks().forEach(function (track) {
@@ -3126,11 +3111,13 @@ var etro = (function () {
3126
3111
  return this;
3127
3112
  };
3128
3113
  /**
3114
+ * Processes one frame of the movie and draws it to the canvas
3115
+ *
3129
3116
  * @param [timestamp=performance.now()]
3130
3117
  * @param [done=undefined] - Called when done playing or when the current
3131
3118
  * frame is loaded
3132
3119
  */
3133
- Movie.prototype._render = function (repeat, timestamp, done) {
3120
+ Movie.prototype._render = function (timestamp, done) {
3134
3121
  var _this = this;
3135
3122
  if (timestamp === void 0) { timestamp = performance.now(); }
3136
3123
  if (done === void 0) { done = undefined; }
@@ -3167,8 +3154,6 @@ var etro = (function () {
3167
3154
  // value and publish a 'imeupdate' event.
3168
3155
  this._currentTime = 0;
3169
3156
  publish(this, 'movie.timeupdate', { movie: this });
3170
- this._lastPlayed = performance.now();
3171
- this._lastPlayedOffset = 0; // this.currentTime
3172
3157
  this._renderingFrame = false;
3173
3158
  // Stop playback or recording if done (except if it's playing and repeat
3174
3159
  // is true)
@@ -3220,18 +3205,19 @@ var etro = (function () {
3220
3205
  }
3221
3206
  // TODO: Is making a new arrow function every frame bad for performance?
3222
3207
  window.requestAnimationFrame(function () {
3223
- _this._render(repeat, undefined, done);
3208
+ _this._render(undefined, done);
3224
3209
  });
3225
3210
  };
3226
3211
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
3227
3212
  // If we're only frame-rendering (current frame only), it doesn't matter if
3228
3213
  // it's paused or not.
3229
3214
  if (!this._renderingFrame) {
3230
- var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
3231
- var currentTime = this._lastPlayedOffset + sinceLastPlayed;
3232
- if (this.currentTime !== currentTime) {
3215
+ var timestamp = timestampMs / 1000;
3216
+ var delta = timestamp - this._lastRealTime;
3217
+ this._lastRealTime = timestamp;
3218
+ if (delta > 0) {
3233
3219
  // Update the current time (don't use setter)
3234
- this._currentTime = currentTime;
3220
+ this._currentTime += delta;
3235
3221
  // For backwards compatibility, publish a 'movie.timeupdate' event.
3236
3222
  publish(this, 'movie.timeupdate', { movie: this });
3237
3223
  }
@@ -3240,6 +3226,11 @@ var etro = (function () {
3240
3226
  }
3241
3227
  }
3242
3228
  };
3229
+ /**
3230
+ * Draws the movie's background to the canvas
3231
+ *
3232
+ * @param timestamp The current high-resolution timestamp in milliseconds
3233
+ */
3243
3234
  Movie.prototype._renderBackground = function (timestamp) {
3244
3235
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3245
3236
  // Evaluate background color (since it's a dynamic property)
@@ -3250,7 +3241,7 @@ var etro = (function () {
3250
3241
  }
3251
3242
  };
3252
3243
  /**
3253
- * @param [timestamp=performance.now()]
3244
+ * Ticks all layers and renders them to the canvas
3254
3245
  */
3255
3246
  Movie.prototype._renderLayers = function () {
3256
3247
  for (var i = 0; i < this.layers.length; i++) {
@@ -3295,6 +3286,12 @@ var etro = (function () {
3295
3286
  }
3296
3287
  }
3297
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
+ */
3298
3295
  Movie.prototype._applyEffects = function () {
3299
3296
  for (var i = 0; i < this.effects.length; i++) {
3300
3297
  var effect = this.effects[i];
@@ -3321,7 +3318,7 @@ var etro = (function () {
3321
3318
  }
3322
3319
  return new Promise(function (resolve) {
3323
3320
  _this._renderingFrame = true;
3324
- _this._render(false, undefined, resolve);
3321
+ _this._render(undefined, resolve);
3325
3322
  });
3326
3323
  };
3327
3324
  /**
@@ -3370,7 +3367,7 @@ var etro = (function () {
3370
3367
  *
3371
3368
  * Calculated from the end time of the last layer
3372
3369
  */
3373
- // TODO: dirty flag?
3370
+ // TODO: cache
3374
3371
  get: function () {
3375
3372
  return this.layers.reduce(function (end, layer) { return Math.max(layer.startTime + layer.duration, end); }, 0);
3376
3373
  },
@@ -3379,6 +3376,7 @@ var etro = (function () {
3379
3376
  });
3380
3377
  /**
3381
3378
  * Convenience method for `layers.push()`
3379
+ *
3382
3380
  * @param layer
3383
3381
  * @return The movie
3384
3382
  */
@@ -3388,6 +3386,7 @@ var etro = (function () {
3388
3386
  };
3389
3387
  /**
3390
3388
  * Convenience method for `effects.push()`
3389
+ *
3391
3390
  * @param effect
3392
3391
  * @return the movie
3393
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
  },
@@ -65,7 +72,7 @@ module.exports = function (config) {
65
72
  client: {
66
73
  captureConsole: true,
67
74
  jasmine: {
68
- timeoutInterval: 20000
75
+ timeoutInterval: 15000
69
76
  }
70
77
  },
71
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etro",
3
- "version": "0.10.0",
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 () {
@@ -31,10 +31,6 @@ class Audio extends AudioSourceMixin<BaseOptions>(Base) {
31
31
  }
32
32
 
33
33
  super(options)
34
-
35
- if (this.duration === undefined) {
36
- this.duration = (this).source.duration - this.sourceStartTime
37
- }
38
34
  }
39
35
 
40
36
  /**
@@ -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
@@ -371,7 +371,7 @@ export class Movie {
371
371
  if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
372
372
  const layer = this.layers[i]
373
373
 
374
- if(layer.active) {
374
+ if (layer.active) {
375
375
  layer.stop()
376
376
  layer.active = false
377
377
  }
@@ -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
  */