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.
@@ -22024,6 +22024,131 @@ function copyDataTexture(source) {
22024
22024
  return new Texture3().copy(copySource);
22025
22025
  }
22026
22026
 
22027
+ // src/export/VideoExporter.ts
22028
+ var VideoExporter = class {
22029
+ constructor(source, options) {
22030
+ this.source = source;
22031
+ this.chunks = [];
22032
+ this.settled = false;
22033
+ this.stopping = false;
22034
+ this.onStart = () => {
22035
+ this.timer = setTimeout(() => {
22036
+ this.stopping = true;
22037
+ try {
22038
+ this.recorder.stop();
22039
+ } catch (error) {
22040
+ this.cancel(error);
22041
+ }
22042
+ }, this.duration * 1e3);
22043
+ };
22044
+ this.onData = (event) => {
22045
+ if (event.data.size) this.chunks.push(event.data);
22046
+ };
22047
+ this.onStop = () => {
22048
+ if (!this.stopping) return this.cancel(new Error("Video recording stopped before its duration elapsed"));
22049
+ if (!this.chunks.length) return this.cancel(new Error("The browser produced no video data"));
22050
+ const mimeType = this.chunks.find((chunk) => chunk.type)?.type || this.recorder.mimeType;
22051
+ const blob = new Blob(this.chunks, { type: mimeType });
22052
+ this.settled = true;
22053
+ this.cleanup();
22054
+ this.resolve(blob);
22055
+ };
22056
+ this.onError = (event) => {
22057
+ this.cancel(event.error ?? new Error("Video recording failed"));
22058
+ };
22059
+ this.onAbort = () => {
22060
+ this.cancel(this.signal.reason);
22061
+ };
22062
+ this.onTrackEnded = () => {
22063
+ this.cancel(new Error("The video capture stream stopped producing frames"));
22064
+ };
22065
+ const fps = options.fps ?? 30;
22066
+ if (!Number.isFinite(options.duration) || options.duration <= 0 || options.duration * 1e3 > 2147483647) {
22067
+ throw new RangeError("Video duration must be greater than zero and at most 2147483.647 seconds");
22068
+ }
22069
+ if (!Number.isFinite(fps) || fps <= 0) throw new RangeError("Video fps must be a finite positive number");
22070
+ const bitrate = options.videoBitsPerSecond;
22071
+ if (bitrate !== void 0 && (!Number.isInteger(bitrate) || bitrate <= 0 || bitrate > 4294967295)) {
22072
+ throw new RangeError("Video bitrate must be a positive unsigned 32-bit integer");
22073
+ }
22074
+ if (!source.width || !source.height) throw new Error("Cannot export video from an empty canvas");
22075
+ options.signal?.throwIfAborted();
22076
+ if (typeof document === "undefined" || typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
22077
+ throw new Error("Video export requires a browser with MediaRecorder support");
22078
+ }
22079
+ const mimeTypes = options.mimeType === void 0 ? ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm", "video/mp4"] : [options.mimeType];
22080
+ const mimeType = mimeTypes.find((type) => type.startsWith("video/") && MediaRecorder.isTypeSupported(type));
22081
+ if (!mimeType) throw new Error("The browser does not support the requested video format");
22082
+ this.canvas = document.createElement("canvas");
22083
+ this.canvas.width = source.width;
22084
+ this.canvas.height = source.height;
22085
+ if (typeof this.canvas.captureStream !== "function") throw new Error("Video export requires canvas captureStream support");
22086
+ const context = this.canvas.getContext("2d", { alpha: false });
22087
+ if (!context) throw new Error("Could not create a canvas for video export");
22088
+ this.context = context;
22089
+ this.signal = options.signal;
22090
+ this.duration = options.duration;
22091
+ this.result = new Promise((resolve, reject) => {
22092
+ this.resolve = resolve;
22093
+ this.reject = reject;
22094
+ });
22095
+ try {
22096
+ this.stream = this.canvas.captureStream(fps);
22097
+ this.recorder = new MediaRecorder(this.stream, { mimeType, videoBitsPerSecond: bitrate });
22098
+ this.recorder.addEventListener("start", this.onStart);
22099
+ this.recorder.addEventListener("dataavailable", this.onData);
22100
+ this.recorder.addEventListener("stop", this.onStop);
22101
+ this.recorder.addEventListener("error", this.onError);
22102
+ for (const track of this.stream.getTracks()) {
22103
+ track.addEventListener("ended", this.onTrackEnded);
22104
+ }
22105
+ this.signal?.addEventListener("abort", this.onAbort, { once: true });
22106
+ this.recorder.start();
22107
+ } catch (error) {
22108
+ this.cancel(error);
22109
+ }
22110
+ }
22111
+ /** Copies a freshly drawn source frame before WebGL can clear its drawing buffer. */
22112
+ capture() {
22113
+ if (this.settled || this.stopping) return;
22114
+ try {
22115
+ this.context.fillStyle = "#000000";
22116
+ this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
22117
+ this.context.drawImage(this.source, 0, 0, this.canvas.width, this.canvas.height);
22118
+ } catch (error) {
22119
+ this.cancel(error);
22120
+ }
22121
+ }
22122
+ /** Cancels recording, releases its capture stream, and rejects {@link result}. */
22123
+ cancel(reason = new DOMException("Video export cancelled", "AbortError")) {
22124
+ if (this.settled) return;
22125
+ this.settled = true;
22126
+ this.cleanup();
22127
+ this.reject(reason);
22128
+ }
22129
+ cleanup() {
22130
+ if (this.timer !== void 0) clearTimeout(this.timer);
22131
+ this.signal?.removeEventListener("abort", this.onAbort);
22132
+ if (this.recorder) {
22133
+ this.recorder.removeEventListener("start", this.onStart);
22134
+ this.recorder.removeEventListener("dataavailable", this.onData);
22135
+ this.recorder.removeEventListener("stop", this.onStop);
22136
+ this.recorder.removeEventListener("error", this.onError);
22137
+ if (this.recorder.state !== "inactive") {
22138
+ try {
22139
+ this.recorder.stop();
22140
+ } catch {
22141
+ }
22142
+ }
22143
+ }
22144
+ for (const track of this.stream?.getTracks() ?? []) {
22145
+ track.removeEventListener("ended", this.onTrackEnded);
22146
+ track.stop();
22147
+ }
22148
+ this.chunks.length = 0;
22149
+ }
22150
+ };
22151
+
22027
22152
  // src/gui/GuiHelper.ts
