pixi-effects 0.7.0 → 0.9.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +8 -7
  3. package/ai/SKILL.md +6 -5
  4. package/ai/reference/cheatsheet.md +9 -1
  5. package/ai/template.html +4 -3
  6. package/ai/tools/check.mjs +8 -0
  7. package/dist/Composition-CM62XJKT.cjs +14 -0
  8. package/dist/{Composition-3KHQPRBA.cjs.map → Composition-CM62XJKT.cjs.map} +1 -1
  9. package/dist/Composition-RJDSQYTV.js +5 -0
  10. package/dist/{Composition-DT657FO5.js.map → Composition-RJDSQYTV.js.map} +1 -1
  11. package/dist/Controller.cjs +55 -20
  12. package/dist/Controller.cjs.map +1 -1
  13. package/dist/Controller.d.cts +31 -2
  14. package/dist/Controller.d.ts +31 -2
  15. package/dist/Controller.js +55 -21
  16. package/dist/Controller.js.map +1 -1
  17. package/dist/{Movie-B0OUKlZI.d.ts → Movie-QMlFyHkf.d.ts} +49 -1
  18. package/dist/{Movie-BK9daiTt.d.cts → Movie-qGHp15c_.d.cts} +49 -1
  19. package/dist/{chunk-6I55T4OH.cjs → chunk-AGMNSJJH.cjs} +13 -4
  20. package/dist/chunk-AGMNSJJH.cjs.map +1 -0
  21. package/dist/{chunk-4O2NYDJG.js → chunk-FNKSXBKB.js} +13 -4
  22. package/dist/chunk-FNKSXBKB.js.map +1 -0
  23. package/dist/{chunk-36Z4RIRK.js → chunk-O4AJYUPK.js} +3 -3
  24. package/dist/{chunk-36Z4RIRK.js.map → chunk-O4AJYUPK.js.map} +1 -1
  25. package/dist/{chunk-4VSIJLU3.cjs → chunk-PDBDDGG5.cjs} +21 -21
  26. package/dist/{chunk-4VSIJLU3.cjs.map → chunk-PDBDDGG5.cjs.map} +1 -1
  27. package/dist/index.cjs +141 -26
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +130 -15
  32. package/dist/index.js.map +1 -1
  33. package/dist/three.cjs +5 -5
  34. package/dist/three.js +1 -1
  35. package/docs/api.md +67 -0
  36. package/docs/dsl.md +14 -2
  37. package/llms-full.txt +96 -8
  38. package/package.json +4 -1
  39. package/dist/Composition-3KHQPRBA.cjs +0 -14
  40. package/dist/Composition-DT657FO5.js +0 -5
  41. package/dist/chunk-4O2NYDJG.js.map +0 -1
  42. package/dist/chunk-6I55T4OH.cjs.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, e as SoundMeasure, T as TimelineData, f as TimelineHtmlOptions, g as TimelineRow, h as TimelineSvgOptions, i as TimelineTransition } from './Movie-BK9daiTt.cjs';
1
+ export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieErrorEvent, e as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, f as SoundMeasure, T as TimelineData, g as TimelineHtmlOptions, h as TimelineRow, i as TimelineSvgOptions, j as TimelineTransition, V as VolumeEvent } from './Movie-qGHp15c_.cjs';
2
2
  import { S as Sequence } from './Base-CSIulyIq.cjs';
3
3
  import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-oJijbn-K.cjs';
4
4
  export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-oJijbn-K.cjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, e as SoundMeasure, T as TimelineData, f as TimelineHtmlOptions, g as TimelineRow, h as TimelineSvgOptions, i as TimelineTransition } from './Movie-B0OUKlZI.js';
1
+ export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieErrorEvent, e as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, f as SoundMeasure, T as TimelineData, g as TimelineHtmlOptions, h as TimelineRow, i as TimelineSvgOptions, j as TimelineTransition, V as VolumeEvent } from './Movie-QMlFyHkf.js';
2
2
  import { S as Sequence } from './Base-DVgwyIUj.js';
3
3
  import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-oJijbn-K.js';
4
4
  export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-oJijbn-K.js';
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { CompositionSequence } from './chunk-36Z4RIRK.js';
2
- import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-4O2NYDJG.js';
3
- export { registerSequenceType } from './chunk-4O2NYDJG.js';
1
+ import { CompositionSequence } from './chunk-O4AJYUPK.js';
2
+ import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-FNKSXBKB.js';
3
+ export { registerSequenceType } from './chunk-FNKSXBKB.js';
4
4
  import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
