minerender 2.1.0-alpha.85 → 2.1.0-alpha.86

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/bundle.js CHANGED
@@ -106580,6 +106580,7 @@ ${shader.fragmentShader}`.replace(
106580
106580
  Ticker: () => Ticker,
106581
106581
  TintColors: () => TintColors,
106582
106582
  UVMapper: () => UVMapper,
106583
+ VideoExporter: () => VideoExporter,
106583
106584
  WrappedImage: () => WrappedImage,
106584
106585
  addBox3WireframeToObject: () => addBox3WireframeToObject,
106585
106586
  addWireframeToMesh: () => addWireframeToMesh,
@@ -110099,6 +110100,134 @@ property list uchar int vertex_index
110099
110100
  return new Texture().copy(copySource);
110100
110101
  }
110101
110102
 
110103
+ // src/export/VideoExporter.ts
110104
+ init_dirname();
110105
+ init_buffer2();
110106
+ init_process2();
110107
+ var VideoExporter = class {
110108
+ constructor(source, options) {
110109
+ this.source = source;
110110
+ this.chunks = [];
110111
+ this.settled = false;
110112
+ this.stopping = false;
110113
+ this.onStart = () => {
110114
+ this.timer = setTimeout(() => {
110115
+ this.stopping = true;
110116
+ try {
110117
+ this.recorder.stop();
110118
+ } catch (error2) {
110119
+ this.cancel(error2);
110120
+ }
110121
+ }, this.duration * 1e3);
110122
+ };
110123
+ this.onData = (event) => {
110124
+ if (event.data.size) this.chunks.push(event.data);
110125
+ };
110126
+ this.onStop = () => {
110127
+ if (!this.stopping) return this.cancel(new Error("Video recording stopped before its duration elapsed"));
110128
+ if (!this.chunks.length) return this.cancel(new Error("The browser produced no video data"));
110129
+ const mimeType = this.chunks.find((chunk) => chunk.type)?.type || this.recorder.mimeType;
110130
+ const blob = new Blob(this.chunks, { type: mimeType });
110131
+ this.settled = true;
110132
+ this.cleanup();
110133
+ this.resolve(blob);
110134
+ };
110135
+ this.onError = (event) => {
110136
+ this.cancel(event.error ?? new Error("Video recording failed"));
110137
+ };
110138
+ this.onAbort = () => {
110139
+ this.cancel(this.signal.reason);
110140
+ };
110141
+ this.onTrackEnded = () => {
110142
+ this.cancel(new Error("The video capture stream stopped producing frames"));
110143
+ };
110144
+ const fps = options.fps ?? 30;
110145
+ if (!Number.isFinite(options.duration) || options.duration <= 0 || options.duration * 1e3 > 2147483647) {
110146
+ throw new RangeError("Video duration must be greater than zero and at most 2147483.647 seconds");
110147
+ }
110148
+ if (!Number.isFinite(fps) || fps <= 0) throw new RangeError("Video fps must be a finite positive number");
110149
+ const bitrate = options.videoBitsPerSecond;
110150
+ if (bitrate !== void 0 && (!Number.isInteger(bitrate) || bitrate <= 0 || bitrate > 4294967295)) {
110151
+ throw new RangeError("Video bitrate must be a positive unsigned 32-bit integer");
110152
+ }
110153
+ if (!source.width || !source.height) throw new Error("Cannot export video from an empty canvas");
110154
+ options.signal?.throwIfAborted();
110155
+ if (typeof document === "undefined" || typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
110156
+ throw new Error("Video export requires a browser with MediaRecorder support");
110157
+ }
110158
+ const mimeTypes2 = options.mimeType === void 0 ? ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm", "video/mp4"] : [options.mimeType];
110159
+ const mimeType = mimeTypes2.find((type) => type.startsWith("video/") && MediaRecorder.isTypeSupported(type));
110160
+ if (!mimeType) throw new Error("The browser does not support the requested video format");
110161
+ this.canvas = document.createElement("canvas");
110162
+ this.canvas.width = source.width;
110163
+ this.canvas.height = source.height;
110164
+ if (typeof this.canvas.captureStream !== "function") throw new Error("Video export requires canvas captureStream support");
110165
+ const context = this.canvas.getContext("2d", { alpha: false });
110166
+ if (!context) throw new Error("Could not create a canvas for video export");
110167
+ this.context = context;
110168
+ this.signal = options.signal;
110169
+ this.duration = options.duration;
110170
+ this.result = new Promise((resolve, reject) => {
110171
+ this.resolve = resolve;
110172
+ this.reject = reject;
110173
+ });
110174
+ try {
110175
+ this.stream = this.canvas.captureStream(fps);
110176
+ this.recorder = new MediaRecorder(this.stream, { mimeType, videoBitsPerSecond: bitrate });
110177
+ this.recorder.addEventListener("start", this.onStart);
110178
+ this.recorder.addEventListener("dataavailable", this.onData);
110179
+ this.recorder.addEventListener("stop", this.onStop);
110180
+ this.recorder.addEventListener("error", this.onError);
110181
+ for (const track of this.stream.getTracks()) {
110182
+ track.addEventListener("ended", this.onTrackEnded);
110183
+ }
110184
+ this.signal?.addEventListener("abort", this.onAbort, { once: true });
110185
+ this.recorder.start();
110186
+ } catch (error2) {
110187
+ this.cancel(error2);
110188
+ }
110189
+ }
110190
+ /** Copies a freshly drawn source frame before WebGL can clear its drawing buffer. */
110191
+ capture() {
110192
+ if (this.settled || this.stopping) return;
110193
+ try {
110194
+ this.context.fillStyle = "#000000";
110195
+ this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
110196
+ this.context.drawImage(this.source, 0, 0, this.canvas.width, this.canvas.height);
110197
+ } catch (error2) {
110198
+ this.cancel(error2);
110199
+ }
110200
+ }
110201
+ /** Cancels recording, releases its capture stream, and rejects {@link result}. */
110202
+ cancel(reason = new DOMException("Video export cancelled", "AbortError")) {
110203
+ if (this.settled) return;
110204
+ this.settled = true;
110205
+ this.cleanup();
110206
+ this.reject(reason);
110207
+ }
110208
+ cleanup() {
110209
+ if (this.timer !== void 0) clearTimeout(this.timer);
110210
+ this.signal?.removeEventListener("abort", this.onAbort);
110211
+ if (this.recorder) {
110212
+ this.recorder.removeEventListener("start", this.onStart);
110213
+ this.recorder.removeEventListener("dataavailable", this.onData);
110214
+ this.recorder.removeEventListener("stop", this.onStop);
110215
+ this.recorder.removeEventListener("error", this.onError);
110216
+ if (this.recorder.state !== "inactive") {
110217
+ try {
110218
+ this.recorder.stop();
110219
+ } catch {
110220
+ }
110221
+ }
110222
+ }
110223
+ for (const track of this.stream?.getTracks() ?? []) {
110224
+ track.removeEventListener("ended", this.onTrackEnded);
110225
+ track.stop();
110226
+ }
110227
+ this.chunks.length = 0;
110228
+ }
110229
+ };
110230
+
110102
110231
  // src/gui/index.ts
110103
110232
  init_dirname();
110104
110233
  init_buffer2();
@@ -117544,6 +117673,9 @@ gl_FragDepth=readDepth(vUv);
117544
117673
  this._inAnimationLoop = true;
117545
117674
  try {
117546
117675
  this.animate(time);
117676
+ } catch (error2) {
117677
+ this._videoExporter?.cancel(error2);
117678
+ throw error2;
117547
117679
  } finally {
117548
117680
  this._inAnimationLoop = false;
117549
117681
  }
@@ -117738,16 +117870,18 @@ gl_FragDepth=readDepth(vUv);
117738
117870
  if (this._frameCallbacks.get(callback) === subscription) this._frameCallbacks.delete(callback);
117739
117871
  };
117740
117872
  }
117741
- /** Starts or resumes the animation loop and registered frame callbacks. */
117873
+ /** Starts or resumes the animation loop and registered frame callbacks. Has no effect while running. */
117742
117874
  start() {
117743
- if (this._disposed) return;
117875
+ if (this._disposed || this._running) return;
117744
117876
  this.stop();
117745
117877
  this._running = true;
117746
117878
  if (!this._inAnimationLoop) this.renderer.setAnimationLoop(this._animationLoop);
117747
117879
  }
117748
- /** Pauses rendering and frame callbacks. Call {@link start} to resume. */
117880
+ /** Pauses rendering and frame callbacks, and cancels video export. Call {@link start} to resume. */
117749
117881
  stop() {
117750
117882
  if (this._disposed) return;
117883
+ this._videoExporter?.cancel();
117884
+ this._videoExporter = void 0;
117751
117885
  this._running = false;
117752
117886
  if (this._inAnimationLoop) {
117753
117887
  queueMicrotask(() => {
@@ -117809,7 +117943,7 @@ gl_FragDepth=readDepth(vUv);
117809
117943
  this._controls.update();
117810
117944
  }
117811
117945
  if (!this._running) return;
117812
- if (!this.dirty && !this.options.render.renderAlways && !this._frameCallbacks.size) return;
117946
+ if (!this.dirty && !this.options.render.renderAlways && !this._frameCallbacks.size && !this._videoExporter) return;
117813
117947
  const interval = this._frameInterval;
117814
117948
  if (interval) {
117815
117949
  const next = this._nextFrameTime;
@@ -117834,6 +117968,7 @@ gl_FragDepth=readDepth(vUv);
117834
117968
  } else {
117835
117969
  this.renderer.render(this.scene, this.camera);
117836
117970
  }
117971
+ this._videoExporter?.capture();
117837
117972
  this.dirty = false;
117838
117973
  if (this._stats) {
117839
117974
  this._stats.end();
@@ -117854,6 +117989,35 @@ gl_FragDepth=readDepth(vUv);
117854
117989
  const canvas = trim ? trimCanvas(this.renderer.domElement) : this.renderer.domElement;
117855
117990
  return canvas.toDataURL(mime, quality);
117856
117991
  }
117992
+ /**
117993
+ * Records a silent browser video in real time, including frame callbacks and animated textures.
117994
+ * Keep the tab visible; browser scheduling and `render.fpsLimit` limit the capture rate.
117995
+ * The video keeps the initial drawing-buffer size and fills transparent pixels with black.
117996
+ * Temporarily starts a stopped renderer. Stop, disposal, or `options.signal` cancels the export.
117997
+ * Only one export can run at a time. Use the returned Blob's type to select a file extension.
117998
+ */
117999
+ async toVideo(options) {
118000
+ if (this._disposed) throw new Error("Cannot export a video from a disposed renderer");
118001
+ if (this._videoExporter) throw new Error("A video export is already running");
118002
+ const exporter = new VideoExporter(this.renderer.domElement, options);
118003
+ const wasRunning = this._running;
118004
+ try {
118005
+ try {
118006
+ if (!wasRunning) this.start();
118007
+ this._videoExporter = exporter;
118008
+ if (this._controls?.enabled) this._controls.update();
118009
+ this.drawFrame();
118010
+ } catch (error2) {
118011
+ exporter.cancel(error2);
118012
+ }
118013
+ return await exporter.result;
118014
+ } finally {
118015
+ if (this._videoExporter === exporter) {
118016
+ this._videoExporter = void 0;
118017
+ if (!wasRunning) this.stop();
118018
+ }
118019
+ }
118020
+ }
117857
118021
  /** Exports visible scene meshes as OBJ text, without texture images. */
117858
118022
  toObj() {
117859
118023
  return SceneExporter.toObj(this.scene);