22028
22153
  init_AssetKey();
22029
22154
  init_ModelTextures();
@@ -27341,6 +27466,9 @@ var _Renderer = class _Renderer {
27341
27466
  this._inAnimationLoop = true;
27342
27467
  try {
27343
27468
  this.animate(time);
27469
+ } catch (error) {
27470
+ this._videoExporter?.cancel(error);
27471
+ throw error;
27344
27472
  } finally {
27345
27473
  this._inAnimationLoop = false;
27346
27474
  }
@@ -27535,16 +27663,18 @@ var _Renderer = class _Renderer {
27535
27663
  if (this._frameCallbacks.get(callback) === subscription) this._frameCallbacks.delete(callback);
27536
27664
  };
27537
27665
  }
27538
- /** Starts or resumes the animation loop and registered frame callbacks. */
27666
+ /** Starts or resumes the animation loop and registered frame callbacks. Has no effect while running. */
27539
27667
  start() {
27540
- if (this._disposed) return;
27668
+ if (this._disposed || this._running) return;
27541
27669
  this.stop();
27542
27670
  this._running = true;
27543
27671
  if (!this._inAnimationLoop) this.renderer.setAnimationLoop(this._animationLoop);
27544
27672
  }
27545
- /** Pauses rendering and frame callbacks. Call {@link start} to resume. */
27673
+ /** Pauses rendering and frame callbacks, and cancels video export. Call {@link start} to resume. */
27546
27674
  stop() {
27547
27675
  if (this._disposed) return;
27676
+ this._videoExporter?.cancel();
27677
+ this._videoExporter = void 0;
27548
27678
  this._running = false;
27549
27679
  if (this._inAnimationLoop) {
27550
27680
  queueMicrotask(() => {
@@ -27606,7 +27736,7 @@ var _Renderer = class _Renderer {
27606
27736
  this._controls.update();
27607
27737
  }
27608
27738
  if (!this._running) return;
27609
- if (!this.dirty && !this.options.render.renderAlways && !this._frameCallbacks.size) return;
27739
+ if (!this.dirty && !this.options.render.renderAlways && !this._frameCallbacks.size && !this._videoExporter) return;
27610
27740
  const interval = this._frameInterval;
27611
27741
  if (interval) {
27612
27742
  const next = this._nextFrameTime;
@@ -27631,6 +27761,7 @@ var _Renderer = class _Renderer {
27631
27761
  } else {
27632
27762
  this.renderer.render(this.scene, this.camera);
27633
27763
  }
27764
+ this._videoExporter?.capture();
27634
27765
  this.dirty = false;
27635
27766
  if (this._stats) {
27636
27767
  this._stats.end();
@@ -27651,6 +27782,35 @@ var _Renderer = class _Renderer {
27651
27782
  const canvas = trim ? trimCanvas(this.renderer.domElement) : this.renderer.domElement;
27652
27783
  return canvas.toDataURL(mime, quality);
27653
27784
  }
27785
+ /**
27786
+ * Records a silent browser video in real time, including frame callbacks and animated textures.
27787
+ * Keep the tab visible; browser scheduling and `render.fpsLimit` limit the capture rate.
27788
+ * The video keeps the initial drawing-buffer size and fills transparent pixels with black.
27789
+ * Temporarily starts a stopped renderer. Stop, disposal, or `options.signal` cancels the export.
27790
+ * Only one export can run at a time. Use the returned Blob's type to select a file extension.
27791
+ */
27792
+ async toVideo(options) {
27793
+ if (this._disposed) throw new Error("Cannot export a video from a disposed renderer");
27794
+ if (this._videoExporter) throw new Error("A video export is already running");
27795
+ const exporter = new VideoExporter(this.renderer.domElement, options);
27796
+ const wasRunning = this._running;
27797
+ try {
27798
+ try {
27799
+ if (!wasRunning) this.start();
27800
+ this._videoExporter = exporter;
27801
+ if (this._controls?.enabled) this._controls.update();
27802
+ this.drawFrame();
27803
+ } catch (error) {
27804
+ exporter.cancel(error);
27805
+ }
27806
+ return await exporter.result;
27807
+ } finally {
27808
+ if (this._videoExporter === exporter) {
27809
+ this._videoExporter = void 0;
27810
+ if (!wasRunning) this.stop();
27811
+ }
27812
+ }
27813
+ }
27654
27814
  /** Exports visible scene meshes as OBJ text, without texture images. */
27655
27815
  toObj() {
27656
27816
  return SceneExporter.toObj(this.scene);
@@ -29381,6 +29541,7 @@ export {
29381
29541
  Ticker,
29382
29542
  TintColors,
29383
29543
  UVMapper,
29544
+ VideoExporter,
29384
29545
  WrappedImage,
29385
29546
  addBox3WireframeToObject,
29386
29547
  addWireframeToMesh,