@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.cjs CHANGED
@@ -9944,6 +9944,10 @@ async function getFileInfo(uri) {
9944
9944
  found = true;
9945
9945
  controller.abort();
9946
9946
  const track = info.videoTracks[0];
9947
+ if (!track) {
9948
+ rej("No video track found");
9949
+ return;
9950
+ }
9947
9951
  const config = {
9948
9952
  // Browser doesn't support parsing full vp8 codec (eg: `vp08.00.41.08`),
9949
9953
  // they only support `vp8`.
@@ -9974,25 +9978,35 @@ async function getFileInfo(uri) {
9974
9978
  file.start();
9975
9979
  res({ file, edits, config });
9976
9980
  };
9981
+ file.onError = (e) => rej(e);
9977
9982
  return fetch(uri, { signal: controller.signal }).then(async (response) => {
9983
+ if (!response.ok) {
9984
+ rej(`HTTP ${response.status}: ${response.statusText}`);
9985
+ return;
9986
+ }
9978
9987
  if (!response.body) {
9979
- throw new Error("Response body is null");
9988
+ rej("Response body is null");
9989
+ return;
9980
9990
  }
9981
9991
  const reader = response.body.getReader();
9982
9992
  const sink = new MP4FileSink(file, () => {
9983
9993
  });
9994
+ let bytesRead = 0;
9995
+ let chunks = 0;
9984
9996
  while (!found) {
9985
9997
  await reader.read().then(({ done, value }) => {
9986
9998
  if (done) {
9987
9999
  file.flush();
9988
10000
  controller.abort();
9989
- rej("Could not find moov");
10001
+ if (!found) rej("Could not find moov box in video file");
9990
10002
  return;
9991
10003
  }
10004
+ bytesRead += value.byteLength;
10005
+ chunks++;
9992
10006
  sink.write(value);
9993
10007
  });
9994
10008
  }
9995
- });
10009
+ }).catch((err) => rej(err));
9996
10010
  });
9997
10011
  }
9998
10012
  var Mp4Parser = class {
@@ -10016,9 +10030,7 @@ var Mp4Parser = class {
10016
10030
  const segmentDurationInSeconds = this.getSecondDurationOfSegment(
10017
10031
  this.edits[this.nextSegment]
10018
10032
  );
10019
- if (startTimeWithinSegment < segmentDurationInSeconds) {
10020
- break;
10021
- }
10033
+ if (startTimeWithinSegment < segmentDurationInSeconds) break;
10022
10034
  startTimeWithinSegment -= segmentDurationInSeconds;
10023
10035
  this.nextSegment++;
10024
10036
  }
@@ -10110,8 +10122,94 @@ var Mp4Parser = class {
10110
10122
  }
10111
10123
  };
10112
10124
 
10125
+ // src/lib/utils/video/html-video-extractor.ts
10126
+ var HTMLVideoFrameExtractor = class {
10127
+ constructor(src) {
10128
+ this.src = src;
10129
+ this.lastTime = -1;
10130
+ this.video = document.createElement("video");
10131
+ this.video.crossOrigin = "anonymous";
10132
+ this.video.preload = "auto";
10133
+ this.video.src = src;
10134
+ this.video.muted = true;
10135
+ this.canvas = document.createElement("canvas");
10136
+ this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
10137
+ }
10138
+ async start() {
10139
+ return new Promise((resolve, reject) => {
10140
+ const timeout = setTimeout(() => {
10141
+ reject(new Error("Timeout loading video metadata"));
10142
+ }, 3e4);
10143
+ this.video.addEventListener("loadedmetadata", () => {
10144
+ clearTimeout(timeout);
10145
+ this.canvas.width = this.video.videoWidth;
10146
+ this.canvas.height = this.video.videoHeight;
10147
+ resolve();
10148
+ }, { once: true });
10149
+ this.video.addEventListener("error", () => {
10150
+ clearTimeout(timeout);
10151
+ reject(new Error("Failed to load video"));
10152
+ }, { once: true });
10153
+ });
10154
+ }
10155
+ async getFrameAt(time) {
10156
+ if (Math.abs(this.video.currentTime - time) > 0.016) {
10157
+ await this.seekTo(time);
10158
+ }
10159
+ this.ctx.drawImage(this.video, 0, 0);
10160
+ return createImageBitmap(this.canvas);
10161
+ }
10162
+ async seekTo(time) {
10163
+ return new Promise((resolve, reject) => {
10164
+ const timeout = setTimeout(() => {
10165
+ reject(new Error(`Seek timeout at ${time}s`));
10166
+ }, 5e3);
10167
+ const onSeeked = () => {
10168
+ clearTimeout(timeout);
10169
+ this.video.removeEventListener("seeked", onSeeked);
10170
+ resolve();
10171
+ };
10172
+ this.video.addEventListener("seeked", onSeeked, { once: true });
10173
+ this.video.currentTime = time;
10174
+ });
10175
+ }
10176
+ getDuration() {
10177
+ return this.video.duration || 0;
10178
+ }
10179
+ getTime() {
10180
+ return this.video.currentTime;
10181
+ }
10182
+ getLastTime() {
10183
+ return this.lastTime;
10184
+ }
10185
+ close() {
10186
+ this.video.src = "";
10187
+ this.video.load();
10188
+ }
10189
+ };
10190
+ var htmlVideoExtractors = /* @__PURE__ */ new Map();
10191
+ async function getFrameHTML(id, filePath, time, fps) {
10192
+ const extractorId = filePath + "-" + id;
10193
+ let extractor = htmlVideoExtractors.get(extractorId);
10194
+ if (!extractor) {
10195
+ extractor = new HTMLVideoFrameExtractor(filePath);
10196
+ await extractor.start();
10197
+ htmlVideoExtractors.set(extractorId, extractor);
10198
+ }
10199
+ return extractor.getFrameAt(time);
10200
+ }
10201
+ function dropHTMLExtractor(id, filePath) {
10202
+ const extractorId = filePath + "-" + id;
10203
+ const extractor = htmlVideoExtractors.get(extractorId);
10204
+ if (extractor) {
10205
+ extractor.close();
10206
+ htmlVideoExtractors.delete(extractorId);
10207
+ }
10208
+ }
10209
+
10113
10210
  // src/lib/utils/video/mp4-parser-manager.ts
