@twick/2d 0.15.1 → 0.15.2

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/index.d.cts CHANGED
@@ -4153,13 +4153,29 @@ declare class Video extends Media {
4153
4153
  readonly decoder: SimpleSignal<'web' | 'ffmpeg' | 'slow' | null, this>;
4154
4154
  detectedFileType: 'mp4' | 'webm' | 'hls' | 'mov' | 'unknown';
4155
4155
  private fileTypeWasDetected;
4156
+ /**
4157
+ * Static pool of video elements cached by source URL.
4158
+ * Multiple Video components with the same src will share the same HTMLVideoElement
4159
+ * to avoid duplicate network requests and improve performance.
4160
+ */
4156
4161
  private static readonly pool;
4157
4162
  private static readonly imageCommunication;
4158
4163
  constructor(props: VideoProps);
4164
+ /**
4165
+ * Creates a video element with CORS fallback handling.
4166
+ * First tries with crossOrigin='anonymous', and if that fails due to CORS,
4167
+ * falls back to creating a video without crossOrigin.
4168
+ */
4169
+ private createVideoElement;
4159
4170
  protected desiredSize(): SerializedVector2<DesiredLength>;
4160
4171
  protected mediaElement(): HTMLVideoElement;
4161
4172
  protected seekedMedia(): HTMLVideoElement;
4162
4173
  protected fastSeekedMedia(): HTMLVideoElement;
4174
+ /**
4175
+ * Generates a normalized cache key based on the video source URL.
4176
+ * This ensures that all Video elements with the same src share the same HTMLVideoElement.
4177
+ */
4178
+ private getCacheKey;
4163
4179
  private video;
4164
4180
  protected seekedVideo(): HTMLVideoElement;
4165
4181
  protected fastSeekedVideo(): HTMLVideoElement;
package/dist/index.d.ts CHANGED
@@ -4153,13 +4153,29 @@ declare class Video extends Media {
4153
4153
  readonly decoder: SimpleSignal<'web' | 'ffmpeg' | 'slow' | null, this>;
4154
4154
  detectedFileType: 'mp4' | 'webm' | 'hls' | 'mov' | 'unknown';
4155
4155
  private fileTypeWasDetected;
4156
+ /**
4157
+ * Static pool of video elements cached by source URL.
4158
+ * Multiple Video components with the same src will share the same HTMLVideoElement
4159
+ * to avoid duplicate network requests and improve performance.
4160
+ */
4156
4161
  private static readonly pool;
4157
4162
  private static readonly imageCommunication;
4158
4163
  constructor(props: VideoProps);
4164
+ /**
4165
+ * Creates a video element with CORS fallback handling.
4166
+ * First tries with crossOrigin='anonymous', and if that fails due to CORS,
4167
+ * falls back to creating a video without crossOrigin.
4168
+ */
4169
+ private createVideoElement;
4159
4170
  protected desiredSize(): SerializedVector2<DesiredLength>;
4160
4171
  protected mediaElement(): HTMLVideoElement;
4161
4172
  protected seekedMedia(): HTMLVideoElement;
4162
4173
  protected fastSeekedMedia(): HTMLVideoElement;
4174
+ /**
4175
+ * Generates a normalized cache key based on the video source URL.
4176
+ * This ensures that all Video elements with the same src share the same HTMLVideoElement.
4177
+ */
4178
+ private getCacheKey;
4163
4179
  private video;
4164
4180
  protected seekedVideo(): HTMLVideoElement;
4165
4181
  protected fastSeekedVideo(): HTMLVideoElement;
package/dist/index.js CHANGED
@@ -5574,8 +5574,8 @@ var Media = class extends Rect {
5574
5574
  const onError = () => {
5575
5575
  const reason = this.getErrorReason(media.error?.code);
5576
5576
  const srcValue = this.src();
5577
- console.log(`ERROR: Error loading video: src="${srcValue}", ${reason}`);
5578
- console.log(`Media element src: "${media.src}"`);
5577
+ console.error(`Error loading video: src="${srcValue}", ${reason}`);
5578
+ console.error(`Media element src: "${media.src}"`);
5579
5579
  media.removeEventListener("error", onError);
5580
5580
  media.removeEventListener("canplay", onCanPlayWrapper);
5581
5581
  media.removeEventListener("canplaythrough", onCanPlayWrapper);
@@ -5594,7 +5594,6 @@ var Media = class extends Rect {
5594
5594
  return this.awaitCanPlay() || this.view().playbackState() === PlaybackState.Rendering;
5595
5595
  }
5596
5596
  play() {
5597
- console.log("=== Media.play() called ===");
5598
5597
  this.playing(true);
5599
5598
  this.schedulePlay();
5600
5599
  }
@@ -5646,12 +5645,14 @@ var Media = class extends Rect {
5646
5645
  const playPromise = media.play();
5647
5646
  if (playPromise !== void 0) {
5648
5647
  playPromise.then(() => {
5649
- console.log("Simple play started successfully");
5650
5648
  }).catch((error) => {
5651
5649
  if (error.name !== "AbortError") {
5652
5650
  console.warn("Error in simple play:", error);
5653
5651
  }
5654
- this.playing(false);
5652
+ const playbackState = this.view().playbackState();
5653
+ if (playbackState !== PlaybackState.Rendering) {
5654
+ this.playing(false);
5655
+ }
5655
5656
  });
5656
5657
  }
5657
5658
  }
@@ -5661,33 +5662,24 @@ var Media = class extends Rect {
5661
5662
  }, 10);
5662
5663
  }
5663
5664
  actuallyPlay(media, timeFunction) {
5664
- console.log("=== actuallyPlay called ===");
5665
- console.log("Media element:", media);
5666
- console.log("Media src:", media.src);
5667
- console.log("Media paused:", media.paused);
5668
- console.log("Media readyState:", media.readyState);
5669
5665
  if (!this.playing()) {
5670
- console.log("Playing state is false, aborting actuallyPlay");
5671
5666
  return;
5672
5667
  }
5673
5668
  media.playbackRate = this.playbackRate();
5674
5669
  if (media.paused) {
5675
- console.log("Media is paused, calling play()");
5676
5670
  const playPromise = media.play();
5677
5671
  if (playPromise !== void 0) {
5678
5672
  playPromise.then(() => {
5679
- console.log("Media play() promise resolved - should be playing now");
5680
- console.log("Post-play media paused:", media.paused);
5681
- console.log("Post-play media currentTime:", media.currentTime);
5682
5673
  }).catch((error) => {
5683
5674
  if (error.name !== "AbortError") {
5684
5675
  console.warn("Error playing media:", error);
5685
5676
  }
5686
- this.playing(false);
5677
+ const playbackState = this.view().playbackState();
5678
+ if (playbackState !== PlaybackState.Rendering) {
5679
+ this.playing(false);
5680
+ }
5687
5681
  });
5688
5682
  }
5689
- } else {
5690
- console.log("Media is already playing");
5691
5683
  }
5692
5684
  const start = timeFunction();
5693
5685
  const offset = media.currentTime;
@@ -5721,11 +5713,13 @@ var Media = class extends Rect {
5721
5713
  }
5722
5714
  autoPlayBasedOnTwick() {
5723
5715
  const playbackState = this.view().playbackState();
5724
- if ((playbackState === PlaybackState.Playing || playbackState === PlaybackState.Presenting) && !this.playing()) {
5725
- console.log("Auto-starting media playback via play() method");
5716
+ const shouldBePlaying = playbackState === PlaybackState.Playing || playbackState === PlaybackState.Presenting || playbackState === PlaybackState.Rendering;
5717
+ if (shouldBePlaying && !this.playing()) {
5718
+ if (playbackState === PlaybackState.Rendering) {
5719
+ this.playing(true);
5720
+ }
5726
5721
  this.play();
5727
- } else if (playbackState === PlaybackState.Paused && this.playing()) {
5728
- console.log("Auto-pausing media playback via pause() method");
5722
+ } else if (!shouldBePlaying && this.playing()) {
5729
5723
  this.pause();
5730
5724
  }
5731
5725
  }
@@ -10127,6 +10121,57 @@ var Video = class extends Media {
10127
10121
  this.fileTypeWasDetected = false;
10128
10122
  this.lastFrame = null;
10129
10123
  }
10124
+ /**
10125
+ * Creates a video element with CORS fallback handling.
10126
+ * First tries with crossOrigin='anonymous', and if that fails due to CORS,
10127
+ * falls back to creating a video without crossOrigin.
10128
+ */
10129
+ createVideoElement(src, key) {
10130
+ const video = document.createElement("video");
10131
+ video.crossOrigin = "anonymous";
10132
+ if (src && src !== "undefined") {
10133
+ try {
10134
+ const parsedSrc = new URL(src, window.location.origin);
10135
+ if (parsedSrc.pathname.endsWith(".m3u8")) {
10136
+ const hls = new Hls();
10137
+ hls.loadSource(src);
10138
+ hls.attachMedia(video);
10139
+ } else {
10140
+ video.src = src;
10141
+ }
10142
+ } catch (error) {
10143
+ video.src = src;
10144
+ }
10145
+ const errorHandler = () => {
10146
+ const error = video.error;
10147
+ if (error && (error.code === 4 || error.code === 2)) {
10148
+ video.removeEventListener("error", errorHandler);
10149
+ const fallbackKey = `${key}_no_cors`;
10150
+ let fallbackVideo = Video.pool[fallbackKey];
10151
+ if (!fallbackVideo) {
10152
+ fallbackVideo = document.createElement("video");
10153
+ fallbackVideo.crossOrigin = null;
10154
+ try {
10155
+ const parsedSrc = new URL(src, window.location.origin);
10156
+ if (parsedSrc.pathname.endsWith(".m3u8")) {
10157
+ const hls = new Hls();
10158
+ hls.loadSource(src);
10159
+ hls.attachMedia(fallbackVideo);
10160
+ } else {
10161
+ fallbackVideo.src = src;
10162
+ }
10163
+ } catch (err) {
10164
+ fallbackVideo.src = src;
10165
+ }
10166
+ Video.pool[fallbackKey] = fallbackVideo;
10167
+ }
10168
+ Video.pool[key] = fallbackVideo;
10169
+ }
10170
+ };
10171
+ video.addEventListener("error", errorHandler, { once: true });
10172
+ }
10173
+ return video;
10174
+ }
10130
10175
  desiredSize() {
10131
10176
  const custom = super.desiredSize();
10132
10177
  if (custom.x === null && custom.y === null) {
@@ -10147,44 +10192,55 @@ var Video = class extends Media {
10147
10192
  fastSeekedMedia() {
10148
10193
  return this.fastSeekedVideo();
10149
10194
  }
10195
+ /**
10196
+ * Generates a normalized cache key based on the video source URL.
10197
+ * This ensures that all Video elements with the same src share the same HTMLVideoElement.
10198
+ */
10199
+ getCacheKey(src) {
10200
+ if (!src || src === "undefined") {
10201
+ return `${this.key}/pending`;
10202
+ }
10203
+ try {
10204
+ const url = new URL(src, window.location.origin);
10205
+ return url.href.split("#")[0];
10206
+ } catch {
10207
+ return src;
10208
+ }
10209
+ }
10150
10210
  video() {
10151
10211
  const src = this.src();
10152
- const key = `${this.key}/${src || "pending"}`;
10212
+ const key = this.getCacheKey(src);
10153
10213
  let video = Video.pool[key];
10154
10214
  if (!video) {
10155
- video = document.createElement("video");
10156
- video.crossOrigin = "anonymous";
10215
+ video = this.createVideoElement(src, key);
10216
+ Video.pool[key] = video;
10217
+ } else {
10157
10218
  if (src && src !== "undefined") {
10158
- try {
10159
- const parsedSrc = new URL(src, window.location.origin);
10160
- if (parsedSrc.pathname.endsWith(".m3u8")) {
10161
- const hls = new Hls();
10162
- hls.loadSource(src);
10163
- hls.attachMedia(video);
10164
- } else {
10219
+ const normalizeUrl = (url) => {
10220
+ try {
10221
+ const parsed = new URL(url, window.location.origin);
10222
+ return parsed.href.split("#")[0];
10223
+ } catch {
10224
+ return url;
10225
+ }
10226
+ };
10227
+ const normalizedSrc = normalizeUrl(src);
10228
+ const normalizedVideoSrc = video.src ? normalizeUrl(video.src) : "";
10229
+ if (normalizedVideoSrc !== normalizedSrc) {
10230
+ try {
10231
+ const parsedSrc = new URL(src, window.location.origin);
10232
+ if (parsedSrc.pathname.endsWith(".m3u8")) {
10233
+ const hls = new Hls();
10234
+ hls.loadSource(src);
10235
+ hls.attachMedia(video);
10236
+ } else {
10237
+ video.src = src;
10238
+ }
10239
+ } catch (error) {
10165
10240
  video.src = src;
10166
10241
  }
10167
- } catch (error) {
10168
- video.src = src;
10169
10242
  }
10170
10243
  }
10171
- Video.pool[key] = video;
10172
- } else if (src && src !== "undefined" && video.src !== src) {
10173
- try {
10174
- const parsedSrc = new URL(src, window.location.origin);
10175
- if (parsedSrc.pathname.endsWith(".m3u8")) {
10176
- const hls = new Hls();
10177
- hls.loadSource(src);
10178
- hls.attachMedia(video);
10179
- } else {
10180
- video.src = src;
10181
- }
10182
- } catch (error) {
10183
- video.src = src;
10184
- }
10185
- delete Video.pool[key];
10186
- const newKey = `${this.key}/${src}`;
10187
- Video.pool[newKey] = video;
10188
10244
  }
10189
10245
  if (!src || src === "undefined") {
10190
10246
  DependencyContext8.collectPromise(
@@ -10403,6 +10459,11 @@ var Video = class extends Media {
10403
10459
  );
10404
10460
  }
10405
10461
  };
10462
+ /**
10463
+ * Static pool of video elements cached by source URL.
10464
+ * Multiple Video components with the same src will share the same HTMLVideoElement
10465
+ * to avoid duplicate network requests and improve performance.
10466
+ */
10406
10467
  Video.pool = {};
10407
10468
  Video.imageCommunication = !import.meta.hot ? null : new ImageCommunication();
10408
10469
  __decorateClass([
@@ -10453,6 +10514,7 @@ function jsx(type, config, key) {
10453
10514
  // src/lib/scenes/Scene2D.ts
10454
10515
  import {
10455
10516
  GeneratorScene,
10517
+ PlaybackState as PlaybackState5,
10456
10518
  SceneRenderEvent,
10457
10519
  Vector2 as Vector224,
10458
10520
  transformVectorAsPoint as transformVectorAsPoint10,
@@ -10570,8 +10632,26 @@ var Scene2D = class extends GeneratorScene {
10570
10632
  }
10571
10633
  }
10572
10634
  getMediaAssets() {
10573
- const playingVideos = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Video).filter((video) => video.isPlaying());
10574
- const playingAudios = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Audio).filter((audio) => audio.isPlaying());
10635
+ const playbackState = this.playback.state;
10636
+ const isRendering = playbackState === PlaybackState5.Rendering;
10637
+ const allVideos = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Video);
10638
+ const allAudios = Array.from(this.registeredNodes.values()).filter((node) => node instanceof Audio);
10639
+ if (isRendering) {
10640
+ allVideos.forEach((video) => {
10641
+ const src = video.src();
10642
+ if (src && src !== "undefined" && !video.isPlaying()) {
10643
+ video.playing(true);
10644
+ }
10645
+ });
10646
+ allAudios.forEach((audio) => {
10647
+ const src = audio.src();
10648
+ if (src && src !== "undefined" && !audio.isPlaying()) {
10649
+ audio.playing(true);
10650
+ }
10651
+ });
10652
+ }
10653
+ const playingVideos = allVideos.filter((video) => video.isPlaying());
10654
+ const playingAudios = allAudios.filter((audio) => audio.isPlaying());
10575
10655
  const returnObjects = [];
10576
10656
  returnObjects.push(
10577
10657
  ...playingVideos.map((vid) => ({