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.
- package/.github/workflows/nodejs.yml +17 -4
- package/.github/workflows/shipjs-trigger.yml +4 -1
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +7 -0
- package/.husky/prepare-commit-msg +11 -0
- package/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +16 -40
- package/README.md +1 -2
- package/commitlint.config.ts +39 -0
- package/dist/etro-cjs.js +47 -43
- package/dist/etro-iife.js +47 -43
- package/dist/movie/movie.d.ts +20 -3
- package/karma.conf.js +10 -3
- package/package.json +13 -6
- package/scripts/effect/save-effect-samples.js +1 -1
- package/src/layer/audio-source.ts +11 -25
- package/src/movie/movie.ts +38 -24
|
@@ -21,12 +21,25 @@ jobs:
|
|
|
21
21
|
- name: Update npm
|
|
22
22
|
run: |
|
|
23
23
|
npm i -g npm@^7.x
|
|
24
|
-
- name: npm
|
|
24
|
+
- name: Install npm dependencies
|
|
25
25
|
run: |
|
|
26
26
|
npm ci
|
|
27
27
|
node node_modules/puppeteer/install.js
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
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 }}
|
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
|
-
-
|
|
32
|
+
- As you work, you can run
|
|
39
33
|
```
|
|
40
|
-
npm run
|
|
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
|
|
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
|
|
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
|
-
|
|
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://www.npmjs.com/package/etro)
|
|
4
4
|
[](https://actions-badge.atrox.dev/etro-js/etro/goto)
|
|
5
|
+
[](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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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.
|
|
2889
|
-
this.
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
3225
|
-
var
|
|
3226
|
-
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
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:
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
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.
|
|
2890
|
-
this.
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
3226
|
-
var
|
|
3227
|
-
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
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:
|
|
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
|
*/
|
package/dist/movie/movie.d.ts
CHANGED
|
@@ -62,8 +62,8 @@ export declare class Movie {
|
|
|
62
62
|
private _recording;
|
|
63
63
|
private _currentStream;
|
|
64
64
|
private _endTime;
|
|
65
|
-
|
|
66
|
-
private
|
|
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
|
-
*
|
|
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
|
-
|
|
20
|
-
{ pattern: 'spec/
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
55
|
-
"lint:
|
|
56
|
-
"lint:
|
|
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/
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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 () {
|
package/src/movie/movie.ts
CHANGED
|
@@ -74,8 +74,8 @@ export class Movie {
|
|
|
74
74
|
private _recording = false
|
|
75
75
|
private _currentStream: MediaStream
|
|
76
76
|
private _endTime: number
|
|
77
|
-
|
|
78
|
-
private
|
|
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.
|
|
147
|
-
this.
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
517
|
-
const
|
|
518
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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(
|
|
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:
|
|
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
|
*/
|