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.
- 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 +19 -0
- package/CONTRIBUTING.md +18 -40
- package/README.md +10 -2
- package/commitlint.config.ts +39 -0
- package/dist/etro-cjs.js +48 -49
- package/dist/etro-iife.js +48 -49
- package/dist/movie/movie.d.ts +20 -3
- package/karma.conf.js +11 -4
- package/package.json +13 -6
- package/scripts/effect/save-effect-samples.js +1 -1
- package/src/layer/audio-source.ts +11 -25
- package/src/layer/audio.ts +0 -4
- package/src/movie/movie.ts +39 -25
|
@@ -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,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
|
-
-
|
|
32
|
+
- As you work, you can run
|
|
37
33
|
```
|
|
38
|
-
npm run
|
|
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
|
|
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
|
-
|
|
56
|
-
```
|
|
44
|
+
- Please commit to a new branch, not master
|
|
57
45
|
|
|
58
46
|
## Submitting your changes
|
|
59
47
|
|
|
60
|
-
|
|
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://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
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -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
|
-
|
|
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.
|
|
2894
|
-
this.
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
3230
|
-
var
|
|
3231
|
-
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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);
|
|
@@ -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
|
-
|
|
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.
|
|
2895
|
-
this.
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
3231
|
-
var
|
|
3232
|
-
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
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:
|
|
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
|
*/
|
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
|
},
|
|
@@ -65,7 +72,7 @@ module.exports = function (config) {
|
|
|
65
72
|
client: {
|
|
66
73
|
captureConsole: true,
|
|
67
74
|
jasmine: {
|
|
68
|
-
timeoutInterval:
|
|
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.
|
|
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/layer/audio.ts
CHANGED
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
|
|
@@ -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 (
|
|
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
|
*/
|