@twick/2d 0.15.7 → 0.15.8

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.js CHANGED
@@ -9903,6 +9903,10 @@ async function getFileInfo(uri) {
9903
9903
  found = true;
9904
9904
  controller.abort();
9905
9905
  const track = info.videoTracks[0];
9906
+ if (!track) {
9907
+ rej("No video track found");
9908
+ return;
9909
+ }
9906
9910
  const config = {
9907
9911
  // Browser doesn't support parsing full vp8 codec (eg: `vp08.00.41.08`),
9908
9912
  // they only support `vp8`.
@@ -9933,25 +9937,35 @@ async function getFileInfo(uri) {
9933
9937
  file.start();
9934
9938
  res({ file, edits, config });
9935
9939
  };
9940
+ file.onError = (e) => rej(e);
9936
9941
  return fetch(uri, { signal: controller.signal }).then(async (response) => {
9942
+ if (!response.ok) {
9943
+ rej(`HTTP ${response.status}: ${response.statusText}`);
9944
+ return;
9945
+ }
9937
9946
  if (!response.body) {
9938
- throw new Error("Response body is null");
9947
+ rej("Response body is null");
9948
+ return;
9939
9949
  }
9940
9950
  const reader = response.body.getReader();
9941
9951
  const sink = new MP4FileSink(file, () => {
9942
9952
  });
9953
+ let bytesRead = 0;
9954
+ let chunks = 0;
9943
9955
  while (!found) {
9944
9956
  await reader.read().then(({ done, value }) => {
9945
9957
  if (done) {
9946
9958
  file.flush();
9947
9959
  controller.abort();
9948
- rej("Could not find moov");
9960
+ if (!found) rej("Could not find moov box in video file");
9949
9961
  return;
9950
9962
  }
9963
+ bytesRead += value.byteLength;
9964
+ chunks++;
9951
9965
  sink.write(value);
9952
9966
  });
9953
9967
  }
9954
- });
9968
+ }).catch((err) => rej(err));
9955
9969
  });
9956
9970
  }
9957
9971
  var Mp4Parser = class {
@@ -9975,9 +9989,7 @@ var Mp4Parser = class {
9975
9989
  const segmentDurationInSeconds = this.getSecondDurationOfSegment(
9976
9990
  this.edits[this.nextSegment]
9977
9991
  );
9978
- if (startTimeWithinSegment < segmentDurationInSeconds) {
9979
- break;
9980
- }
9992
+ if (startTimeWithinSegment < segmentDurationInSeconds) break;
9981
9993
  startTimeWithinSegment -= segmentDurationInSeconds;
9982
9994
  this.nextSegment++;
9983
9995
  }
@@ -10069,8 +10081,94 @@ var Mp4Parser = class {
10069
10081
  }
10070
10082
  };
10071
10083
 
10084
+ // src/lib/utils/video/html-video-extractor.ts
10085
+ var HTMLVideoFrameExtractor = class {
10086
+ constructor(src) {
10087
+ this.src = src;
10088
+ this.lastTime = -1;
10089
+ this.video = document.createElement("video");
10090
+ this.video.crossOrigin = "anonymous";
10091
+ this.video.preload = "auto";
10092
+ this.video.src = src;
10093
+ this.video.muted = true;
10094
+ this.canvas = document.createElement("canvas");
10095
+ this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
10096
+ }
10097
+ async start() {
10098
+ return new Promise((resolve, reject) => {
10099
+ const timeout = setTimeout(() => {
10100
+ reject(new Error("Timeout loading video metadata"));
10101
+ }, 3e4);
10102
+ this.video.addEventListener("loadedmetadata", () => {
10103
+ clearTimeout(timeout);
10104
+ this.canvas.width = this.video.videoWidth;
10105
+ this.canvas.height = this.video.videoHeight;
10106
+ resolve();
10107
+ }, { once: true });
10108
+ this.video.addEventListener("error", () => {
10109
+ clearTimeout(timeout);
10110
+ reject(new Error("Failed to load video"));
10111
+ }, { once: true });
10112
+ });
10113
+ }
10114
+ async getFrameAt(time) {
10115
+ if (Math.abs(this.video.currentTime - time) > 0.016) {
10116
+ await this.seekTo(time);
10117
+ }
10118
+ this.ctx.drawImage(this.video, 0, 0);
10119
+ return createImageBitmap(this.canvas);
10120
+ }
10121
+ async seekTo(time) {
10122
+ return new Promise((resolve, reject) => {
10123
+ const timeout = setTimeout(() => {
10124
+ reject(new Error(`Seek timeout at ${time}s`));
10125
+ }, 5e3);
10126
+ const onSeeked = () => {
10127
+ clearTimeout(timeout);
10128
+ this.video.removeEventListener("seeked", onSeeked);
10129
+ resolve();
10130
+ };
10131
+ this.video.addEventListener("seeked", onSeeked, { once: true });
10132
+ this.video.currentTime = time;
10133
+ });
10134
+ }
10135
+ getDuration() {
10136
+ return this.video.duration || 0;
10137
+ }
10138
+ getTime() {
10139
+ return this.video.currentTime;
10140
+ }
10141
+ getLastTime() {
10142
+ return this.lastTime;
10143
+ }
10144
+ close() {
10145
+ this.video.src = "";
10146
+ this.video.load();
10147
+ }
10148
+ };
10149
+ var htmlVideoExtractors = /* @__PURE__ */ new Map();
10150
+ async function getFrameHTML(id, filePath, time, fps) {
10151
+ const extractorId = filePath + "-" + id;
10152
+ let extractor = htmlVideoExtractors.get(extractorId);
10153
+ if (!extractor) {
10154
+ extractor = new HTMLVideoFrameExtractor(filePath);
10155
+ await extractor.start();
10156
+ htmlVideoExtractors.set(extractorId, extractor);
10157
+ }
10158
+ return extractor.getFrameAt(time);
10159
+ }
10160
+ function dropHTMLExtractor(id, filePath) {
10161
+ const extractorId = filePath + "-" + id;
10162
+ const extractor = htmlVideoExtractors.get(extractorId);
10163
+ if (extractor) {
10164
+ extractor.close();
10165
+ htmlVideoExtractors.delete(extractorId);
10166
+ }
10167
+ }
10168
+
10072
10169
  // src/lib/utils/video/mp4-parser-manager.ts