5
5
  import * as PIXI from 'pixi.js';
6
6
  import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
7
7
  import { gsap } from 'gsap';
8
8
  import { PixiPlugin } from 'gsap/PixiPlugin';
9
- import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat } from 'mediabunny';
9
+ import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, getFirstEncodableAudioCodec } from 'mediabunny';
10
10
 
11
11
  var registered = false;
12
12
  function ensureLoadersRegistered(audioContext) {
@@ -288,6 +288,28 @@ var AUDIO_CODEC_BY_FORMAT = {
288
288
  webm: "opus",
289
289
  mkv: "opus"
290
290
  };
291
+ var AUDIO_PREFERENCE = {
292
+ mp4: ["aac", "opus", "mp3", "flac"],
293
+ mov: ["aac", "mp3"],
294
+ webm: ["opus", "vorbis"],
295
+ mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
296
+ };
297
+ async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
298
+ const supported = container.getSupportedAudioCodecs?.();
299
+ const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
300
+ const canEncode = (list) => getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
301
+ if (requested) {
302
+ if (await canEncode([requested])) return requested;
303
+ const alt = await canEncode(preferred.filter((c) => c !== requested));
304
+ throw new Error(`pixi-effects: this browser cannot encode "${requested}" audio for ${fmt}` + (alt ? `; "${alt}" would work: render({ format: '${fmt}', audio: { codec: '${alt}' } }), or leave audio.codec out and the best one is chosen.` : "; it has no audio encoder for this format at all (try another browser, or a movie without sound)."));
305
+ }
306
+ const chosen = await canEncode(preferred);
307
+ if (!chosen) throw new Error(`pixi-effects: this browser has no audio encoder for ${fmt} (it tried ${preferred.join(", ")}): try another browser, or render a movie without sound.`);
308
+ if (chosen !== preferred[0]) {
309
+ console.warn(`pixi-effects: this browser cannot encode ${preferred[0].toUpperCase()} audio (Chrome on Linux has no AAC encoder), so the ${fmt} carries ${chosen} instead; current players play it. A webm export needs no AAC.`);
310
+ }
311
+ return chosen;
312
+ }
291
313
  var qualityMap = {
292
314
  "very-low": QUALITY_VERY_LOW,
293
315
  "low": QUALITY_LOW,
@@ -320,8 +342,10 @@ async function exportFrames(movie, options = {}) {
320
342
  bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? QUALITY_HIGH
321
343
  }
322
344
  };
345
+ const container = makeOutputFormat(opts.format);
346
+ if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
323
347
  const output = new Output({
324
- format: makeOutputFormat(opts.format),
348
+ format: container,
325
349
  target: new BufferTarget()
326
350
  });
