etro 0.10.1 → 0.12.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/dist/etro-iife.js CHANGED
@@ -668,9 +668,11 @@ var etro = (function () {
668
668
  _this._sourceStartTime = options.sourceStartTime || 0;
669
669
  applyOptions(options, _this);
670
670
  var load = function () {
671
- // TODO: && ?
672
- if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0) {
673
- throw new Error('Invalid options.duration or options.sourceStartTime');
671
+ if (options.duration < 0) {
672
+ throw new Error('Invalid options.duration. It must be a non-negative value.');
673
+ }
674
+ if (_this.sourceStartTime > _this.source.duration) {
675
+ throw new Error('options.sourceStartTime cannot exceed options.source.duration');
674
676
  }
675
677
  _this._unstretchedDuration = options.duration || (_this.source.duration - _this.sourceStartTime);
676
678
  _this.duration = _this._unstretchedDuration / (_this.playbackRate);
@@ -716,32 +718,15 @@ var etro = (function () {
716
718
  _super.prototype.attach.call(this, movie);
717
719
  // TODO: on unattach?
718
720
  subscribe(movie, 'audiodestinationupdate', function (event) {
719
- // Connect to new destination if immediately connected to the existing
720
- // destination.
721
- if (_this._connectedToDestination) {
722
- _this.audioNode.disconnect(movie.actx.destination);
723
- _this.audioNode.connect(event.destination);
724
- }
721
+ _this.audioNode.disconnect(_this._lastAudioDestination);
722
+ _this.audioNode.connect(event.destination);
723
+ _this._lastAudioDestination = event.destination;
725
724
  });
726
725
  // connect to audiocontext
727
726
  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
727
  // Connect to actx.destination by default (can be rewired by user)
744
728
  this.audioNode.connect(movie.actx.destination);
729
+ this._lastAudioDestination = movie.actx.destination;
745
730
  };
746
731
  MixedAudioSource.prototype.detach = function () {
747
732
  // Cache dest before super.detach() unsets this.movie
@@ -755,7 +740,12 @@ var etro = (function () {
755
740
  };
756
741
  MixedAudioSource.prototype.seek = function (time) {
757
742
  _super.prototype.seek.call(this, time);
758
- this.source.currentTime = this.currentTime + this.sourceStartTime;
743
+ if (isNaN(this.currentTime)) {
744
+ this.source.currentTime = this.sourceStartTime;
745
+ }
746
+ else {
747
+ this.source.currentTime = this.currentTime + this.sourceStartTime;
748
+ }
759
749
  };
760
750
  MixedAudioSource.prototype.render = function () {
761
751
  _super.prototype.render.call(this);
@@ -1445,6 +1435,12 @@ var etro = (function () {
1445
1435
  return Image;
1446
1436
  }(VisualSourceMixin(Visual)));
1447
1437
 
1438
+ var TextStrokePosition;
1439
+ (function (TextStrokePosition) {
1440
+ TextStrokePosition[TextStrokePosition["Inside"] = 0] = "Inside";
1441
+ TextStrokePosition[TextStrokePosition["Center"] = 1] = "Center";
1442
+ TextStrokePosition[TextStrokePosition["Outside"] = 2] = "Outside";
1443
+ })(TextStrokePosition || (TextStrokePosition = {}));
1448
1444
  var Text = /** @class */ (function (_super) {
1449
1445
  __extends(Text, _super);
1450
1446
  /**
@@ -1469,6 +1465,7 @@ var etro = (function () {
1469
1465
  // this._prevMaxWidth = undefined;
1470
1466
  }
1471
1467
  Text.prototype.doRender = function () {
1468
+ var _a, _b;
1472
1469
  _super.prototype.doRender.call(this);
1473
1470
  var text = val(this, 'text', this.currentTime);
1474
1471
  var font = val(this, 'font', this.currentTime);
@@ -1482,6 +1479,28 @@ var etro = (function () {
1482
1479
  this.cctx.textBaseline = val(this, 'textBaseline', this.currentTime);
1483
1480
  this.cctx.direction = val(this, 'textDirection', this.currentTime);
1484
1481
  this.cctx.fillText(text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime), maxWidth);
1482
+ var textStroke = val(this, 'textStroke', this.currentTime);
1483
+ if (textStroke) {
1484
+ this.cctx.strokeStyle = textStroke.color;
1485
+ this.cctx.lineWidth = (_a = textStroke.thickness) !== null && _a !== void 0 ? _a : 1;
1486
+ var position = (_b = textStroke.position) !== null && _b !== void 0 ? _b : 'outer';
1487
+ // Save the globalCompositeOperation, we have to revert it after stroking the text.
1488
+ var globalCompositionOperation = this.cctx.globalCompositeOperation;
1489
+ switch (position) {
1490
+ case TextStrokePosition.Inside:
1491
+ this.cctx.globalCompositeOperation = 'source-atop';
1492
+ this.cctx.lineWidth *= 2;
1493
+ break;
1494
+ case TextStrokePosition.Center:
1495
+ break;
1496
+ case TextStrokePosition.Outside:
1497
+ this.cctx.globalCompositeOperation = 'destination-over';
1498
+ this.cctx.lineWidth *= 2;
1499
+ break;
1500
+ }
1501
+ this.cctx.strokeText(text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime), maxWidth);
1502
+ this.cctx.globalCompositeOperation = globalCompositionOperation;
1503
+ }
1485
1504
  this._prevText = text;
1486
1505
  this._prevFont = font;
1487
1506
  this._prevMaxWidth = maxWidth;
@@ -1510,7 +1529,7 @@ var etro = (function () {
1510
1529
  * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1511
1530
  */
1512
1531
  Text.prototype.getDefaultOptions = function () {
1513
- return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
1532
+ return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr', textStroke: null });
1514
1533
  };
1515
1534
  return Text;
1516
1535
  }(Visual));
@@ -1548,6 +1567,7 @@ var etro = (function () {
1548
1567
  Base: Base,
1549
1568
  Image: Image,
1550
1569
  Text: Text,
1570
+ get TextStrokePosition () { return TextStrokePosition; },
1551
1571
  Video: Video,
1552
1572
  VisualSourceMixin: VisualSourceMixin,
1553
1573
  Visual: Visual
@@ -2844,10 +2864,6 @@ var etro = (function () {
2844
2864
  // `render`). It's only valid while rendering.
2845
2865
  this._renderingFrame = false;
2846
2866
  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
2867
  }
2852
2868
  Movie.prototype._whenReady = function () {
2853
2869
  return __awaiter(this, void 0, void 0, function () {
@@ -2869,6 +2885,7 @@ var etro = (function () {
2869
2885
  *
2870
2886
  * @param [options]
2871
2887
  * @param [options.onStart] Called when the movie starts playing
2888
+ * @param [options.duration] The duration of the movie to play in seconds
2872
2889
  *
2873
2890
  * @return Fulfilled when the movie is done playing, never fails
2874
2891
  */
@@ -2886,8 +2903,8 @@ var etro = (function () {
2886
2903
  throw new Error('Already playing');
2887
2904
  }
2888
2905
  this._paused = this._ended = false;
2889
- this._lastPlayed = performance.now();
2890
- this._lastPlayedOffset = this.currentTime;
2906
+ this._lastRealTime = performance.now();
2907
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2891
2908
  (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2892
2909
  // For backwards compatibility
2893
2910
  publish(this, 'movie.play', {});
@@ -2895,15 +2912,19 @@ var etro = (function () {
2895
2912
  return [4 /*yield*/, new Promise(function (resolve) {
2896
2913
  if (!_this.renderingFrame) {
2897
2914
  // Not rendering (and not playing), so play.
2898
- _this._render(true, undefined, resolve);
2915
+ _this._render(undefined, resolve);
2899
2916
  }
2900
2917
  // Stop rendering frame if currently doing so, because playing has higher
2901
2918
  // priority. This will affect the next _render call.
2902
2919
  _this._renderingFrame = false;
2903
- })];
2920
+ })
2921
+ // After we're done playing, clear the last timestamp
2922
+ ];
2904
2923
  case 2:
2905
2924
  // Repeatedly render frames until the movie ends
2906
2925
  _b.sent();
2926
+ // After we're done playing, clear the last timestamp
2927
+ this._lastRealTime = undefined;
2907
2928
  return [2 /*return*/];
2908
2929
  }
2909
2930
  });
@@ -2977,16 +2998,17 @@ var etro = (function () {
2977
2998
  // Create the stream
2978
2999
  this._currentStream = new MediaStream(tracks);
2979
3000
  // Play the movie
2980
- this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2981
3001
  return [4 /*yield*/, this.play({
2982
3002
  onStart: function () {
2983
3003
  // Call the user's onStart callback
2984
3004
  options.onStart(_this._currentStream);
2985
- }
3005
+ },
3006
+ duration: options.duration
2986
3007
  })
2987
3008
  // Clear the stream after the movie is done playing
2988
3009
  ];
2989
3010
  case 2:
3011
+ // Play the movie
2990
3012
  _a.sent();
2991
3013
  // Clear the stream after the movie is done playing
2992
3014
  this._currentStream.getTracks().forEach(function (track) {
@@ -3121,11 +3143,13 @@ var etro = (function () {
3121
3143
  return this;
3122
3144
  };
3123
3145
  /**
3146
+ * Processes one frame of the movie and draws it to the canvas
3147
+ *
3124
3148
  * @param [timestamp=performance.now()]
3125
3149
  * @param [done=undefined] - Called when done playing or when the current
3126
3150
  * frame is loaded
3127
3151
  */
3128
- Movie.prototype._render = function (repeat, timestamp, done) {
3152
+ Movie.prototype._render = function (timestamp, done) {
3129
3153
  var _this = this;
3130
3154
  if (timestamp === void 0) { timestamp = performance.now(); }
3131
3155
  if (done === void 0) { done = undefined; }
@@ -3162,8 +3186,6 @@ var etro = (function () {
3162
3186
  // value and publish a 'imeupdate' event.
3163
3187
  this._currentTime = 0;
3164
3188
  publish(this, 'movie.timeupdate', { movie: this });
3165
- this._lastPlayed = performance.now();
3166
- this._lastPlayedOffset = 0; // this.currentTime
3167
3189
  this._renderingFrame = false;
3168
3190
  // Stop playback or recording if done (except if it's playing and repeat
3169
3191
  // is true)
@@ -3215,18 +3237,19 @@ var etro = (function () {
3215
3237
  }
3216
3238
  // TODO: Is making a new arrow function every frame bad for performance?
3217
3239
  window.requestAnimationFrame(function () {
3218
- _this._render(repeat, undefined, done);
3240
+ _this._render(undefined, done);
3219
3241
  });
3220
3242
  };
3221
3243
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
3222
3244
  // If we're only frame-rendering (current frame only), it doesn't matter if
3223
3245
  // it's paused or not.
3224
3246
  if (!this._renderingFrame) {
3225
- var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
3226
- var currentTime = this._lastPlayedOffset + sinceLastPlayed;
3227
- if (this.currentTime !== currentTime) {
3247
+ var timestamp = timestampMs / 1000;
3248
+ var delta = timestamp - this._lastRealTime;
3249
+ this._lastRealTime = timestamp;
3250
+ if (delta > 0) {
3228
3251
  // Update the current time (don't use setter)
3229
- this._currentTime = currentTime;
3252
+ this._currentTime += delta;
3230
3253
  // For backwards compatibility, publish a 'movie.timeupdate' event.
3231
3254
  publish(this, 'movie.timeupdate', { movie: this });
3232
3255
  }
@@ -3235,6 +3258,11 @@ var etro = (function () {
3235
3258
  }
3236
3259
  }
3237
3260
  };
3261
+ /**
3262
+ * Draws the movie's background to the canvas
3263
+ *
3264
+ * @param timestamp The current high-resolution timestamp in milliseconds
3265
+ */
3238
3266
  Movie.prototype._renderBackground = function (timestamp) {
3239
3267
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3240
3268
  // Evaluate background color (since it's a dynamic property)
@@ -3245,7 +3273,7 @@ var etro = (function () {
3245
3273
  }
3246
3274
  };
3247
3275
  /**
3248
- * @param [timestamp=performance.now()]
3276
+ * Ticks all layers and renders them to the canvas
3249
3277
  */
3250
3278
  Movie.prototype._renderLayers = function () {
3251
3279
  for (var i = 0; i < this.layers.length; i++) {
@@ -3290,6 +3318,12 @@ var etro = (function () {
3290
3318
  }
3291
3319
  }
3292
3320
  };
3321
+ /**
3322
+ * Applies all of the movie's effects to the canvas
3323
+ *
3324
+ * Note: This method only applies the movie's effects, not the layers'
3325
+ * effects.
3326
+ */
3293
3327
  Movie.prototype._applyEffects = function () {
3294
3328
  for (var i = 0; i < this.effects.length; i++) {
3295
3329
  var effect = this.effects[i];
@@ -3316,7 +3350,7 @@ var etro = (function () {
3316
3350
  }
3317
3351
  return new Promise(function (resolve) {
3318
3352
  _this._renderingFrame = true;
3319
- _this._render(false, undefined, resolve);
3353
+ _this._render(undefined, resolve);
3320
3354
  });
3321
3355
  };
3322
3356
  /**
@@ -3365,7 +3399,7 @@ var etro = (function () {
3365
3399
  *
3366
3400
  * Calculated from the end time of the last layer
3367
3401
  */
3368
- // TODO: dirty flag?
3402
+ // TODO: cache
3369
3403
  get: function () {
3370
3404
  return this.layers.reduce(function (end, layer) { return Math.max(layer.startTime + layer.duration, end); }, 0);
3371
3405
  },
@@ -3374,6 +3408,7 @@ var etro = (function () {
3374
3408
  });
3375
3409
  /**
3376
3410
  * Convenience method for `layers.push()`
3411
+ *
3377
3412
  * @param layer
3378
3413
  * @return The movie
3379
3414
  */
@@ -3383,6 +3418,7 @@ var etro = (function () {
3383
3418
  };
3384
3419
  /**
3385
3420
  * Convenience method for `effects.push()`
3421
+ *
3386
3422
  * @param effect
3387
3423
  * @return the movie
3388
3424
  */
@@ -1,5 +1,10 @@
1
1
  import { Dynamic, Color } from '../util';
2
2
  import { Visual, VisualOptions } from './visual';
3
+ declare enum TextStrokePosition {
4
+ Inside = 0,
5
+ Center = 1,
6
+ Outside = 2
7
+ }
3
8
  interface TextOptions extends VisualOptions {
4
9
  text: Dynamic<string>;
5
10
  font?: Dynamic<string>;
@@ -23,6 +28,11 @@ interface TextOptions extends VisualOptions {
23
28
  * @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
24
29
  */
25
30
  textDirection?: Dynamic<string>;
31
+ textStroke?: Dynamic<{
32
+ color: Color;
33
+ position?: TextStrokePosition;
34
+ thickness?: number;
35
+ }>;
26
36
  }
27
37
  declare class Text extends Visual {
28
38
  text: Dynamic<string>;
@@ -47,6 +57,11 @@ declare class Text extends Visual {
47
57
  * @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
48
58
  */
49
59
  textDirection: Dynamic<string>;
60
+ textStroke?: Dynamic<{
61
+ color: Color;
62
+ position?: TextStrokePosition;
63
+ thickness?: number;
64
+ }>;
50
65
  private _prevText;
51
66
  private _prevFont;
52
67
  private _prevMaxWidth;
@@ -60,4 +75,4 @@ declare class Text extends Visual {
60
75
  */
61
76
  getDefaultOptions(): TextOptions;
62
77
  }
63
- export { Text, TextOptions };
78
+ export { Text, TextOptions, TextStrokePosition };
@@ -62,8 +62,8 @@ export declare class Movie {
62
62
  private _recording;
63
63
  private _currentStream;
64
64
  private _endTime;
65
- private _lastPlayed;
66
- private _lastPlayedOffset;
65
+ /** The timestamp last frame in seconds */
66
+ private _lastRealTime;
67
67
  /**
68
68
  * Creates a new movie.
69
69
  */
@@ -74,11 +74,13 @@ export declare class Movie {
74
74
  *
75
75
  * @param [options]
76
76
  * @param [options.onStart] Called when the movie starts playing
77
+ * @param [options.duration] The duration of the movie to play in seconds
77
78
  *
78
79
  * @return Fulfilled when the movie is done playing, never fails
79
80
  */
80
81
  play(options?: {
81
82
  onStart?: () => void;
83
+ duration?: number;
82
84
  }): Promise<void>;
83
85
  /**
84
86
  * Updates the rendering canvas and audio destination to the visible canvas
@@ -136,17 +138,30 @@ export declare class Movie {
136
138
  */
137
139
  stop(): Movie;
138
140
  /**
141
+ * Processes one frame of the movie and draws it to the canvas
142
+ *
139
143
  * @param [timestamp=performance.now()]
140
144
  * @param [done=undefined] - Called when done playing or when the current
141
145
  * frame is loaded
142
146
  */
143
147
  private _render;
144
148
  private _updateCurrentTime;
149
+ /**
150
+ * Draws the movie's background to the canvas
151
+ *
152
+ * @param timestamp The current high-resolution timestamp in milliseconds
153
+ */
145
154
  private _renderBackground;
146
155
  /**
147
- * @param [timestamp=performance.now()]
156
+ * Ticks all layers and renders them to the canvas
148
157
  */
149
158
  private _renderLayers;
159
+ /**
160
+ * Applies all of the movie's effects to the canvas
161
+ *
162
+ * Note: This method only applies the movie's effects, not the layers'
163
+ * effects.
164
+ */
150
165
  private _applyEffects;
151
166
  /**
152
167
  * Refreshes the screen
@@ -180,12 +195,14 @@ export declare class Movie {
180
195
  get duration(): number;
181
196
  /**
182
197
  * Convenience method for `layers.push()`
198
+ *
183
199
  * @param layer
184
200
  * @return The movie
185
201
  */
186
202
  addLayer(layer: BaseLayer): Movie;
187
203
  /**
188
204
  * Convenience method for `effects.push()`
205
+ *
189
206
  * @param effect
190
207
  * @return the movie
191
208
  */
package/karma.conf.js CHANGED
@@ -3,6 +3,12 @@
3
3
 
4
4
  process.env.CHROME_BIN = require('puppeteer').executablePath()
5
5
 
6
+ // Make sure TEST_SUITE is set
7
+ if (!process.env.TEST_SUITE) {
8
+ console.error('TEST_SUITE environment variable must be set')
9
+ process.exit(1)
10
+ }
11
+
6
12
  module.exports = function (config) {
7
13
  config.set({
8
14
 
@@ -16,8 +22,8 @@ module.exports = function (config) {
16
22
  // list of files / patterns to load in the browser
17
23
  files: [
18
24
  'src/**/*.ts',
19
- 'spec/**/*.ts',
20
- { pattern: 'spec/integration/assets/**/*', included: false }
25
+ `spec/${process.env.TEST_SUITE}/**/*.ts`,
26
+ { pattern: 'spec/assets/**/*', included: false }
21
27
  ],
22
28
 
23
29
  // list of files / patterns to exclude
@@ -57,7 +63,8 @@ module.exports = function (config) {
57
63
  base: 'Firefox',
58
64
  flags: ['-headless'],
59
65
  prefs: {
60
- 'network.proxy.type': 0
66
+ 'network.proxy.type': 0,
67
+ 'media.autoplay.default': 0 // Allow all autoplay
61
68
  }
62
69
  }
63
70
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etro",
3
- "version": "0.10.1",
3
+ "version": "0.12.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,11 +10,14 @@
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",
16
18
  "@typescript-eslint/parser": "^5.30.7",
17
19
  "canvas": "^2.11.2",
20
+ "cross-env": "^7.0.3",
18
21
  "docdash": "^1.1.1",
19
22
  "ecstatic": ">=4.1.3",
20
23
  "eslint": "^8.20.0",
@@ -25,7 +28,9 @@
25
28
  "eslint-plugin-promise": "^6.0.0",
26
29
  "eslint-plugin-standard": "^5.0.0",
27
30
  "ev": "0.0.7",
31
+ "gitmoji-cli": "^9.0.0",
28
32
  "http-server": "^14.1.1",
33
+ "husky": "^8.0.3",
29
34
  "jasmine": "^3.4.0",
30
35
  "jasmine-ts": "^0.4.0",
31
36
  "karma": "^6.1.1",
@@ -48,14 +53,17 @@
48
53
  "scripts": {
49
54
  "build": "rollup -c",
50
55
  "doc": "rm -rf docs && npx typedoc src/etro.ts --excludePrivate --readme none",
51
- "assets": "git fetch origin example-assets:example-assets && git cherry-pick example-assets && git reset --soft HEAD^ && git reset HEAD examples/assets",
56
+ "prepare": "husky install",
52
57
  "effects": "node scripts/effect/save-effect-samples.js",
53
58
  "lint": "npm run --silent lint:main && npm run --silent lint:test && npm run --silent lint:examples",
54
- "lint:main": "eslint -c eslint.typescript-conf.js --ext .ts --fix src",
55
- "lint:test": "eslint -c eslint.test-conf.js --ext .ts --fix spec",
56
- "lint:examples": "eslint -c eslint.example-conf.js --ext .html --fix examples",
59
+ "fix": "npm run --silent lint:main -- --fix && npm run --silent lint:test -- --fix && npm run --silent lint:examples -- --fix",
60
+ "lint:main": "eslint -c eslint.typescript-conf.js --ext .ts src",
61
+ "lint:test": "eslint -c eslint.test-conf.js --ext .ts spec",
62
+ "lint:examples": "eslint -c eslint.example-conf.js --ext .html examples",
57
63
  "start": "http-server",
58
- "test": "karma start",
64
+ "test:unit": "cross-env TEST_SUITE=unit karma start",
65
+ "test:smoke": "cross-env TEST_SUITE=smoke karma start",
66
+ "test:integration": "cross-env TEST_SUITE=integration karma start",
59
67
  "release": "shipjs prepare"
60
68
  },
61
69
  "repository": {
@@ -39,7 +39,7 @@ function createDirs(filePath) {
39
39
  // remove prefix and save to png
40
40
  const buffer = Buffer.from(item.data.replace(/^data:image\/png;base64,/, ''), 'base64')
41
41
  console.log(`writing ${item.path} ...`)
42
- const path = projectDir + '/spec/integration/assets/effect/' + item.path
42
+ const path = projectDir + '/spec/assets/effect/' + item.path
43
43
  createDirs(path)
44
44
  fs.writeFileSync(path, buffer)
45
45
  })
package/ship.config.js CHANGED
@@ -24,7 +24,18 @@ module.exports = {
24
24
  release.setDate(new Date()) // today
25
25
  const newChangelog = parsed.toString()
26
26
  fs.writeFileSync(changelogFile, newChangelog, 'utf8')
27
- }
27
+ },
28
+ releases: [
29
+ {
30
+ extractChangelog: ({ version, dir }) => {
31
+ const changelogFile = `${dir}/CHANGELOG.md`
32
+ const changelog = fs.readFileSync(changelogFile, 'utf8')
33
+ const parsed = parser(changelog)
34
+ const release = parsed.findRelease(version)
35
+ return `${release.toString()}\n${release.getCompareLink()}`
36
+ },
37
+ }
38
+ ]
28
39
  }
29
40
 
30
41
  class Changelog {
@@ -50,7 +50,7 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
50
50
  private _unstretchedDuration: number
51
51
  private _playbackRate: number
52
52
  private _initialized: boolean
53
- private _connectedToDestination: boolean
53
+ private _lastAudioDestination: AudioNode
54
54
 
55
55
  /**
56
56
  * @param options
@@ -85,9 +85,12 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
85
85
  applyOptions(options, this)
86
86
 
87
87
  const load = () => {
88
- // TODO: && ?
89
- if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0) {
90
- throw new Error('Invalid options.duration or options.sourceStartTime')
88
+ if (options.duration < 0) {
89
+ throw new Error('Invalid options.duration. It must be a non-negative value.')
90
+ }
91
+
92
+ if (this.sourceStartTime > this.source.duration) {
93
+ throw new Error('options.sourceStartTime cannot exceed options.source.duration')
91
94
  }
92
95
 
93
96
  this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
@@ -123,36 +126,18 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
123
126
 
124
127
  // TODO: on unattach?
125
128
  subscribe(movie, 'audiodestinationupdate', event => {
126
- // Connect to new destination if immediately connected to the existing
127
- // destination.
128
- if (this._connectedToDestination) {
129
- this.audioNode.disconnect(movie.actx.destination)
130
- this.audioNode.connect(event.destination)
131
- }
129
+ this.audioNode.disconnect(this._lastAudioDestination)
130
+ this.audioNode.connect(event.destination)
131
+
132
+ this._lastAudioDestination = event.destination
132
133
  })
133
134
 
134
135
  // connect to audiocontext
135
136
  this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source)
136
137
 
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
138
  // Connect to actx.destination by default (can be rewired by user)
155
139
  this.audioNode.connect(movie.actx.destination)
140
+ this._lastAudioDestination = movie.actx.destination
156
141
  }
157
142
 
158
143
  detach () {
@@ -170,7 +155,11 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
170
155
  seek (time: number): void {
171
156
  super.seek(time)
172
157
 
173
- this.source.currentTime = this.currentTime + this.sourceStartTime
158
+ if (isNaN(this.currentTime)) {
159
+ this.source.currentTime = this.sourceStartTime
160
+ } else {
161
+ this.source.currentTime = this.currentTime + this.sourceStartTime
162
+ }
174
163
  }
175
164
 
176
165
  render () {