10114
10211
  var videoFrameExtractors = /* @__PURE__ */ new Map();
10212
+ var htmlFallbackVideos = /* @__PURE__ */ new Set();
10115
10213
  async function dropExtractor(id, filePath) {
10116
10214
  const extractorId = filePath + "-" + id;
10117
10215
  const extractor = videoFrameExtractors.get(extractorId);
@@ -10119,9 +10217,16 @@ async function dropExtractor(id, filePath) {
10119
10217
  extractor.close();
10120
10218
  videoFrameExtractors.delete(extractorId);
10121
10219
  }
10220
+ if (htmlFallbackVideos.has(extractorId)) {
10221
+ dropHTMLExtractor(id, filePath);
10222
+ htmlFallbackVideos.delete(extractorId);
10223
+ }
10122
10224
  }
10123
10225
  async function getFrame(id, filePath, time, fps) {
10124
10226
  const extractorId = filePath + "-" + id;
10227
+ if (htmlFallbackVideos.has(extractorId)) {
10228
+ return getFrameHTML(id, filePath, time, fps);
10229
+ }
10125
10230
  let extractor = videoFrameExtractors.get(extractorId);
10126
10231
  const frameDuration = 1 / fps;
10127
10232
  const duration = extractor?.getDuration();
@@ -10131,9 +10236,7 @@ async function getFrame(id, filePath, time, fps) {
10131
10236
  const isOldFrame = extractor && Math.abs(time - extractor.getLastTime()) < frameDuration / 2;
10132
10237
  if (extractor && isOldFrame) {
10133
10238
  const lastFrame = extractor.getLastFrame();
10134
- if (!lastFrame) {
10135
- throw new Error("No last frame");
10136
- }
10239
+ if (!lastFrame) throw new Error("No last frame");
10137
10240
  return lastFrame;
10138
10241
  }
10139
10242
  if (extractor && time + frameDuration < extractor.getTime()) {
@@ -10148,7 +10251,22 @@ async function getFrame(id, filePath, time, fps) {
10148
10251
  }
10149
10252
  if (!extractor) {
10150
10253
  extractor = new Mp4Parser(filePath, fps, time);
10151
- await extractor.start();
10254
+ try {
10255
+ await extractor.start();
10256
+ } catch {
10257
+ htmlFallbackVideos.add(extractorId);
10258
+ return getFrameHTML(id, filePath, time, fps);
10259
+ }
10260
+ const duration2 = extractor.getDuration();
10261
+ if (duration2 === 0) {
10262
+ extractor.close();
10263
+ videoFrameExtractors.delete(extractorId);
10264
+ htmlFallbackVideos.add(extractorId);
10265
+ return getFrameHTML(id, filePath, time, fps);
10266
+ }
10267
+ if (duration2 > 0 && time > duration2) {
10268
+ throw new Error(`Requested time ${time}s exceeds video duration ${duration2}s`);
10269
+ }
10152
10270
  videoFrameExtractors.set(extractorId, extractor);
10153
10271
  }
10154
10272
  return extractor.getNextFrame();