327
351
  const canvasSource = new CanvasSource(movie.app.canvas, {
@@ -1405,6 +1429,21 @@ function startWhenRunning(ctx, start, gesture) {
1405
1429
  }
1406
1430
  var GESTURES = ["pointerdown", "mousedown", "touchend", "keydown"];
1407
1431
 
1432
+ // src/core/poster.ts
1433
+ function normalizePoster(poster, duration, frameRate) {
1434
+ if (poster === void 0 || poster === null) return { seconds: null, frame: null };
1435
+ if (typeof poster !== "number" || !Number.isFinite(poster)) {
1436
+ console.warn(`pixi-effects: movie.init({ poster }): ${JSON.stringify(poster)} is not a time in seconds; ignored`);
1437
+ return { seconds: null, frame: null };
1438
+ }
1439
+ let seconds = poster < 0 ? Math.max(0, duration + poster) : poster;
1440
+ if (seconds > duration) {
1441
+ console.warn(`pixi-effects: movie.init({ poster }): the poster time ${poster} s is past the end of the movie (${duration} s); using the last frame`);
1442
+ seconds = duration;
1443
+ }
1444
+ return { seconds, frame: Math.round(seconds * frameRate) };
1445
+ }
1446
+
1408
1447
  // src/core/Movie.ts
1409
1448
  extensions.add(CullerPlugin);
1410
1449
  gsap.registerPlugin(PixiPlugin);
@@ -1418,6 +1457,13 @@ var Movie = class {
1418
1457
  audioSource = null;
1419
1458
  gainNode = null;
1420
1459
  _cancelAudioStart = null;
1460
+ /** The poster time in seconds (`movie.init({ poster })`), or null. */
1461
+ poster = null;
1462
+ _posterFrame = null;
1463
+ /** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
1464
+ _atPoster = false;
1465
+ /** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
1466
+ _quiet = 0;
1421
1467
  _volume = 1;
1422
1468
  _muted = false;
1423
1469
  isPlaying = false;
@@ -1459,6 +1505,36 @@ var Movie = class {
1459
1505
  get isReady() {
1460
1506
  return this._initState === "ready";
1461
1507
  }
1508
+ /** The poster time as a frame number, or null. */
1509
+ get posterFrame() {
1510
+ return this._posterFrame;
1511
+ }
1512
+ /** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
1513
+ async _showPoster() {
1514
+ if (this._posterFrame === null || !this.timeline) return;
1515
+ const back = this.currentFrame;
1516
+ this.currentFrame = this._posterFrame;
1517
+ try {
1518
+ this.timeline.time(this._posterFrame / this.frameRate);
1519
+ await this._awaitVideoFrames();
1520
+ this._updateSpace();
1521
+ this._renderNow();
1522
+ } finally {
1523
+ this.currentFrame = back;
1524
+ }
1525
+ this._atPoster = true;
1526
+ }
1527
+ async posterImage(opts = {}) {
1528
+ this._requireReady("posterImage");
1529
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
1530
+ try {
1531
+ return await this.snapshot(this._posterFrame ?? 0, opts);
1532
+ } finally {
1533
+ this.currentFrame = back;
1534
+ if (wasAtPoster) await this._showPoster();
1535
+ else await this._quietly(() => this.gotoFrame(back, true));
1536
+ }
1537
+ }
1462
1538
  _ensureAudioContext() {
1463
1539
  if (!this._audioContext) {
1464
1540
  const Ctx = window.AudioContext ?? window.webkitAudioContext;
@@ -1467,7 +1543,7 @@ var Movie = class {
1467
1543
  return this._audioContext;
1468
1544
  }
1469
1545
  async init(options = {}) {
1470
- warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
1546
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster"]);
1471
1547
  this._initState = "pending";
1472
1548
  try {
1473
1549
  this.width = options.width ?? 1920;
@@ -1475,6 +1551,9 @@ var Movie = class {
1475
1551
  this.duration = options.duration ?? 10;
1476
1552
  this.frameRate = options.frameRate ?? 30;
1477
1553
  this.totalFrames = Math.round(this.duration * this.frameRate);
1554
+ const poster = normalizePoster(options.poster, this.duration, this.frameRate);
1555
+ this.poster = poster.seconds;
1556
+ this._posterFrame = poster.frame;
1478
1557
  this.background = options.background ?? "#000000";
1479
1558
  this.timeline = gsap.timeline({ paused: true, defaults: { ease: "none" } });
1480
1559
  this.timeline.add(gsap.to({}, { duration: this.duration }));
@@ -1538,6 +1617,7 @@ var Movie = class {
1538
1617
  this._updateSpace();
1539
1618
  this._renderNow();
1540
1619
  this._initState = "ready";
1620
+ if (this._posterFrame !== null) await this._showPoster();
1541
1621
  this.emit("ready");
1542
1622
  } catch (err) {
1543
1623
  try {
@@ -1545,12 +1625,33 @@ var Movie = class {
1545
1625
  } catch (cleanupErr) {
1546
1626
  console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
1547
1627
  }
1628
+ this._emitError("init", err);
1548
1629
  throw err;
1549
1630
  }
1550
1631
  }
1551
1632
  async gotoFrame(frame, force = false) {
1552
1633
  if (this._initState !== "ready") return;
1553
- if (!force && this.currentFrame === frame) return;
1634
+ if (!force && !this._atPoster && this.currentFrame === frame) return;
1635
+ if (this._quiet > 0) return this._goto(frame);
1636
+ const target = Math.max(0, Math.min(frame, this.totalFrames));
1637
+ this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
1638
+ await this._goto(frame);
1639
+ this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
1640
+ }
1641
+ /** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
1642
+ async _quietly(fn) {
1643
+ this._quiet++;
1644
+ try {
1645
+ return await fn();
1646
+ } finally {
1647
+ this._quiet--;
1648
+ }
1649
+ }
1650
+ _emitError(where, error) {
1651
+ this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
1652
+ }
1653
+ async _goto(frame) {
1654
+ this._atPoster = false;
1554
1655
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1555
1656
  this.timeline.time(this.currentFrame / this.frameRate);
1556
1657
  await this._awaitVideoFrames();
@@ -1572,7 +1673,7 @@ var Movie = class {
1572
1673
  async snapshot(frame = this.currentFrame, opts = {}) {
1573
1674
  warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1574
1675
  this._requireReady("snapshot");
1575
- await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1676
+ await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
1576
1677
  const scale = opts.scale ?? 1;
1577
1678
  const out = document.createElement("canvas");
1578
1679
  out.width = Math.max(1, Math.round(this.width * scale));
@@ -1592,11 +1693,11 @@ var Movie = class {
1592
1693
  const g = sheet.getContext("2d");
1593
1694
  g.fillStyle = "#10131c";
1594
1695
  g.fillRect(0, 0, L.width, L.height);
1595
- const back = this.currentFrame;
1696
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
1596
1697
  try {
1597
1698
  for (let i = 0; i < frames.length; i++) {
1598
1699
  const f = frames[i];
1599
- await this.gotoFrame(f, true);
1700
+ await this._quietly(() => this.gotoFrame(f, true));
1600
1701
  const { x, y } = L.positions[i];
1601
1702
  g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1602
1703
  g.fillStyle = "#e8ecf8";
@@ -1605,7 +1706,8 @@ var Movie = class {
1605
1706
  g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1606
1707
  }
1607
1708
  } finally {
1608
- await this.gotoFrame(back, true);
1709
+ if (wasAtPoster) await this._showPoster();
1710
+ else await this._quietly(() => this.gotoFrame(back, true));
1609
1711
  }
1610
1712
  return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1611
1713
  }
@@ -1688,17 +1790,20 @@ var Movie = class {
1688
1790
  const frame = Math.floor(elapsed * this.frameRate);
1689
1791
  if (frame <= this.totalFrames) {
1690
1792
  inFlight = true;
1691
- this.gotoFrame(frame).catch((err) => {
1793
+ this._goto(frame).catch((err) => {
1692
1794
  console.warn("pixi-effects: gotoFrame failed during playback:", err);
1795
+ this._emitError("playback", err);
1693
1796
  }).finally(() => {
1694
1797
  inFlight = false;
1695
1798
  });
1696
1799
  this._raf = requestAnimationFrame(tick);
1697
1800
  } else {
1698
1801
  this.pause();
1699
- this.gotoFrame(this.totalFrames).catch((err) => {
1802
+ this._goto(this.totalFrames).catch((err) => {
1700
1803
  console.warn("pixi-effects: final gotoFrame failed:", err);
1804
+ this._emitError("playback", err);
1701
1805
  });
1806
+ this.emit("ended");
1702
1807
  }
1703
1808
  };
1704
1809
  this._raf = requestAnimationFrame(tick);
@@ -1742,15 +1847,20 @@ var Movie = class {
1742
1847
  if (wasPlaying) this.emit("pause");
1743
1848
  }
1744
1849
  set volume(v) {
1745
- this._volume = Math.max(0, Math.min(1, v));
1850
+ const next = Math.max(0, Math.min(1, v));
1851
+ const changed = next !== this._volume;
1852
+ this._volume = next;
1746
1853
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1854
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1747
1855
  }
1748
1856
  get volume() {
1749
1857
  return this._volume;
1750
1858
  }
1751
1859
  set muted(v) {
1860
+ const changed = !!v !== this._muted;
1752
1861
  this._muted = !!v;
1753
1862
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1863
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1754
1864
  }
1755
1865
  get muted() {
1756
1866
  return this._muted;
@@ -1761,7 +1871,12 @@ var Movie = class {
1761
1871
  }
1762
1872
  async render(options) {
1763
1873
  warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1764
- return await exportFrames(this, options);
1874
+ try {
1875
+ return await this._quietly(() => exportFrames(this, options));
1876
+ } catch (err) {
1877
+ this._emitError("render", err);
1878
+ throw err;
1879
+ }
1765
1880
  }
1766
1881
  async destroy() {
1767
1882
  safeRun(() => this.pause());