10073
10170
  var videoFrameExtractors = /* @__PURE__ */ new Map();
10171
+ var htmlFallbackVideos = /* @__PURE__ */ new Set();
10074
10172
  async function dropExtractor(id, filePath) {
10075
10173
  const extractorId = filePath + "-" + id;
10076
10174
  const extractor = videoFrameExtractors.get(extractorId);
@@ -10078,9 +10176,16 @@ async function dropExtractor(id, filePath) {
10078
10176
  extractor.close();
10079
10177
  videoFrameExtractors.delete(extractorId);
10080
10178
  }
10179
+ if (htmlFallbackVideos.has(extractorId)) {
10180
+ dropHTMLExtractor(id, filePath);
10181
+ htmlFallbackVideos.delete(extractorId);
10182
+ }
10081
10183
  }
10082
10184
  async function getFrame(id, filePath, time, fps) {
10083
10185
  const extractorId = filePath + "-" + id;
10186
+ if (htmlFallbackVideos.has(extractorId)) {
10187
+ return getFrameHTML(id, filePath, time, fps);
10188
+ }
10084
10189
  let extractor = videoFrameExtractors.get(extractorId);
10085
10190
  const frameDuration = 1 / fps;
10086
10191
  const duration = extractor?.getDuration();
@@ -10090,9 +10195,7 @@ async function getFrame(id, filePath, time, fps) {
10090
10195
  const isOldFrame = extractor && Math.abs(time - extractor.getLastTime()) < frameDuration / 2;
10091
10196
  if (extractor && isOldFrame) {
10092
10197
  const lastFrame = extractor.getLastFrame();
10093
- if (!lastFrame) {
10094
- throw new Error("No last frame");
10095
- }
10198
+ if (!lastFrame) throw new Error("No last frame");
10096
10199
  return lastFrame;
10097
10200
  }
10098
10201
  if (extractor && time + frameDuration < extractor.getTime()) {
@@ -10107,7 +10210,22 @@ async function getFrame(id, filePath, time, fps) {
10107
10210
  }
10108
10211
  if (!extractor) {
10109
10212
  extractor = new Mp4Parser(filePath, fps, time);
10110
- await extractor.start();
10213
+ try {
10214
+ await extractor.start();
10215
+ } catch {
10216
+ htmlFallbackVideos.add(extractorId);
10217
+ return getFrameHTML(id, filePath, time, fps);
10218
+ }
10219
+ const duration2 = extractor.getDuration();
10220
+ if (duration2 === 0) {
10221
+ extractor.close();
10222
+ videoFrameExtractors.delete(extractorId);
10223
+ htmlFallbackVideos.add(extractorId);
10224
+ return getFrameHTML(id, filePath, time, fps);
10225
+ }
10226
+ if (duration2 > 0 && time > duration2) {
10227
+ throw new Error(`Requested time ${time}s exceeds video duration ${duration2}s`);
10228
+ }
10111
10229
  videoFrameExtractors.set(extractorId, extractor);
10112
10230
  }
10113
10231
  return extractor.getNextFrame();