@stormstreaming/stormstreamer 1.0.0-rc.0 → 1.0.0

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/amd/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * contact@stormstreaming.com
5
5
  * https://stormstreaming.com
6
6
  *
7
- * Version: 1.0.0-rc.0
8
- * Version: 3/28/2025, 9:18:38 AM
7
+ * Version: 1.0.0
8
+ * Version: 2/7/2026, 6:37:17 PM
9
9
  *
10
10
  * LEGAL NOTICE:
11
11
  * This software is subject to the terms and conditions defined in
@@ -2500,6 +2500,9 @@
2500
2500
  * @private
2501
2501
  */
2502
2502
  this._isMutedByBrowser = false;
2503
+ // POPRAWKA: Dodane pola do przechowywania referencji do handlerów
2504
+ this._loadedMetadataHandler = null;
2505
+ this._volumeChangeHandler = null;
2503
2506
  /**
2504
2507
  * Called whenever browser stops playback
2505
2508
  */
@@ -2555,20 +2558,24 @@
2555
2558
  * Fires whenever something changes video object volume
2556
2559
  * @param event
2557
2560
  */
2558
- // @ts-ignore
2559
- this._videoElement.onvolumechange = () => {
2561
+ // POPRAWKA: Zapisanie referencji do handlera
2562
+ this._volumeChangeHandler = () => {
2560
2563
  this.dispatchVolumeEvent();
2561
2564
  };
2565
+ // @ts-ignore
2566
+ this._videoElement.onvolumechange = this._volumeChangeHandler;
2562
2567
  this._videoElement.onpause = () => {
2563
2568
  // nothing
2564
2569
  };
2565
- this._videoElement.addEventListener('loadedmetadata', () => {
2570
+ // POPRAWKA: Zapisanie referencji do handlera
2571
+ this._loadedMetadataHandler = () => {
2566
2572
  this._main.dispatchEvent("metadata", {
2567
2573
  ref: this._main,
2568
2574
  videoWidth: this._videoElement.videoWidth,
2569
2575
  videoHeight: this._videoElement.videoHeight
2570
2576
  });
2571
- });
2577
+ };
2578
+ this._videoElement.addEventListener('loadedmetadata', this._loadedMetadataHandler);
2572
2579
  /**
2573
2580
  * Updates every second,
2574
2581
  * @param event
@@ -2590,7 +2597,9 @@
2590
2597
  this._videoElement.onended = event => {
2591
2598
  this._logger.info(this, "VideoElement :: onended");
2592
2599
  };
2593
- this._videoElement.onplay = () => {};
2600
+ this._videoElement.onplay = () => {
2601
+ // nothing
2602
+ };
2594
2603
  }
2595
2604
  /**
2596
2605
  * Sets new volume for playback. It'll also try to store value in a browser memory
@@ -2647,6 +2656,65 @@
2647
2656
  getVideoElement() {
2648
2657
  return this._videoElement;
2649
2658
  }
2659
+ // POPRAWKA: Dodana metoda destroy
2660
+ /**
2661
+ * Destroys the ScreenElement and cleans up all resources
2662
+ */
2663
+ destroy() {
2664
+ this._logger.info(this, "Destroying ScreenElement...");
2665
+ try {
2666
+ // 1. Usuń event listener z głównej klasy
2667
+ this._main.removeEventListener("playbackForceMute", this.onForceMute);
2668
+ // 2. Zatrzymaj wszelkie aktywne strumienie w elemencie video
2669
+ if (this._videoElement.srcObject instanceof MediaStream) {
2670
+ const stream = this._videoElement.srcObject;
2671
+ stream.getTracks().forEach(track => {
2672
+ track.enabled = false;
2673
+ track.stop();
2674
+ });
2675
+ }
2676
+ this._videoElement.pause();
2677
+ this._videoElement.onload = null;
2678
+ this._videoElement.onstalled = null;
2679
+ this._videoElement.onerror = null;
2680
+ this._videoElement.onvolumechange = null;
2681
+ this._videoElement.onpause = null;
2682
+ this._videoElement.ontimeupdate = null;
2683
+ this._videoElement.onended = null;
2684
+ this._videoElement.onplay = null;
2685
+ this._videoElement.onloadstart = null;
2686
+ this._videoElement.onloadeddata = null;
2687
+ this._videoElement.onloadedmetadata = null;
2688
+ this._videoElement.oncanplay = null;
2689
+ this._videoElement.oncanplaythrough = null;
2690
+ this._videoElement.onprogress = null;
2691
+ this._videoElement.onseeking = null;
2692
+ this._videoElement.onseeked = null;
2693
+ this._videoElement.onwaiting = null;
2694
+ this._videoElement.ondurationchange = null;
2695
+ this._videoElement.onratechange = null;
2696
+ this._videoElement.onsuspend = null;
2697
+ this._videoElement.onemptied = null;
2698
+ if (this._loadedMetadataHandler) {
2699
+ this._videoElement.removeEventListener('loadedmetadata', this._loadedMetadataHandler);
2700
+ this._loadedMetadataHandler = null;
2701
+ }
2702
+ this._videoElement.removeAttribute('src');
2703
+ this._videoElement.srcObject = null;
2704
+ try {
2705
+ this._videoElement.load();
2706
+ } catch (e) {
2707
+ // Ignoruj błędy podczas load()
2708
+ }
2709
+ this._videoElement.removeAttribute('playsinline');
2710
+ this._videoElement.removeAttribute('webkit-playsinline');
2711
+ if (this._videoElement.parentNode) this._videoElement.parentNode.removeChild(this._videoElement);
2712
+ this._volumeChangeHandler = null;
2713
+ this._logger.success(this, "ScreenElement successfully destroyed");
2714
+ } catch (error) {
2715
+ this._logger.error(this, "Error during ScreenElement destroy: " + error);
2716
+ }
2717
+ }
2650
2718
  }
2651
2719
 
2652
2720
  /**
@@ -2741,6 +2809,10 @@
2741
2809
  */
2742
2810
  this._parentOriginalOverflow = '';
2743
2811
  this._debug = false;
2812
+ this._animationFrameId = null;
2813
+ this._isDestroying = false;
2814
+ this._fullscreenChangeHandler = null;
2815
+ this._transitionEndHandler = null;
2744
2816
  //------------------------------------------------------------------------//
2745
2817
  // FULLSCREEN
2746
2818
  //------------------------------------------------------------------------//
@@ -2795,11 +2867,15 @@
2795
2867
  if (this._autoResizeEnabled) this.handleResize();
2796
2868
  });
2797
2869
  }
2798
- document.addEventListener('fullscreenchange', this.onFullScreenChange, false);
2799
- document.addEventListener('webkitfullscreenchange', this.onFullScreenChange, false);
2800
- document.addEventListener('mozfullscreenchange', this.onFullScreenChange, false);
2801
- document.addEventListener('webkitendfullscreen', this.onFullScreenChange, false);
2802
- this._screenElement.getVideoElement().addEventListener('webkitendfullscreen', this.onFullScreenChange, false);
2870
+ this._fullscreenChangeHandler = this.onFullScreenChange;
2871
+ this._transitionEndHandler = () => {
2872
+ this.handleResize();
2873
+ };
2874
+ document.addEventListener('fullscreenchange', this._fullscreenChangeHandler, false);
2875
+ document.addEventListener('webkitfullscreenchange', this._fullscreenChangeHandler, false);
2876
+ document.addEventListener('mozfullscreenchange', this._fullscreenChangeHandler, false);
2877
+ document.addEventListener('webkitendfullscreen', this._fullscreenChangeHandler, false);
2878
+ this._screenElement.getVideoElement().addEventListener('webkitendfullscreen', this._fullscreenChangeHandler, false);
2803
2879
  this._main.addEventListener("metadata", event => {
2804
2880
  this._videoWidth = event.videoWidth;
2805
2881
  this._videoHeight = event.videoHeight;
@@ -2861,12 +2937,14 @@
2861
2937
  let result = false;
2862
2938
  if (this._parentElement != null && this._videoContainer != null) {
2863
2939
  this._logger.info(this, "Detaching from parent: " + this._videoContainer);
2940
+ // POPRAWKA: Usuń event listener z zapisanym handlerem
2941
+ if (this._transitionEndHandler) {
2942
+ this._parentElement.removeEventListener("transitionend", this._transitionEndHandler);
2943
+ }
2864
2944
  this._parentElement.removeChild(this._videoContainer);
2865
2945
  if (this._resizeObserver) {
2866
2946
  this._resizeObserver.unobserve(this._parentElement);
2867
- this._resizeObserver.disconnect();
2868
2947
  }
2869
- if (this._autoResizeEnabled) this._parentElement.removeEventListener("transitionend", this.handleResize);
2870
2948
  this._main.dispatchEvent("containerChange", {
2871
2949
  ref: this._main,
2872
2950
  container: null
@@ -2879,16 +2957,22 @@
2879
2957
  return result;
2880
2958
  }
2881
2959
  handleResize() {
2882
- if (!this._parentElement || this._isResizing) return;
2960
+ if (!this._parentElement || this._isResizing || this._isDestroying) return;
2961
+ // POPRAWKA: Anulowanie poprzedniego requestAnimationFrame
2962
+ if (this._animationFrameId !== null) {
2963
+ cancelAnimationFrame(this._animationFrameId);
2964
+ }
2883
2965
  this._isResizing = true;
2884
2966
  this._parentOriginalOverflow = this._parentElement.style.overflow;
2885
2967
  this._parentElement.style.overflow = 'hidden';
2886
- // Używamy requestAnimationFrame aby dać przeglądarce czas na zastosowanie overflow
2887
- requestAnimationFrame(() => {
2888
- // Obliczamy nowe wymiary
2968
+ this._animationFrameId = requestAnimationFrame(() => {
2969
+ if (this._isDestroying) return;
2889
2970
  this.calculateNewDimensions();
2890
- this._parentElement.style.overflow = this._parentOriginalOverflow;
2971
+ if (this._parentElement) {
2972
+ this._parentElement.style.overflow = this._parentOriginalOverflow;
2973
+ }
2891
2974
  this._isResizing = false;
2975
+ this._animationFrameId = null;
2892
2976
  if (this._tempContainerWidth !== this._containerWidth || this._tempContainerHeight !== this._containerHeight) {
2893
2977
  this._main.dispatchEvent("resizeUpdate", {
2894
2978
  ref: this._main,
@@ -3145,7 +3229,83 @@
3145
3229
  // CLEANUP
3146
3230
  //------------------------------------------------------------------------//
3147
3231
  destroy() {
3148
- this.detachFromParent();
3232
+ var _a;
3233
+ this._logger.info(this, "Starting StageController destroy...");
3234
+ this._isDestroying = true;
3235
+ try {
3236
+ // 1. Anuluj animacje
3237
+ if (this._animationFrameId !== null) {
3238
+ cancelAnimationFrame(this._animationFrameId);
3239
+ this._animationFrameId = null;
3240
+ }
3241
+ // 2. Odłącz od rodzica
3242
+ this.detachFromParent();
3243
+ // 3. Zatrzymaj i rozłącz ResizeObserver
3244
+ if (this._resizeObserver) {
3245
+ this._resizeObserver.disconnect();
3246
+ }
3247
+ // 4. Usuń event listenery fullscreen
3248
+ if (this._fullscreenChangeHandler) {
3249
+ document.removeEventListener('fullscreenchange', this._fullscreenChangeHandler);
3250
+ document.removeEventListener('webkitfullscreenchange', this._fullscreenChangeHandler);
3251
+ document.removeEventListener('mozfullscreenchange', this._fullscreenChangeHandler);
3252
+ document.removeEventListener('webkitendfullscreen', this._fullscreenChangeHandler);
3253
+ if ((_a = this._screenElement) === null || _a === void 0 ? void 0 : _a.getVideoElement()) {
3254
+ this._screenElement.getVideoElement().removeEventListener('webkitendfullscreen', this._fullscreenChangeHandler);
3255
+ }
3256
+ this._fullscreenChangeHandler = null;
3257
+ }
3258
+ // 6. Zniszcz ScreenElement
3259
+ if (this._screenElement) {
3260
+ // Wyczyść video element
3261
+ const videoElement = this._screenElement.getVideoElement();
3262
+ if (videoElement) {
3263
+ // Zatrzymaj wszelkie strumienie
3264
+ if (videoElement.srcObject instanceof MediaStream) {
3265
+ videoElement.srcObject.getTracks().forEach(track => {
3266
+ track.enabled = false;
3267
+ track.stop();
3268
+ });
3269
+ }
3270
+ // Wyczyść element
3271
+ videoElement.pause();
3272
+ videoElement.removeAttribute('src');
3273
+ videoElement.srcObject = null;
3274
+ videoElement.load();
3275
+ // Usuń z DOM jeśli jest podłączony
3276
+ if (videoElement.parentNode) {
3277
+ videoElement.parentNode.removeChild(videoElement);
3278
+ }
3279
+ }
3280
+ // Jeśli ScreenElement ma własną metodę destroy, wywołaj ją
3281
+ if (typeof this._screenElement.destroy === 'function') {
3282
+ this._screenElement.destroy();
3283
+ }
3284
+ this._screenElement = null;
3285
+ }
3286
+ // 7. Usuń kontener video z DOM
3287
+ if (this._videoContainer) {
3288
+ if (this._videoContainer.parentNode) {
3289
+ this._videoContainer.parentNode.removeChild(this._videoContainer);
3290
+ }
3291
+ this._videoContainer = null;
3292
+ }
3293
+ // 8. Resetuj zmienne
3294
+ this._containerWidth = 0;
3295
+ this._containerHeight = 0;
3296
+ this._tempContainerWidth = 0;
3297
+ this._tempContainerHeight = 0;
3298
+ this._videoWidth = 0;
3299
+ this._videoHeight = 0;
3300
+ this.isInFullScreenMode = false;
3301
+ this._isResizing = false;
3302
+ this._autoResizeEnabled = false;
3303
+ this._logger.success(this, "StageController successfully destroyed");
3304
+ } catch (error) {
3305
+ this._logger.error(this, "Error during destroy: " + error);
3306
+ } finally {
3307
+ this._isDestroying = false;
3308
+ }
3149
3309
  }
3150
3310
  }
3151
3311
  /**
@@ -3512,9 +3672,12 @@
3512
3672
  this._lastEventTime = 0;
3513
3673
  this.THROTTLE_INTERVAL = 100; // ms between updates
3514
3674
  this._isMonitoring = false;
3515
- // Moving average variables for smoothing
3516
- this._instant = 0.0;
3517
- this._slow = 0.0;
3675
+ this._animationFrameId = null;
3676
+ // Peak levels for VU meter
3677
+ this._instant = 0.0; // Current peak (fast)
3678
+ this._slow = 0.0; // Peak hold with decay
3679
+ // Decay rate for peak hold (0.95 = slow decay, 0.8 = fast decay)
3680
+ this.PEAK_DECAY = 0.92;
3518
3681
  this._main = main;
3519
3682
  }
3520
3683
  attach(stream) {
@@ -3530,22 +3693,24 @@
3530
3693
  this._microphone = this._audioContext.createMediaStreamSource(stream);
3531
3694
  this._analyser = this._audioContext.createAnalyser();
3532
3695
  this._analyser.fftSize = 2048;
3533
- this._analyser.smoothingTimeConstant = 0.3;
3534
3696
  // Connect nodes
3535
3697
  this._microphone.connect(this._analyser);
3536
3698
  // Start monitoring
3537
3699
  this.startMonitoring();
3538
3700
  } catch (error) {
3539
3701
  console.error('SoundMeter: Error during attach:', error);
3540
- this.detach(); // Cleanup on error
3702
+ this.detach();
3541
3703
  }
3542
3704
  }
3543
-
3544
3705
  detach() {
3545
- var _a, _b;
3546
- // Stop monitoring first
3706
+ if (!this._audioContext && !this._analyser && !this._microphone) {
3707
+ return;
3708
+ }
3547
3709
  this._isMonitoring = false;
3548
- // Disconnect and cleanup nodes in reverse order
3710
+ if (this._animationFrameId !== null) {
3711
+ cancelAnimationFrame(this._animationFrameId);
3712
+ this._animationFrameId = null;
3713
+ }
3549
3714
  if (this._microphone) {
3550
3715
  try {
3551
3716
  this._microphone.disconnect();
@@ -3562,14 +3727,22 @@
3562
3727
  }
3563
3728
  this._analyser = null;
3564
3729
  }
3565
- if (((_a = this._audioContext) === null || _a === void 0 ? void 0 : _a.state) !== 'closed') {
3730
+ if (this._audioContext) {
3566
3731
  try {
3567
- (_b = this._audioContext) === null || _b === void 0 ? void 0 : _b.close();
3732
+ if (this._audioContext.state !== 'closed') {
3733
+ const audioContextRef = this._audioContext;
3734
+ this._audioContext = null;
3735
+ audioContextRef.suspend().then(() => audioContextRef.close()).catch(e => {
3736
+ console.warn('SoundMeter: Error closing audio context:', e);
3737
+ });
3738
+ } else {
3739
+ this._audioContext = null;
3740
+ }
3568
3741
  } catch (e) {
3569
- console.warn('SoundMeter: Error closing audio context:', e);
3742
+ console.warn('SoundMeter: Error handling audio context:', e);
3743
+ this._audioContext = null;
3570
3744
  }
3571
3745
  }
3572
- this._audioContext = null;
3573
3746
  this.clear();
3574
3747
  }
3575
3748
  clear() {
@@ -3582,24 +3755,29 @@
3582
3755
  this._isMonitoring = true;
3583
3756
  const dataArray = new Float32Array(this._analyser.frequencyBinCount);
3584
3757
  const analyze = () => {
3585
- if (!this._analyser || !this._isMonitoring) return;
3758
+ if (!this._analyser || !this._isMonitoring || !this._audioContext) {
3759
+ this._animationFrameId = null;
3760
+ return;
3761
+ }
3586
3762
  const now = Date.now();
3587
3763
  try {
3588
- // Read time-domain data
3589
3764
  this._analyser.getFloatTimeDomainData(dataArray);
3590
- // Calculate levels
3591
- let sum = 0.0;
3592
- let clipcount = 0;
3765
+ // Peak detection - find maximum amplitude
3766
+ let peak = 0.0;
3593
3767
  for (let i = 0; i < dataArray.length; ++i) {
3594
- const amplitude = dataArray[i];
3595
- sum += amplitude * amplitude;
3596
- if (Math.abs(amplitude) > 0.99) {
3597
- clipcount += 1;
3768
+ const amplitude = Math.abs(dataArray[i]);
3769
+ if (amplitude > peak) {
3770
+ peak = amplitude;
3598
3771
  }
3599
3772
  }
3600
- // Update metrics
3601
- this._instant = Math.sqrt(sum / dataArray.length);
3602
- this._slow = 0.05 * this._instant + 0.95 * this._slow;
3773
+ // Instant = current peak (clamped to 1.0)
3774
+ this._instant = Math.min(1.0, peak);
3775
+ // Slow = peak hold with decay (classic VU meter behavior)
3776
+ if (this._instant > this._slow) {
3777
+ this._slow = this._instant;
3778
+ } else {
3779
+ this._slow *= this.PEAK_DECAY;
3780
+ }
3603
3781
  // Throttle event dispatch
3604
3782
  if (now - this._lastEventTime >= this.THROTTLE_INTERVAL) {
3605
3783
  this._lastEventTime = now;
@@ -3612,13 +3790,17 @@
3612
3790
  } catch (error) {
3613
3791
  console.error('SoundMeter: Error during analysis:', error);
3614
3792
  this._isMonitoring = false;
3793
+ this._animationFrameId = null;
3615
3794
  return;
3616
3795
  }
3617
- // Schedule next analysis
3618
- requestAnimationFrame(analyze);
3796
+ if (this._isMonitoring) {
3797
+ this._animationFrameId = requestAnimationFrame(analyze);
3798
+ }
3619
3799
  };
3620
- // Start analysis loop
3621
- requestAnimationFrame(analyze);
3800
+ this._animationFrameId = requestAnimationFrame(analyze);
3801
+ }
3802
+ destroy() {
3803
+ this.detach();
3622
3804
  }
3623
3805
  }
3624
3806
 
@@ -4018,7 +4200,6 @@
4018
4200
  streamStatusInfo.videoWidth = msgJSON.videoWidth;
4019
4201
  streamStatusInfo.videoHeight = msgJSON.videoHeight;
4020
4202
  streamStatusInfo.currentBitrate = msgJSON.realBitrate;
4021
- console.log(streamStatusInfo);
4022
4203
  this._main.dispatchEvent("streamStatusUpdate", {
4023
4204
  ref: this._main,
4024
4205
  streamStatus: streamStatusInfo
@@ -4137,7 +4318,7 @@
4137
4318
  * @param config
4138
4319
  */
4139
4320
  constructor(main) {
4140
- var _a, _b, _c;
4321
+ var _a, _b, _c, _d;
4141
4322
  /**
4142
4323
  * Whenever current window is active or not
4143
4324
  * @private
@@ -4151,7 +4332,7 @@
4151
4332
  'iceServers': []
4152
4333
  };
4153
4334
  /**
4154
- * Whenever microphone is currenly muted
4335
+ * Whenever microphone is currently muted
4155
4336
  * @private
4156
4337
  */
4157
4338
  this._isMicrophoneMuted = false;
@@ -4161,7 +4342,7 @@
4161
4342
  */
4162
4343
  this._pendingMicrophoneState = null;
4163
4344
  /**
4164
- * Whenever we have cheched for permissions
4345
+ * Whenever we have checked for permissions
4165
4346
  * @private
4166
4347
  */
4167
4348
  this._permissionChecked = false;
@@ -4224,57 +4405,93 @@
4224
4405
  this._currentOrientation = ((_a = window.screen.orientation) === null || _a === void 0 ? void 0 : _a.type) || '';
4225
4406
  this._statusTimer = null;
4226
4407
  this._debug = false;
4408
+ // FIELDS FOR OPERATION CANCELLATION
4409
+ this._deviceChangeHandler = null;
4410
+ this._orientationChangeHandler = null;
4411
+ /**
4412
+ * CRITICAL: This flag is checked after EVERY async operation
4413
+ * Set to true immediately when destroy() is called
4414
+ * @private
4415
+ */
4416
+ this._isDestroyed = false;
4417
+ this._cameraAbortController = null;
4418
+ this._microphoneAbortController = null;
4419
+ this._startCameraAbortController = null;
4420
+ this._switchingCamera = false;
4421
+ this._switchingMicrophone = false;
4422
+ this._firstPublish = true;
4423
+ /**
4424
+ * Counter for tracking active media streams (for debugging)
4425
+ * @private
4426
+ */
4427
+ this._activeStreamCount = 0;
4428
+ /**
4429
+ * Permission status objects that need cleanup
4430
+ * @private
4431
+ */
4432
+ this._cameraPermissionStatus = null;
4433
+ this._microphonePermissionStatus = null;
4434
+ /**
4435
+ * Bound handlers for permission changes (needed for removal)
4436
+ * @private
4437
+ */
4438
+ this._boundCameraPermissionHandler = null;
4439
+ this._boundMicrophonePermissionHandler = null;
4227
4440
  /**
4228
4441
  * Handles device state changes and initiates publishing if appropriate
4229
4442
  * @private
4230
4443
  */
4231
4444
  this.onDeviceStateChange = event => {
4232
4445
  var _a;
4446
+ if (this._isDestroyed) return;
4233
4447
  const usedStreamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
4234
4448
  if (event.state == exports.InputDevicesState.READY && usedStreamKey != null) {
4235
4449
  this.publish(usedStreamKey);
4236
4450
  }
4237
4451
  };
4238
4452
  /**
4239
- * This method is responsible for handing orientation changes for mobile devices
4453
+ * Handles orientation changes for mobile devices
4240
4454
  */
4241
4455
  this.handleOrientationChange = () => __awaiter(this, void 0, void 0, function* () {
4242
- var _d, _e;
4243
- // Dajemy chwilę na ustabilizowanie się orientacji
4456
+ var _e, _f;
4457
+ if (this._isDestroyed) return;
4244
4458
  yield new Promise(resolve => setTimeout(resolve, 500));
4245
- const newOrientation = ((_d = window.screen.orientation) === null || _d === void 0 ? void 0 : _d.type) || '';
4459
+ if (this._isDestroyed) return;
4460
+ const newOrientation = ((_e = window.screen.orientation) === null || _e === void 0 ? void 0 : _e.type) || '';
4246
4461
  if (this._currentOrientation !== newOrientation) {
4247
4462
  this._logger.info(this, `Orientation changed from ${this._currentOrientation} to ${newOrientation}`);
4248
4463
  this._currentOrientation = newOrientation;
4249
- // Zapamiętaj aktualny stream key i stan publikacji
4250
- const streamKey = (_e = this._main.getConfigManager()) === null || _e === void 0 ? void 0 : _e.getStreamData().streamKey;
4464
+ const streamKey = (_f = this._main.getConfigManager()) === null || _f === void 0 ? void 0 : _f.getStreamData().streamKey;
4251
4465
  this._publishState === exports.PublishState.PUBLISHED;
4252
- // Zamknij istniejące połączenie i stream
4253
4466
  this.closeWebRTCConnection();
4254
4467
  if (this._stream) {
4468
+ this._logger.info(this, "📹 [RELEASE] handleOrientationChange() - releasing stream for orientation change");
4255
4469
  this._stream.getTracks().forEach(track => {
4256
4470
  track.stop();
4471
+ this._logger.info(this, `📹 [RELEASE] handleOrientationChange() - stopped track: ${track.kind}`);
4257
4472
  });
4258
4473
  this._stream = null;
4474
+ this._activeStreamCount--;
4259
4475
  }
4260
- // Rozpocznij wszystko od nowa
4476
+ if (this._isDestroyed) return;
4261
4477
  try {
4262
4478
  yield this.startCamera();
4263
- // Jeśli stream był opublikowany, publikujemy ponownie
4479
+ if (this._isDestroyed) return;
4264
4480
  if (streamKey) {
4265
4481
  this.publish(streamKey);
4266
4482
  }
4267
4483
  } catch (error) {
4484
+ if (this._isDestroyed) return;
4268
4485
  this._logger.error(this, "Error restarting stream after orientation change: " + JSON.stringify(error));
4269
4486
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4270
4487
  }
4271
4488
  }
4272
4489
  });
4273
- this.onServerDisconnect = () => {};
4274
- /**
4275
- * Method for handling a situation when a given streamKey is already in use.
4276
- */
4490
+ this.onServerDisconnect = () => {
4491
+ // Implementation
4492
+ };
4277
4493
  this.onStreamKeyTaken = () => {
4494
+ if (this._isDestroyed) return;
4278
4495
  if (this._restartTimer != null) {
4279
4496
  clearInterval(this._restartTimer);
4280
4497
  this._restartTimerCount = 0;
@@ -4283,6 +4500,10 @@
4283
4500
  this.setPublishState(exports.PublishState.ERROR);
4284
4501
  this._restartTimer = setInterval(() => {
4285
4502
  var _a, _b;
4503
+ if (this._isDestroyed) {
4504
+ if (this._restartTimer) clearInterval(this._restartTimer);
4505
+ return;
4506
+ }
4286
4507
  if (this._restartTimer != null) {
4287
4508
  if (this._restartTimerCount < this._restartTimerMaxCount) {
4288
4509
  this._logger.info(this, "WebRTCStreamer :: StreamKeyTaken Interval: " + this._restartTimerCount + "/" + this._restartTimerMaxCount);
@@ -4293,41 +4514,37 @@
4293
4514
  this._restartTimerCount = 0;
4294
4515
  const streamData = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData();
4295
4516
  const streamKey = streamData === null || streamData === void 0 ? void 0 : streamData.streamKey;
4296
- if (streamKey != null) {
4297
- const prevStreamKey = streamKey;
4298
- this.publish(prevStreamKey);
4517
+ if (streamKey != null && !this._isDestroyed) {
4518
+ this.publish(streamKey);
4299
4519
  }
4300
4520
  }
4301
- const usedStreamKey = (_b = this._main.getConfigManager().getStreamData().streamKey) !== null && _b !== void 0 ? _b : "unknown";
4302
- this._main.dispatchEvent("streamKeyInUseInterval", {
4303
- ref: this._main,
4304
- streamKey: usedStreamKey,
4305
- count: this._restartTimerCount,
4306
- maxCount: this._restartTimerMaxCount
4307
- });
4521
+ if (!this._isDestroyed) {
4522
+ const usedStreamKey = (_b = this._main.getConfigManager().getStreamData().streamKey) !== null && _b !== void 0 ? _b : "unknown";
4523
+ this._main.dispatchEvent("streamKeyInUseInterval", {
4524
+ ref: this._main,
4525
+ streamKey: usedStreamKey,
4526
+ count: this._restartTimerCount,
4527
+ maxCount: this._restartTimerMaxCount
4528
+ });
4529
+ }
4308
4530
  }
4309
4531
  }, 1000);
4310
4532
  };
4311
4533
  //------------------------------------------------------------------------//
4312
4534
  // NETWORK AND RTC
4313
4535
  //------------------------------------------------------------------------//
4314
- /**
4315
- * Method fires once a connection with wowza is established. It's the main connection where we exchange
4316
- * ice-candidates and SDP. We'll start setting up peer connections.
4317
- */
4318
4536
  this.onServerConnect = () => {
4537
+ if (this._isDestroyed) return;
4319
4538
  if (this._peerConnection) {
4320
4539
  this.closeWebRTCConnection();
4321
4540
  }
4322
4541
  this._peerConnection = new RTCPeerConnection(this._peerConnectionConfig);
4323
- // Najpierw dodaj tracki
4324
4542
  if (this._stream) {
4325
4543
  let localTracks = this._stream.getTracks();
4326
4544
  for (let localTrack in localTracks) {
4327
4545
  this._peerConnection.addTrack(localTracks[localTrack], this._stream);
4328
4546
  }
4329
4547
  }
4330
- // Potem dodaj event handlery
4331
4548
  this._peerConnection.onicecandidate = event => {
4332
4549
  this.onIceCandidate(event);
4333
4550
  };
@@ -4335,26 +4552,29 @@
4335
4552
  this.onConnectionStateChange(event);
4336
4553
  };
4337
4554
  this._peerConnection.onnegotiationneeded = event => __awaiter(this, void 0, void 0, function* () {
4338
- if (this._peerConnection) {
4555
+ if (this._peerConnection && !this._isDestroyed) {
4339
4556
  try {
4340
4557
  const description = yield this._peerConnection.createOffer();
4341
- yield this.onDescriptionSuccess(description);
4558
+ if (!this._isDestroyed) {
4559
+ yield this.onDescriptionSuccess(description);
4560
+ }
4342
4561
  } catch (error) {
4343
4562
  this.onDescriptionError(error);
4344
- console.error('Error creating offer:', error);
4345
4563
  }
4346
4564
  }
4347
4565
  });
4348
4566
  this.createStatusConnection();
4349
4567
  };
4350
- /**
4351
- * Method fires once a status connection is established. We'll set an interval for monitoring stream status.
4352
- */
4353
4568
  this.onStatusServerConnect = () => {
4569
+ if (this._isDestroyed) return;
4354
4570
  const usedStreamKey = this._main.getConfigManager().getStreamData().streamKey;
4355
4571
  if (this._statusTimer == null) {
4356
4572
  if (this._statusConnection != null && usedStreamKey != null) {
4357
4573
  this._statusTimer = setInterval(() => {
4574
+ if (this._isDestroyed) {
4575
+ if (this._statusTimer) clearInterval(this._statusTimer);
4576
+ return;
4577
+ }
4358
4578
  this.requestStatusData();
4359
4579
  }, 1000);
4360
4580
  }
@@ -4362,27 +4582,22 @@
4362
4582
  };
4363
4583
  this.requestStatusData = () => {
4364
4584
  var _a;
4585
+ if (this._isDestroyed) return;
4365
4586
  (_a = this._statusConnection) === null || _a === void 0 ? void 0 : _a.sendData('{"packetID":"STREAM_STATUS", "streamKey": "' + this._fullStreamName + '"}');
4366
4587
  };
4367
- /**
4368
- * If for some reason the status connection is disconnected we have to clean the interval
4369
- */
4370
4588
  this.onStatusServerDisconnect = () => {
4371
4589
  if (this._statusTimer != null) clearInterval(this._statusTimer);
4372
4590
  this._statusTimer = null;
4373
4591
  };
4374
- /**
4375
- * This event fires whenever "STREAM_STATUS_RESPONSE" packet form status connection reports stream status along some stream data. This gives
4376
- * us an insight into whenever our stream is ok (works) or not.
4377
- * @param event
4378
- */
4379
4592
  this.onStreamStatsUpdate = event => {
4593
+ if (this._isDestroyed) return;
4380
4594
  const update = event.streamStatus;
4381
4595
  if (this._publishState == exports.PublishState.PUBLISHED && update.publishState != exports.PublishState.PUBLISHED) this.setPublishState(exports.PublishState.UNPUBLISHED);
4382
4596
  if (this._publishState == exports.PublishState.CONNECTED && update.publishState == exports.PublishState.PUBLISHED) this.setPublishState(exports.PublishState.PUBLISHED);
4383
4597
  };
4384
4598
  this.onDescriptionSuccess = description => {
4385
4599
  var _a, _b, _c;
4600
+ if (this._isDestroyed) return;
4386
4601
  this._fullStreamName = ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey) + "_" + new Date().getTime();
4387
4602
  const streamInfo = {
4388
4603
  applicationName: (_c = (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.getConnection().getCurrentServer()) === null || _c === void 0 ? void 0 : _c.getApplication(),
@@ -4396,22 +4611,19 @@
4396
4611
  videoCodec: "42e01f",
4397
4612
  audioCodec: "opus"
4398
4613
  });
4399
- if (this._peerConnection) {
4614
+ if (this._peerConnection && !this._isDestroyed) {
4400
4615
  this._peerConnection.setLocalDescription(description).then(() => {
4401
4616
  var _a;
4617
+ if (this._isDestroyed) return;
4402
4618
  (_a = this._main.getNetworkController()) === null || _a === void 0 ? void 0 : _a.sendMessage('{"direction":"publish", "command":"sendOffer", "streamInfo":' + JSON.stringify(streamInfo) + ', "sdp":' + JSON.stringify(description) + '}');
4403
4619
  }).catch(error => {
4404
4620
  console.log(error);
4405
- //this.onWebRTCError(error, self);
4406
4621
  });
4407
4622
  }
4408
4623
  };
4409
4624
  //------------------------------------------------------------------------//
4410
4625
  // BLUR & FOCUS
4411
4626
  //------------------------------------------------------------------------//
4412
- /**
4413
- * Methods handles visibility change events
4414
- */
4415
4627
  this.visibilityChange = () => {
4416
4628
  if (document.visibilityState === 'hidden') {
4417
4629
  this.onWindowBlur();
@@ -4419,18 +4631,12 @@
4419
4631
  this.onWindowFocus();
4420
4632
  }
4421
4633
  };
4422
- /**
4423
- * Reacts to browser changing visibility of the document (or blur)
4424
- */
4425
4634
  this.onWindowBlur = () => {
4426
4635
  if (this._isWindowActive) {
4427
4636
  this._logger.warning(this, "Player window is no longer in focus!");
4428
4637
  }
4429
4638
  this._isWindowActive = false;
4430
4639
  };
4431
- /**
4432
- * Reacts to browser changing visibility of the document (or focus)
4433
- */
4434
4640
  this.onWindowFocus = () => {
4435
4641
  if (!this._isWindowActive) {
4436
4642
  this._logger.info(this, "Player window is focused again!");
@@ -4442,38 +4648,76 @@
4442
4648
  this._mungeSDP = new MungeSDP();
4443
4649
  this._soundMeter = new SoundMeter(this._main);
4444
4650
  this._debug = (_c = (_b = this._main.getConfigManager()) === null || _b === void 0 ? void 0 : _b.getSettingsData().getDebugData().streamerControllerDebug) !== null && _c !== void 0 ? _c : this._debug;
4651
+ // Restore saved microphone mute state
4652
+ const savedMuteState = (_d = this._main.getStorageManager()) === null || _d === void 0 ? void 0 : _d.getField("microphoneMuted");
4653
+ if (savedMuteState !== null) {
4654
+ this._isMicrophoneMuted = savedMuteState === "true";
4655
+ this._logger.info(this, `📹 [INIT] Restored microphone mute state: ${this._isMicrophoneMuted}`);
4656
+ }
4445
4657
  // Start initialization process
4446
4658
  this.initialize();
4447
4659
  }
4448
4660
  //------------------------------------------------------------------------//
4661
+ // HELPER: Check if destroyed
4662
+ //------------------------------------------------------------------------//
4663
+ /**
4664
+ * Helper method to check if instance is destroyed
4665
+ * Call this after EVERY await!
4666
+ * @private
4667
+ */
4668
+ isDestroyedCheck(context) {
4669
+ var _a;
4670
+ if (this._isDestroyed) {
4671
+ if (context) {
4672
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.info(this, `📹 [ABORT] ${context} - instance destroyed, aborting`);
4673
+ }
4674
+ return true;
4675
+ }
4676
+ return false;
4677
+ }
4678
+ //------------------------------------------------------------------------//
4449
4679
  // MAIN METHODS
4450
4680
  //------------------------------------------------------------------------//
4451
4681
  /**
4452
4682
  * Initializes the PlaybackController by setting up event listeners and device handling
4453
- * This method orchestrates the initialization process by first checking device availability
4454
- * and permissions, then setting up necessary event listeners and configurations
4455
4683
  * @private
4456
4684
  */
4457
4685
  initialize() {
4458
4686
  var _a, _b;
4459
4687
  return __awaiter(this, void 0, void 0, function* () {
4688
+ this._logger.info(this, "📹 [INIT] initialize() - starting initialization");
4460
4689
  try {
4690
+ if (this.isDestroyedCheck("initialize start")) return;
4461
4691
  yield this.initializeDevices();
4692
+ if (this.isDestroyedCheck("after initializeDevices")) return;
4462
4693
  this.setupEventListeners();
4694
+ if (this.isDestroyedCheck("after setupEventListeners")) return;
4463
4695
  this.initializeStream();
4696
+ if (this.isDestroyedCheck("after initializeStream")) return;
4464
4697
  this.setupOrientationListener();
4465
- this.setupPermissionListeners();
4698
+ yield this.setupPermissionListeners();
4699
+ if (this.isDestroyedCheck("after setupPermissionListeners")) return;
4466
4700
  this.setupDeviceChangeListener();
4701
+ if (this.isDestroyedCheck("before network init")) return;
4467
4702
  if ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getSettingsData().autoConnect) {
4468
4703
  this._logger.info(this, "Initializing NetworkController (autoConnect is true)");
4469
4704
  (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.initialize();
4470
4705
  } else {
4471
4706
  this._logger.warning(this, "Warning - autoConnect is set to false, switching to standby mode!");
4472
4707
  }
4708
+ this._logger.success(this, "📹 [INIT] initialize() - completed successfully");
4473
4709
  } catch (error) {
4710
+ if (this._isDestroyed) {
4711
+ this._logger.warning(this, "📹 [INIT] initialize() - aborted by destroy()");
4712
+ return;
4713
+ }
4474
4714
  this._logger.error(this, "Initialization failed: " + JSON.stringify(error));
4475
4715
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4476
4716
  }
4717
+ this._main.dispatchEvent("microphoneStateChange", {
4718
+ ref: this._main,
4719
+ isMuted: this._isMicrophoneMuted
4720
+ });
4477
4721
  });
4478
4722
  }
4479
4723
  /**
@@ -4481,6 +4725,7 @@
4481
4725
  * @private
4482
4726
  */
4483
4727
  setupEventListeners() {
4728
+ if (this._isDestroyed) return;
4484
4729
  this._main.addEventListener("serverConnect", this.onServerConnect, false);
4485
4730
  this._main.addEventListener("serverDisconnect", this.onServerDisconnect, false);
4486
4731
  this._main.addEventListener("streamKeyInUse", this.onStreamKeyTaken, false);
@@ -4499,18 +4744,38 @@
4499
4744
  initializeDevices() {
4500
4745
  return __awaiter(this, void 0, void 0, function* () {
4501
4746
  try {
4747
+ this._logger.info(this, "📹 [ACQUIRE] initializeDevices() - requesting test getUserMedia for permissions");
4748
+ if (this.isDestroyedCheck("initializeDevices before getUserMedia")) return;
4502
4749
  // Request initial device permissions
4503
4750
  const stream = yield navigator.mediaDevices.getUserMedia({
4504
4751
  video: true,
4505
4752
  audio: true
4506
4753
  });
4754
+ // CRITICAL: Check IMMEDIATELY after getUserMedia returns
4755
+ if (this._isDestroyed) {
4756
+ this._logger.warning(this, "📹 [RELEASE] initializeDevices() - destroyed during getUserMedia, releasing orphan stream");
4757
+ stream.getTracks().forEach(track => {
4758
+ track.stop();
4759
+ this._logger.info(this, `📹 [RELEASE] initializeDevices() - stopped orphan track: ${track.kind}, id: ${track.id}`);
4760
+ });
4761
+ return;
4762
+ }
4763
+ this._logger.info(this, "📹 [RELEASE] initializeDevices() - stopping test stream immediately");
4507
4764
  // Stop the test stream
4508
- stream.getTracks().forEach(track => track.stop());
4765
+ stream.getTracks().forEach(track => {
4766
+ track.stop();
4767
+ this._logger.info(this, `📹 [RELEASE] initializeDevices() - stopped track: ${track.kind}, id: ${track.id}`);
4768
+ });
4769
+ if (this.isDestroyedCheck("initializeDevices before grabDevices")) return;
4509
4770
  // Initialize device lists
4510
4771
  yield this.grabDevices();
4511
4772
  } catch (error) {
4773
+ if (this._isDestroyed) return;
4774
+ console.log(error);
4512
4775
  this._logger.error(this, "Error initializing devices: " + JSON.stringify(error));
4513
- yield this.grabDevices();
4776
+ if (!this._isDestroyed) {
4777
+ yield this.grabDevices();
4778
+ }
4514
4779
  }
4515
4780
  });
4516
4781
  }
@@ -4519,56 +4784,92 @@
4519
4784
  * @private
4520
4785
  */
4521
4786
  initializeStream() {
4787
+ if (this._isDestroyed) return;
4522
4788
  if (this._selectedCamera || this._selectedMicrophone) {
4523
4789
  this.startCamera();
4524
4790
  }
4525
4791
  }
4792
+ /**
4793
+ * Sets up permission listeners with proper cleanup tracking
4794
+ * @private
4795
+ */
4526
4796
  setupPermissionListeners() {
4527
- const cameraQuery = {
4528
- name: 'camera'
4529
- };
4530
- const microphoneQuery = {
4531
- name: 'microphone'
4532
- };
4533
- navigator.permissions.query(cameraQuery).then(permissionStatus => {
4534
- permissionStatus.onchange = () => {
4535
- this._permissionChecked = false;
4536
- this.handlePermissionChange('camera', permissionStatus.state);
4537
- };
4538
- });
4539
- navigator.permissions.query(microphoneQuery).then(permissionStatus => {
4540
- permissionStatus.onchange = () => {
4541
- this._permissionChecked = false;
4542
- this.handlePermissionChange('microphone', permissionStatus.state);
4543
- };
4797
+ return __awaiter(this, void 0, void 0, function* () {
4798
+ if (this._isDestroyed) return;
4799
+ try {
4800
+ const cameraQuery = {
4801
+ name: 'camera'
4802
+ };
4803
+ const microphoneQuery = {
4804
+ name: 'microphone'
4805
+ };
4806
+ // Camera permission
4807
+ this._cameraPermissionStatus = yield navigator.permissions.query(cameraQuery);
4808
+ if (this._isDestroyed) return;
4809
+ this._boundCameraPermissionHandler = () => {
4810
+ if (this._isDestroyed) return;
4811
+ this._permissionChecked = false;
4812
+ this.handlePermissionChange('camera', this._cameraPermissionStatus.state);
4813
+ };
4814
+ this._cameraPermissionStatus.addEventListener('change', this._boundCameraPermissionHandler);
4815
+ // Microphone permission
4816
+ this._microphonePermissionStatus = yield navigator.permissions.query(microphoneQuery);
4817
+ if (this._isDestroyed) return;
4818
+ this._boundMicrophonePermissionHandler = () => {
4819
+ if (this._isDestroyed) return;
4820
+ this._permissionChecked = false;
4821
+ this.handlePermissionChange('microphone', this._microphonePermissionStatus.state);
4822
+ };
4823
+ this._microphonePermissionStatus.addEventListener('change', this._boundMicrophonePermissionHandler);
4824
+ } catch (error) {
4825
+ this._logger.warning(this, "Could not set up permission listeners: " + error);
4826
+ }
4544
4827
  });
4545
4828
  }
4829
+ /**
4830
+ * Removes permission listeners
4831
+ * @private
4832
+ */
4833
+ removePermissionListeners() {
4834
+ if (this._cameraPermissionStatus && this._boundCameraPermissionHandler) {
4835
+ this._cameraPermissionStatus.removeEventListener('change', this._boundCameraPermissionHandler);
4836
+ this._cameraPermissionStatus = null;
4837
+ this._boundCameraPermissionHandler = null;
4838
+ }
4839
+ if (this._microphonePermissionStatus && this._boundMicrophonePermissionHandler) {
4840
+ this._microphonePermissionStatus.removeEventListener('change', this._boundMicrophonePermissionHandler);
4841
+ this._microphonePermissionStatus = null;
4842
+ this._boundMicrophonePermissionHandler = null;
4843
+ }
4844
+ }
4546
4845
  /**
4547
4846
  * Sets up event listener for device changes and handles them appropriately
4548
4847
  * @private
4549
4848
  */
4550
4849
  setupDeviceChangeListener() {
4551
- navigator.mediaDevices.addEventListener('devicechange', () => __awaiter(this, void 0, void 0, function* () {
4850
+ if (this._isDestroyed) return;
4851
+ this._deviceChangeHandler = () => __awaiter(this, void 0, void 0, function* () {
4552
4852
  var _a;
4853
+ if (this._isDestroyed) return;
4854
+ if (this._publishState === exports.PublishState.PUBLISHED) {
4855
+ this._logger.info(this, "Device change detected, but already publish - no restarting streamer");
4856
+ return;
4857
+ }
4553
4858
  this._logger.info(this, "Device change detected, restarting streamer");
4554
- // Store current stream key if we're publishing
4555
4859
  const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
4556
- const wasPublishing = this._publishState === exports.PublishState.CONNECTED || this._publishState === exports.PublishState.PUBLISHED;
4860
+ const wasPublishing = this._publishState === exports.PublishState.CONNECTED;
4557
4861
  try {
4558
- // Stop all current operations
4559
4862
  this.stop();
4560
- // Wait a moment for devices to settle
4561
4863
  yield new Promise(resolve => setTimeout(resolve, 500));
4562
- // Restart the streamer
4864
+ if (this._isDestroyed) return;
4563
4865
  yield this.start();
4564
- // If we were publishing before, try to resume
4866
+ if (this._isDestroyed) return;
4565
4867
  if (wasPublishing && streamKey) {
4566
4868
  this._logger.info(this, "Resuming publishing after device change");
4567
4869
  if (this.isStreamReady(true, true)) {
4568
4870
  this.publish(streamKey);
4569
4871
  } else {
4570
4872
  this._logger.warning(this, "Cannot resume publishing - stream not ready after device change");
4571
- // Używamy ogólnego eventu zamiast nowego
4572
4873
  this._main.dispatchEvent("inputDeviceError", {
4573
4874
  ref: this._main
4574
4875
  });
@@ -4576,20 +4877,33 @@
4576
4877
  }
4577
4878
  this._logger.success(this, "Successfully handled device change");
4578
4879
  } catch (error) {
4880
+ if (this._isDestroyed) return;
4579
4881
  this._logger.error(this, "Error handling device change: " + JSON.stringify(error));
4580
4882
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4581
4883
  }
4582
- }));
4884
+ });
4885
+ navigator.mediaDevices.addEventListener('devicechange', this._deviceChangeHandler);
4583
4886
  }
4887
+ /**
4888
+ * Handles permission changes for camera or microphone
4889
+ * @private
4890
+ */
4584
4891
  handlePermissionChange(device, state) {
4585
4892
  return __awaiter(this, void 0, void 0, function* () {
4893
+ if (this._isDestroyed) return;
4894
+ this._logger.info(this, `📹 [PERMISSION] handlePermissionChange() - device: ${device}, state: ${state}`);
4586
4895
  if (state === 'denied') {
4587
4896
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4588
- if (this._publishState == exports.PublishState.CONNECTED) {
4589
- this.closeStream();
4897
+ // Stop the media stream AND WebRTC
4898
+ if (this._publishState == exports.PublishState.CONNECTED || this._stream) {
4899
+ this._logger.info(this, "📹 [RELEASE] handlePermissionChange() - permission denied, stopping all streams");
4900
+ this.stopCameraStream();
4901
+ this.closeWebRTCConnection();
4590
4902
  }
4591
4903
  }
4904
+ if (this._isDestroyed) return;
4592
4905
  yield this.grabDevices();
4906
+ if (this._isDestroyed) return;
4593
4907
  if (state === 'granted') {
4594
4908
  yield this.startCamera();
4595
4909
  }
@@ -4599,7 +4913,17 @@
4599
4913
  * Handles successful camera stream initialization
4600
4914
  */
4601
4915
  onCameraStreamSuccess(stream) {
4602
- this._logger.success(this, "Camera stream successfully retrieved");
4916
+ var _a, _b;
4917
+ // CRITICAL: Check if we were destroyed while waiting
4918
+ if (this._isDestroyed) {
4919
+ this._logger.warning(this, "📹 [RELEASE] onCameraStreamSuccess() - destroyed, releasing stream immediately");
4920
+ stream.getTracks().forEach(track => {
4921
+ track.stop();
4922
+ });
4923
+ return;
4924
+ }
4925
+ this._activeStreamCount++;
4926
+ this._logger.success(this, `📹 [ACQUIRED] onCameraStreamSuccess() - stream acquired, id: ${stream.id}, active streams: ${this._activeStreamCount}`);
4603
4927
  // Get actual stream dimensions
4604
4928
  const videoTrack = stream.getVideoTracks()[0];
4605
4929
  const audioTrack = stream.getAudioTracks()[0];
@@ -4608,7 +4932,6 @@
4608
4932
  const settings = videoTrack.getSettings();
4609
4933
  let width = settings.width;
4610
4934
  let height = settings.height;
4611
- // Na urządzeniach mobilnych potrzebujemy skorygować wymiary
4612
4935
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
4613
4936
  if (isMobile) {
4614
4937
  if (width > height && window.innerWidth < window.innerHeight || width < height && window.innerWidth > window.innerHeight) {
@@ -4618,12 +4941,10 @@
4618
4941
  streamData.streamWidth = width;
4619
4942
  streamData.streamHeight = height;
4620
4943
  streamData.videoTrackPresent = true;
4621
- // Obliczanie proporcji i konwersja na string
4622
4944
  const gcd = (a, b) => b ? gcd(b, a % b) : a;
4623
4945
  const divisor = gcd(width, height);
4624
4946
  const widthRatio = width / divisor;
4625
4947
  const heightRatio = height / divisor;
4626
- // Sprawdzamy typowe proporcje z pewną tolerancją
4627
4948
  const ratio = width / height;
4628
4949
  let aspectRatioString = `${widthRatio}:${heightRatio}`;
4629
4950
  if (Math.abs(ratio - 16 / 9) < 0.1) {
@@ -4632,11 +4953,22 @@
4632
4953
  aspectRatioString = width > height ? "4:3" : "3:4";
4633
4954
  }
4634
4955
  streamData.aspectRatio = aspectRatioString;
4956
+ this._logger.info(this, `📹 [INFO] Video track - id: ${videoTrack.id}, enabled: ${videoTrack.enabled}, readyState: ${videoTrack.readyState}`);
4635
4957
  } else {
4636
4958
  streamData.videoTrackPresent = false;
4637
4959
  }
4960
+ if (audioTrack) {
4961
+ this._logger.info(this, `📹 [INFO] Audio track - id: ${audioTrack.id}, enabled: ${audioTrack.enabled}, readyState: ${audioTrack.readyState}`);
4962
+ }
4638
4963
  streamData.audioTrackPresent = !!audioTrack;
4639
4964
  this._logger.info(this, `Publish MetaData :: Resolution: ${streamData.streamWidth}x${streamData.streamHeight} | ` + `Aspect ratio: ${streamData.aspectRatio}, ` + `Video track: ${streamData.videoTrackPresent} | Audio track: ${streamData.audioTrackPresent}`);
4965
+ // Double-check we weren't destroyed during setup
4966
+ if (this._isDestroyed) {
4967
+ this._logger.warning(this, "📹 [RELEASE] onCameraStreamSuccess() - destroyed during setup, releasing");
4968
+ stream.getTracks().forEach(track => track.stop());
4969
+ this._activeStreamCount--;
4970
+ return;
4971
+ }
4640
4972
  // Dispatch event with stream data
4641
4973
  this._main.dispatchEvent("publishMetadataUpdate", {
4642
4974
  ref: this._main,
@@ -4655,13 +4987,15 @@
4655
4987
  if (this._cameraList == null || this._microphoneList == null) {
4656
4988
  this.grabDevices();
4657
4989
  }
4658
- const videoElement = this._main.getStageController().getScreenElement().getVideoElement();
4659
- videoElement.srcObject = stream;
4660
- videoElement.autoplay = true;
4661
- videoElement.playsInline = true;
4662
- videoElement.disableRemotePlayback = true;
4663
- videoElement.controls = false;
4664
- videoElement.muted = true;
4990
+ const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
4991
+ if (videoElement) {
4992
+ videoElement.srcObject = stream;
4993
+ videoElement.autoplay = true;
4994
+ videoElement.playsInline = true;
4995
+ videoElement.disableRemotePlayback = true;
4996
+ videoElement.controls = false;
4997
+ videoElement.muted = true;
4998
+ }
4665
4999
  this.setPublishState(exports.PublishState.INITIALIZED);
4666
5000
  }
4667
5001
  /**
@@ -4669,6 +5003,7 @@
4669
5003
  */
4670
5004
  initializeWebRTC() {
4671
5005
  var _a;
5006
+ if (this._isDestroyed) return;
4672
5007
  if (!this._stream) {
4673
5008
  this._logger.error(this, "Cannot initialize WebRTC - no camera stream available");
4674
5009
  return;
@@ -4682,35 +5017,36 @@
4682
5017
  }
4683
5018
  }
4684
5019
  /**
4685
- * Modified publish method to handle both camera and WebRTC
4686
- *
4687
- * @param streamKey - klucz streamu
4688
- * @returns {boolean} - true jeśli udało się rozpocząć publikowanie
5020
+ * Publish method
4689
5021
  */
4690
5022
  publish(streamKey) {
5023
+ if (this._isDestroyed) return false;
5024
+ if (this._debug) this._logger.decoratedLog("Publishing: " + streamKey, "dark-red");
5025
+ this._logger.info(this, "Publish: " + streamKey);
4691
5026
  if (this._statusTimer != null) clearInterval(this._statusTimer);
4692
- if (this._main.getConfigManager().getStreamData().streamKey == streamKey && this._publishState == exports.PublishState.CONNECTED) {
4693
- this._logger.warning(this, "Already published!");
4694
- return false;
5027
+ if (this._main.getConfigManager().getStreamData().streamKey != null && !this._firstPublish) {
5028
+ this.unpublish();
4695
5029
  }
4696
- if (this._main.getConfigManager().getStreamData().streamKey != null) this.unpublish();
4697
5030
  this._main.getConfigManager().getStreamData().streamKey = streamKey;
4698
5031
  if (!this.isStreamReady(true, true)) {
4699
5032
  this._logger.warning(this, "Cannot publish - stream not ready (missing video or audio track)");
4700
5033
  return false;
4701
5034
  }
4702
- if (this._debug) this._logger.decoratedLog("Publishing: " + streamKey, "dark-red");
4703
- this._logger.info(this, "Publish: " + streamKey);
4704
5035
  this.closeWebRTCConnection();
4705
5036
  this._main.dispatchEvent("publish", {
4706
5037
  ref: this._main,
4707
5038
  streamKey: streamKey
4708
5039
  });
4709
5040
  this.initializeWebRTC();
5041
+ this._firstPublish = false;
4710
5042
  return true;
4711
5043
  }
5044
+ /**
5045
+ * Stops publishing and cleans up WebRTC connection
5046
+ */
4712
5047
  unpublish() {
4713
5048
  if (this._debug) this._logger.decoratedLog("Unpublish", "dark-red");
5049
+ this._logger.info(this, "📹 [UNPUBLISH] unpublish() - stopping WebRTC but keeping camera preview");
4714
5050
  if (this._statusConnection != null) {
4715
5051
  this._statusConnection.destroy();
4716
5052
  this._statusConnection = null;
@@ -4724,40 +5060,48 @@
4724
5060
  this._main.dispatchEvent("unpublish", {
4725
5061
  ref: this._main
4726
5062
  });
4727
- this.closeStream();
5063
+ this.closeWebRTCConnection();
5064
+ this.setPublishState(exports.PublishState.UNPUBLISHED);
4728
5065
  }
4729
5066
  /**
4730
- * This method
5067
+ * Stops publishing AND releases camera/microphone
5068
+ */
5069
+ unpublishAndRelease() {
5070
+ this._logger.info(this, "📹 [UNPUBLISH+RELEASE] unpublishAndRelease() - stopping everything");
5071
+ this.unpublish();
5072
+ this.stopCameraStream();
5073
+ }
5074
+ /**
5075
+ * Sets up orientation listener
4731
5076
  * @private
4732
5077
  */
4733
5078
  setupOrientationListener() {
4734
- // Sprawdzamy czy urządzenie wspiera event orientationchange
5079
+ if (this._isDestroyed) return;
5080
+ this._orientationChangeHandler = this.handleOrientationChange;
4735
5081
  if (window.screen && window.screen.orientation) {
4736
- window.screen.orientation.addEventListener('change', this.handleOrientationChange);
5082
+ window.screen.orientation.addEventListener('change', this._orientationChangeHandler);
4737
5083
  } else {
4738
- // Fallback dla starszych urządzeń
4739
- window.addEventListener('orientationchange', this.handleOrientationChange);
5084
+ window.addEventListener('orientationchange', this._orientationChangeHandler);
4740
5085
  }
4741
5086
  }
4742
5087
  //------------------------------------------------------------------------//
4743
5088
  // USER MEDIA
4744
5089
  //------------------------------------------------------------------------//
4745
5090
  /**
4746
- * Error on trying to grab video stream (it usually means that browser does not support WebRTC streaming)
4747
- *
4748
- * @param error
5091
+ * Error handler for getUserMedia
4749
5092
  */
4750
5093
  onUserMediaError(error) {
4751
5094
  return __awaiter(this, void 0, void 0, function* () {
5095
+ if (this._isDestroyed) return;
5096
+ this._logger.error(this, `📹 [ERROR] onUserMediaError() - ${error.name}: ${error.message}`);
4752
5097
  yield this.grabDevices();
4753
- // Dodatkowa obsługa specyficznych błędów getUserMedia, jeśli potrzebna
4754
5098
  if (error.name === "OverconstrainedError") {
4755
5099
  this._logger.warning(this, "Device constraints not satisfied");
4756
5100
  }
4757
5101
  });
4758
5102
  }
4759
5103
  /**
4760
- * This method is used for checking individual access to output devices
5104
+ * Checks individual device access
4761
5105
  * @private
4762
5106
  */
4763
5107
  checkIndividualDeviceAccess() {
@@ -4772,44 +5116,47 @@
4772
5116
  available: false
4773
5117
  }
4774
5118
  };
4775
- // Sprawdzamy fizyczną dostępność urządzeń
5119
+ if (this._isDestroyed) return results;
4776
5120
  try {
4777
5121
  const devices = yield navigator.mediaDevices.enumerateDevices();
5122
+ if (this._isDestroyed) return results;
4778
5123
  results.camera.available = devices.some(device => device.kind === 'videoinput');
4779
5124
  results.microphone.available = devices.some(device => device.kind === 'audioinput');
4780
- // Sprawdzamy czy mamy etykiety urządzeń - ich brak może oznaczać brak uprawnień
4781
5125
  const hasLabels = devices.some(device => device.label !== '');
4782
5126
  if (!hasLabels) {
4783
- // Jeśli nie mamy etykiet, wymuszamy pytanie o uprawnienia
5127
+ this._logger.info(this, "📹 [ACQUIRE] checkIndividualDeviceAccess() - no labels, requesting permissions");
5128
+ if (this._isDestroyed) return results;
4784
5129
  try {
4785
5130
  const stream = yield navigator.mediaDevices.getUserMedia({
4786
5131
  video: results.camera.available,
4787
5132
  audio: results.microphone.available
4788
5133
  });
4789
- // Jeśli udało się uzyskać strumień, mamy uprawnienia
5134
+ // CRITICAL: Check IMMEDIATELY after getUserMedia
5135
+ if (this._isDestroyed) {
5136
+ this._logger.warning(this, "📹 [RELEASE] checkIndividualDeviceAccess() - destroyed, releasing orphan stream");
5137
+ stream.getTracks().forEach(track => track.stop());
5138
+ return results;
5139
+ }
4790
5140
  results.camera.allowed = stream.getVideoTracks().length > 0;
4791
5141
  results.microphone.allowed = stream.getAudioTracks().length > 0;
4792
- // Zatrzymujemy strumień testowy
4793
- stream.getTracks().forEach(track => track.stop());
4794
- // Aktualizujemy listę urządzeń po uzyskaniu uprawnień
5142
+ this._logger.info(this, "📹 [RELEASE] checkIndividualDeviceAccess() - stopping test stream");
5143
+ stream.getTracks().forEach(track => {
5144
+ track.stop();
5145
+ this._logger.info(this, `📹 [RELEASE] checkIndividualDeviceAccess() - stopped track: ${track.kind}`);
5146
+ });
5147
+ if (this._isDestroyed) return results;
4795
5148
  yield this.grabDevices();
4796
5149
  } catch (error) {
4797
5150
  console.error('Error requesting permissions:', error);
4798
- // Nie udało się uzyskać uprawnień
4799
5151
  results.camera.allowed = false;
4800
5152
  results.microphone.allowed = false;
4801
5153
  }
4802
5154
  } else {
4803
- // Jeśli mamy etykiety, prawdopodobnie mamy już uprawnienia
4804
5155
  results.camera.allowed = devices.some(device => device.kind === 'videoinput' && device.label !== '');
4805
5156
  results.microphone.allowed = devices.some(device => device.kind === 'audioinput' && device.label !== '');
4806
5157
  }
4807
5158
  } catch (error) {
4808
5159
  console.error('Error checking devices:', error);
4809
- results.camera.available = false;
4810
- results.microphone.available = false;
4811
- results.camera.allowed = false;
4812
- results.microphone.allowed = false;
4813
5160
  }
4814
5161
  return results;
4815
5162
  });
@@ -4818,21 +5165,18 @@
4818
5165
  // SOCKETS & SDP
4819
5166
  //------------------------------------------------------------------------//
4820
5167
  /**
4821
- * This method handles basic SDP/ICE-Candidate exchange with a Wowza Server
4822
- *
4823
- * @param data
5168
+ * Handles SDP/ICE-Candidate exchange
4824
5169
  */
4825
5170
  onSocketMessage(data) {
4826
5171
  var _a;
5172
+ if (this._isDestroyed) return;
4827
5173
  let msgJSON = JSON.parse(data);
4828
5174
  let msgStatus = Number(msgJSON["status"]);
4829
5175
  switch (msgStatus) {
4830
5176
  case 200:
4831
- // OK
4832
5177
  this._logger.info(this, "SDP Exchange Successful");
4833
5178
  let sdpData = msgJSON['sdp'];
4834
- if (sdpData !== undefined) {
4835
- // @ts-ignore
5179
+ if (sdpData !== undefined && this._peerConnection) {
4836
5180
  this._peerConnection.setRemoteDescription(new RTCSessionDescription(sdpData), () => {}, () => {});
4837
5181
  }
4838
5182
  let iceCandidates = msgJSON['iceCandidates'];
@@ -4843,7 +5187,6 @@
4843
5187
  }
4844
5188
  break;
4845
5189
  case 503:
4846
- // NOT OK
4847
5190
  this._logger.error(this, "StreamKey already use");
4848
5191
  const usedStreamKey = (_a = this._main.getConfigManager().getStreamData().streamKey) !== null && _a !== void 0 ? _a : "unknown";
4849
5192
  this._main.dispatchEvent("streamKeyInUse", {
@@ -4857,14 +5200,8 @@
4857
5200
  //------------------------------------------------------------------------//
4858
5201
  // EVENTS
4859
5202
  //------------------------------------------------------------------------//
4860
- /**
4861
- * Recives events related to peerConnection (change of state)
4862
- *
4863
- * @param event event with its data
4864
- * @param thisRef reference to player classonConnectionStateChange
4865
- * @private
4866
- */
4867
5203
  onConnectionStateChange(event) {
5204
+ if (this._isDestroyed) return;
4868
5205
  this._logger.info(this, "Connection State Change: " + JSON.stringify(event));
4869
5206
  if (event !== null) {
4870
5207
  switch (event.currentTarget.connectionState) {
@@ -4895,16 +5232,16 @@
4895
5232
  // DEVICES
4896
5233
  //------------------------------------------------------------------------//
4897
5234
  /**
4898
- * Returns list od devices (cameras, microphones) available for user's device
5235
+ * Grabs available devices
4899
5236
  */
4900
5237
  grabDevices() {
4901
5238
  return __awaiter(this, void 0, void 0, function* () {
5239
+ if (this._isDestroyed) return;
4902
5240
  try {
4903
5241
  const deviceAccess = yield this.checkIndividualDeviceAccess();
5242
+ if (this._isDestroyed) return;
4904
5243
  this._cameraList = new InputDeviceList();
4905
5244
  this._microphoneList = new InputDeviceList();
4906
- // Wysyłamy eventy tylko jeśli nie sprawdzaliśmy wcześniej uprawnień
4907
- // lub jeśli zmienił się stan uprawnień (resetowana flaga)
4908
5245
  if (!this._permissionChecked) {
4909
5246
  if (!deviceAccess.camera.allowed) {
4910
5247
  this._main.dispatchEvent("cameraAccessDenied", {
@@ -4933,8 +5270,9 @@
4933
5270
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4934
5271
  }
4935
5272
  }
4936
- // Wypełnianie list dostępnymi urządzeniami
5273
+ if (this._isDestroyed) return;
4937
5274
  const devices = yield navigator.mediaDevices.enumerateDevices();
5275
+ if (this._isDestroyed) return;
4938
5276
  for (const device of devices) {
4939
5277
  if (device.deviceId && device.label) {
4940
5278
  if (device.kind === 'videoinput' && deviceAccess.camera.allowed) {
@@ -4946,8 +5284,8 @@
4946
5284
  }
4947
5285
  }
4948
5286
  }
5287
+ if (this._isDestroyed) return;
4949
5288
  try {
4950
- // Aktualizacja wybranych urządzeń
4951
5289
  if (deviceAccess.camera.allowed) {
4952
5290
  this._selectedCamera = this.pickCamera();
4953
5291
  }
@@ -4955,153 +5293,217 @@
4955
5293
  this._selectedMicrophone = this.pickMicrophone();
4956
5294
  }
4957
5295
  } catch (error) {
5296
+ console.log(error);
4958
5297
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4959
- this._logger.error(this, "Errror on grab devices: " + JSON.stringify(error));
5298
+ this._logger.error(this, "Error on grab devices: " + JSON.stringify(error));
5299
+ }
5300
+ if (!this._isDestroyed) {
5301
+ this._main.dispatchEvent("deviceListUpdate", {
5302
+ ref: this._main,
5303
+ cameraList: this._cameraList.getArray(),
5304
+ microphoneList: this._microphoneList.getArray()
5305
+ });
4960
5306
  }
4961
- // Zawsze wysyłamy aktualizację list urządzeń
4962
- this._main.dispatchEvent("deviceListUpdate", {
4963
- ref: this._main,
4964
- cameraList: this._cameraList.getArray(),
4965
- microphoneList: this._microphoneList.getArray()
4966
- });
4967
5307
  this._permissionChecked = true;
4968
5308
  } catch (error) {
5309
+ if (this._isDestroyed) return;
4969
5310
  console.error("Error in grabDevices:", error);
4970
5311
  this._cameraList = new InputDeviceList();
4971
5312
  this._microphoneList = new InputDeviceList();
5313
+ if (!this._isDestroyed) {
5314
+ this._main.dispatchEvent("deviceListUpdate", {
5315
+ ref: this._main,
5316
+ cameraList: this._cameraList.getArray(),
5317
+ microphoneList: this._microphoneList.getArray()
5318
+ });
5319
+ this._main.dispatchEvent("inputDeviceError", {
5320
+ ref: this._main
5321
+ });
5322
+ }
5323
+ }
5324
+ });
5325
+ }
5326
+ /**
5327
+ * Selects camera by ID
5328
+ */
5329
+ selectCamera(cameraID) {
5330
+ var _a, _b, _c;
5331
+ return __awaiter(this, void 0, void 0, function* () {
5332
+ if (this._isDestroyed) return;
5333
+ if (this._cameraAbortController) {
5334
+ this._cameraAbortController.abort();
5335
+ }
5336
+ this._cameraAbortController = new AbortController();
5337
+ const signal = this._cameraAbortController.signal;
5338
+ try {
5339
+ this._switchingCamera = true;
5340
+ this._logger.info(this, `📹 [SWITCH] selectCamera() - switching to camera: ${cameraID}`);
5341
+ for (let i = 0; i < this._cameraList.getSize(); i++) {
5342
+ this._cameraList.get(i).isSelected = false;
5343
+ }
5344
+ this._selectedCamera = null;
5345
+ this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5346
+ this.setCameraState(exports.DeviceState.NOT_INITIALIZED);
5347
+ const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5348
+ const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5349
+ let found = false;
5350
+ for (let i = 0; i < this._cameraList.getSize(); i++) {
5351
+ if (this._cameraList.get(i).id == cameraID) {
5352
+ this._selectedCamera = this._cameraList.get(i);
5353
+ this._selectedCamera.isSelected = true;
5354
+ (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("cameraID", this._selectedCamera.id);
5355
+ found = true;
5356
+ break;
5357
+ }
5358
+ }
4972
5359
  this._main.dispatchEvent("deviceListUpdate", {
4973
5360
  ref: this._main,
4974
5361
  cameraList: this._cameraList.getArray(),
4975
5362
  microphoneList: this._microphoneList.getArray()
4976
5363
  });
4977
- this._main.dispatchEvent("inputDeviceError", {
4978
- ref: this._main
4979
- });
5364
+ if (signal.aborted || this._isDestroyed) return;
5365
+ this.stopCameraStream();
5366
+ if (this._selectedCamera != null) {
5367
+ this._constraints.video.deviceId = this._selectedCamera.id;
5368
+ if (signal.aborted || this._isDestroyed) return;
5369
+ yield new Promise((resolve, reject) => {
5370
+ const timeout = setTimeout(resolve, 500);
5371
+ signal.addEventListener('abort', () => {
5372
+ clearTimeout(timeout);
5373
+ reject(new Error('Aborted'));
5374
+ });
5375
+ });
5376
+ if (signal.aborted || this._isDestroyed) return;
5377
+ yield this.startCamera();
5378
+ if (this._isDestroyed) return;
5379
+ this.setCameraState(exports.DeviceState.ENABLED);
5380
+ if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) this.setInputDeviceState(exports.InputDevicesState.READY);else this.setInputDeviceState(exports.InputDevicesState.INVALID);
5381
+ if (wasPublished && streamKey && !signal.aborted && !this._isDestroyed) {
5382
+ this.publish(streamKey);
5383
+ }
5384
+ } else {
5385
+ this.setInputDeviceState(exports.InputDevicesState.INVALID);
5386
+ }
5387
+ } catch (error) {
5388
+ if (error.message !== 'Aborted' && !this._isDestroyed) {
5389
+ this._logger.error(this, '📹 [ERROR] selectCamera() - Error switching camera: ' + error);
5390
+ this.setInputDeviceState(exports.InputDevicesState.INVALID);
5391
+ }
5392
+ } finally {
5393
+ this._switchingCamera = false;
5394
+ if (((_c = this._cameraAbortController) === null || _c === void 0 ? void 0 : _c.signal) === signal) {
5395
+ this._cameraAbortController = null;
5396
+ }
4980
5397
  }
4981
5398
  });
4982
5399
  }
4983
5400
  /**
4984
- * Selects camera based on camera device ID;
4985
- * @param cameraID
5401
+ * Selects microphone by ID
4986
5402
  */
4987
- selectCamera(cameraID) {
4988
- var _a, _b;
4989
- for (let i = 0; i < this._cameraList.getSize(); i++) {
4990
- this._cameraList.get(i).isSelected = false;
4991
- }
4992
- this._selectedCamera = null;
4993
- this.setInputDeviceState(exports.InputDevicesState.UPDATING);
4994
- this.setCameraState(exports.DeviceState.NOT_INITIALIZED);
4995
- // Zapamiętaj aktualny stream key i stan publikacji
4996
- const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
4997
- const wasPublished = this._publishState === exports.PublishState.CONNECTED;
4998
- for (let i = 0; i < this._cameraList.getSize(); i++) {
4999
- if (this._cameraList.get(i).id == cameraID) {
5000
- this._selectedCamera = this._cameraList.get(i);
5001
- this._selectedCamera.isSelected = true;
5002
- (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("cameraID", this._selectedCamera.id);
5003
- break;
5403
+ selectMicrophone(micID) {
5404
+ var _a, _b, _c;
5405
+ return __awaiter(this, void 0, void 0, function* () {
5406
+ if (this._isDestroyed) return;
5407
+ if (this._microphoneAbortController) {
5408
+ this._microphoneAbortController.abort();
5004
5409
  }
5005
- }
5006
- this._main.dispatchEvent("deviceListUpdate", {
5007
- ref: this._main,
5008
- cameraList: this._cameraList.getArray(),
5009
- microphoneList: this._microphoneList.getArray()
5010
- });
5011
- this.stopCameraStream();
5012
- if (this._selectedCamera != null) {
5013
- // Update constraints with new device
5014
- this._constraints.video.deviceId = this._selectedCamera.id;
5015
- // Restart camera stream
5016
- this.startCamera().then(() => {
5017
- // Jeśli stream był opublikowany, publikujemy ponownie
5018
- this.setCameraState(exports.DeviceState.ENABLED);
5019
- if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) this.setInputDeviceState(exports.InputDevicesState.READY);else this.setInputDeviceState(exports.InputDevicesState.INVALID);
5020
- if (wasPublished && streamKey) {
5021
- this.publish(streamKey);
5410
+ this._microphoneAbortController = new AbortController();
5411
+ const signal = this._microphoneAbortController.signal;
5412
+ try {
5413
+ this._switchingMicrophone = true;
5414
+ this._logger.info(this, `📹 [SWITCH] selectMicrophone() - switching to microphone: ${micID}`);
5415
+ for (let i = 0; i < this._microphoneList.getSize(); i++) {
5416
+ this._microphoneList.get(i).isSelected = false;
5022
5417
  }
5023
- });
5024
- } else {
5025
- this.setInputDeviceState(exports.InputDevicesState.INVALID);
5026
- }
5027
- }
5028
- /**
5029
- * Method tries to select (change) microphone based on its system ID
5030
- * @param micID
5031
- */
5032
- selectMicrophone(micID) {
5033
- var _a, _b;
5034
- return __awaiter(this, void 0, void 0, function* () {
5035
- for (let i = 0; i < this._microphoneList.getSize(); i++) {
5036
- this._microphoneList.get(i).isSelected = false;
5037
- }
5038
- this._selectedMicrophone = null;
5039
- this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5040
- this.setMicrophoneState(exports.DeviceState.NOT_INITIALIZED);
5041
- this._logger.info(this, "Selecting microphone: " + micID);
5042
- // Zapamiętaj aktualny stream key i stan publikacji
5043
- const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5044
- const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5045
- // Znajdź i zapisz wybrany mikrofon
5046
- for (let i = 0; i < this._microphoneList.getSize(); i++) {
5047
- if (this._microphoneList.get(i).id == micID) {
5048
- this._selectedMicrophone = this._microphoneList.get(i);
5049
- this._selectedMicrophone.isSelected = true;
5050
- (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("microphoneID", this._selectedMicrophone.id);
5051
- break;
5418
+ this._selectedMicrophone = null;
5419
+ this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5420
+ this.setMicrophoneState(exports.DeviceState.NOT_INITIALIZED);
5421
+ const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5422
+ const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5423
+ for (let i = 0; i < this._microphoneList.getSize(); i++) {
5424
+ if (this._microphoneList.get(i).id == micID) {
5425
+ this._selectedMicrophone = this._microphoneList.get(i);
5426
+ this._selectedMicrophone.isSelected = true;
5427
+ (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("microphoneID", this._selectedMicrophone.id);
5428
+ break;
5429
+ }
5052
5430
  }
5053
- }
5054
- // Zawsze wysyłamy aktualizację list urządzeń
5055
- this._main.dispatchEvent("deviceListUpdate", {
5056
- ref: this._main,
5057
- cameraList: this._cameraList.getArray(),
5058
- microphoneList: this._microphoneList.getArray()
5059
- });
5060
- // Odłącz SoundMeter przed zmianą strumienia
5061
- this._soundMeter.detach();
5062
- // Zamknij istniejące połączenie WebRTC
5063
- this.closeWebRTCConnection();
5064
- // Zatrzymaj obecny strumień
5065
- if (this._stream) {
5066
- this._stream.getTracks().forEach(track => {
5067
- track.stop();
5431
+ this._main.dispatchEvent("deviceListUpdate", {
5432
+ ref: this._main,
5433
+ cameraList: this._cameraList.getArray(),
5434
+ microphoneList: this._microphoneList.getArray()
5068
5435
  });
5069
- this._stream = null;
5070
- }
5071
- try {
5072
- // Rozpocznij wszystko od nowa
5436
+ if (signal.aborted || this._isDestroyed) return;
5437
+ this._soundMeter.detach();
5438
+ this.closeWebRTCConnection();
5439
+ if (this._stream) {
5440
+ this._logger.info(this, "📹 [RELEASE] selectMicrophone() - stopping current stream");
5441
+ this._stream.getTracks().forEach(track => {
5442
+ track.stop();
5443
+ });
5444
+ this._stream = null;
5445
+ this._activeStreamCount--;
5446
+ }
5447
+ if (signal.aborted || this._isDestroyed) return;
5448
+ yield new Promise((resolve, reject) => {
5449
+ const timeout = setTimeout(resolve, 500);
5450
+ signal.addEventListener('abort', () => {
5451
+ clearTimeout(timeout);
5452
+ reject(new Error('Aborted'));
5453
+ });
5454
+ });
5455
+ if (signal.aborted || this._isDestroyed) return;
5073
5456
  yield this.startCamera();
5457
+ if (this._isDestroyed) return;
5074
5458
  this.setMicrophoneState(exports.DeviceState.ENABLED);
5075
5459
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) {
5076
5460
  this.setInputDeviceState(exports.InputDevicesState.READY);
5077
5461
  } else {
5078
5462
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5079
5463
  }
5080
- // Jeśli stream był opublikowany, publikujemy ponownie
5081
- if (wasPublished && streamKey) {
5464
+ if (wasPublished && streamKey && !signal.aborted && !this._isDestroyed) {
5082
5465
  this.publish(streamKey);
5083
5466
  }
5084
5467
  } catch (error) {
5085
- console.error("Error changing microphone:", error);
5086
- this._main.dispatchEvent("inputDeviceError", {
5087
- ref: this._main
5088
- });
5089
- this.setInputDeviceState(exports.InputDevicesState.INVALID);
5468
+ if (error.message !== 'Aborted' && !this._isDestroyed) {
5469
+ console.error("Error changing microphone:", error);
5470
+ this._main.dispatchEvent("inputDeviceError", {
5471
+ ref: this._main
5472
+ });
5473
+ this.setInputDeviceState(exports.InputDevicesState.INVALID);
5474
+ }
5475
+ } finally {
5476
+ this._switchingMicrophone = false;
5477
+ if (((_c = this._microphoneAbortController) === null || _c === void 0 ? void 0 : _c.signal) === signal) {
5478
+ this._microphoneAbortController = null;
5479
+ }
5090
5480
  }
5091
5481
  });
5092
5482
  }
5093
5483
  /**
5094
- * This method tries to start a camera.
5095
- *
5484
+ * Starts camera with abort support
5096
5485
  * @private
5097
5486
  */
5098
5487
  startCamera() {
5488
+ var _a;
5099
5489
  return __awaiter(this, void 0, void 0, function* () {
5490
+ if (this._isDestroyed) {
5491
+ this._logger.warning(this, "📹 [ACQUIRE] startCamera() - aborted, instance is destroyed");
5492
+ return;
5493
+ }
5494
+ if (this._startCameraAbortController) {
5495
+ this._startCameraAbortController.abort();
5496
+ }
5497
+ this._startCameraAbortController = new AbortController();
5498
+ const signal = this._startCameraAbortController.signal;
5499
+ // Release existing stream first
5100
5500
  if (this._stream) {
5501
+ this._logger.info(this, "📹 [RELEASE] startCamera() - releasing existing stream before acquiring new one");
5101
5502
  this._stream.getTracks().forEach(track => {
5102
5503
  track.stop();
5103
5504
  });
5104
5505
  this._stream = null;
5506
+ this._activeStreamCount--;
5105
5507
  }
5106
5508
  try {
5107
5509
  const constraints = {
@@ -5116,11 +5518,24 @@
5116
5518
  }
5117
5519
  } : false
5118
5520
  };
5521
+ if (signal.aborted || this._isDestroyed) return;
5522
+ this._logger.info(this, `📹 [ACQUIRE] startCamera() - requesting getUserMedia`);
5119
5523
  try {
5120
5524
  const stream = yield navigator.mediaDevices.getUserMedia(constraints);
5525
+ // CRITICAL: Check IMMEDIATELY after getUserMedia returns
5526
+ if (signal.aborted || this._isDestroyed) {
5527
+ this._logger.warning(this, "📹 [RELEASE] startCamera() - destroyed during getUserMedia, releasing orphan stream");
5528
+ stream.getTracks().forEach(track => {
5529
+ track.stop();
5530
+ this._logger.info(this, `📹 [RELEASE] startCamera() - stopped orphan track: ${track.kind}, id: ${track.id}`);
5531
+ });
5532
+ return;
5533
+ }
5121
5534
  this._stream = stream;
5122
5535
  this.onCameraStreamSuccess(this._stream);
5123
5536
  } catch (error) {
5537
+ if (signal.aborted || this._isDestroyed) return;
5538
+ this._logger.error(this, `📹 [ERROR] startCamera() - getUserMedia failed: ${error.name}: ${error.message}`);
5124
5539
  if (constraints.video) {
5125
5540
  this.onUserMediaError({
5126
5541
  name: error.name || 'Error',
@@ -5138,8 +5553,13 @@
5138
5553
  }
5139
5554
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) this.setInputDeviceState(exports.InputDevicesState.READY);
5140
5555
  } catch (error) {
5556
+ if (this._isDestroyed) return;
5141
5557
  console.error("Error in startCamera:", error);
5142
5558
  yield this.grabDevices();
5559
+ } finally {
5560
+ if (((_a = this._startCameraAbortController) === null || _a === void 0 ? void 0 : _a.signal) === signal) {
5561
+ this._startCameraAbortController = null;
5562
+ }
5143
5563
  }
5144
5564
  });
5145
5565
  }
@@ -5147,15 +5567,14 @@
5147
5567
  * Updates WebRTC connection with new stream
5148
5568
  */
5149
5569
  updateWebRTCStream() {
5570
+ if (this._isDestroyed) return;
5150
5571
  if (!this._peerConnection || !this._stream) {
5151
5572
  return;
5152
5573
  }
5153
- // Remove all existing tracks from the peer connection
5154
5574
  const senders = this._peerConnection.getSenders();
5155
5575
  senders.forEach(sender => {
5156
5576
  if (this._peerConnection) this._peerConnection.removeTrack(sender);
5157
5577
  });
5158
- // Add new tracks
5159
5578
  this._stream.getTracks().forEach(track => {
5160
5579
  if (this._stream != null && this._peerConnection) {
5161
5580
  this._peerConnection.addTrack(track, this._stream);
@@ -5163,33 +5582,24 @@
5163
5582
  });
5164
5583
  }
5165
5584
  /**
5166
- * Modified closeStream to handle both camera and WebRTC completely
5167
- */
5168
- closeStream() {
5169
- if (this._peerConnection !== undefined && this._peerConnection !== null) this._peerConnection.close();
5170
- this.setPublishState(exports.PublishState.UNPUBLISHED);
5171
- }
5172
- /**
5173
- * This method selects a camera based on previous uses or saved IDs
5174
- *
5585
+ * Picks camera based on saved ID or defaults
5175
5586
  * @private
5176
5587
  */
5177
5588
  pickCamera() {
5178
5589
  var _a, _b, _c, _d, _e, _f;
5590
+ if (this._isDestroyed) return null;
5179
5591
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5180
5592
  this._cameraList.get(i).isSelected = false;
5181
5593
  }
5182
5594
  let savedCameraID = (_b = (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.getField("cameraID")) !== null && _b !== void 0 ? _b : null;
5183
5595
  if (this._cameraList.getSize() > 0) {
5184
5596
  if (savedCameraID) {
5185
- // Szukamy zapisanej kamery
5186
5597
  let found = false;
5187
5598
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5188
5599
  if (this._cameraList.get(i).id === savedCameraID) {
5189
5600
  this._selectedCamera = this._cameraList.get(i);
5190
5601
  this._selectedCamera.isSelected = true;
5191
5602
  this.setCameraState(exports.DeviceState.ENABLED);
5192
- // Ustaw deviceId w constraints
5193
5603
  found = true;
5194
5604
  this._constraints.video.deviceId = this._selectedCamera.id;
5195
5605
  break;
@@ -5206,10 +5616,8 @@
5206
5616
  this._logger.info(this, "Canceling Publish!");
5207
5617
  this._main.getConfigManager().getStreamData().streamKey = null;
5208
5618
  }
5209
- return null;
5210
5619
  }
5211
5620
  }
5212
- // Jeśli nie znaleziono zapisanej kamery, używamy pierwszej
5213
5621
  if (!this._selectedCamera) {
5214
5622
  this._main.dispatchEvent("savedCameraNotFound", {
5215
5623
  ref: this._main,
@@ -5232,20 +5640,22 @@
5232
5640
  }
5233
5641
  }
5234
5642
  }
5235
- this._main.dispatchEvent("deviceListUpdate", {
5236
- ref: this._main,
5237
- cameraList: this._cameraList.getArray(),
5238
- microphoneList: this._microphoneList.getArray()
5239
- });
5643
+ if (!this._isDestroyed) {
5644
+ this._main.dispatchEvent("deviceListUpdate", {
5645
+ ref: this._main,
5646
+ cameraList: this._cameraList.getArray(),
5647
+ microphoneList: this._microphoneList.getArray()
5648
+ });
5649
+ }
5240
5650
  return this._selectedCamera;
5241
5651
  }
5242
5652
  /**
5243
- * This method selects a microphone based on previous uses or saved IDs
5244
- *
5653
+ * Picks microphone based on saved ID or defaults
5245
5654
  * @private
5246
5655
  */
5247
5656
  pickMicrophone() {
5248
5657
  var _a, _b, _c, _d, _e, _f;
5658
+ if (this._isDestroyed) return null;
5249
5659
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5250
5660
  this._microphoneList.get(i).isSelected = false;
5251
5661
  }
@@ -5272,7 +5682,6 @@
5272
5682
  this._logger.info(this, "Canceling Publish!");
5273
5683
  this._main.getConfigManager().getStreamData().streamKey = null;
5274
5684
  }
5275
- return null;
5276
5685
  }
5277
5686
  }
5278
5687
  if (!this._selectedMicrophone) {
@@ -5301,75 +5710,43 @@
5301
5710
  }
5302
5711
  return this._selectedMicrophone;
5303
5712
  }
5304
- /**
5305
- * Cleans all saved cameras and microphones IDs.
5306
- */
5307
5713
  clearSavedDevices() {
5308
5714
  var _a, _b;
5309
5715
  (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.removeField("cameraID");
5310
5716
  (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.removeField("microphoneID");
5311
5717
  }
5312
- /**
5313
- * Messes up camera's and microphone's id (for testing only)
5314
- */
5315
5718
  messSavedDevices() {
5316
5719
  var _a, _b;
5317
5720
  (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.saveField("cameraID", "a");
5318
5721
  (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("microphoneID", "b");
5319
5722
  }
5320
- /**
5321
- * Handles microphone muting state
5322
- * @param microphoneState true to unmute, false to mute
5323
- */
5324
5723
  muteMicrophone(shouldMute) {
5724
+ var _a;
5725
+ if (this._isDestroyed) return;
5325
5726
  if (this._isMicrophoneMuted === shouldMute) {
5326
- // State hasn't changed, no need to do anything
5327
5727
  return;
5328
5728
  }
5329
5729
  this._isMicrophoneMuted = shouldMute;
5730
+ (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.saveField("microphoneMuted", shouldMute ? "true" : "false");
5330
5731
  if (this._stream) {
5331
- this.applyMicrophoneState(!shouldMute); // Odwracamy wartość dla track.enabled
5732
+ this.applyMicrophoneState(!shouldMute);
5332
5733
  } else {
5333
- // Store the desired state to be applied when stream becomes available
5334
- this._pendingMicrophoneState = !shouldMute; // Odwracamy wartość dla przyszłego track.enabled
5335
- this._logger.info(this, `WebRTCStreamer :: Stream not yet available, storing microphone state (muted: ${shouldMute})`);
5734
+ this._pendingMicrophoneState = !shouldMute;
5336
5735
  }
5337
- // Always dispatch the event to keep UI in sync
5338
5736
  this._main.dispatchEvent("microphoneStateChange", {
5339
5737
  ref: this._main,
5340
5738
  isMuted: this._isMicrophoneMuted
5341
5739
  });
5342
5740
  }
5343
- /**
5344
- * Applies the microphone state to the actual stream tracks
5345
- *
5346
- * @param enabled true to enable tracks, false to disable
5347
- * @private
5348
- */
5349
5741
  applyMicrophoneState(enabled) {
5350
- if (!this._stream) {
5351
- this._logger.warning(this, "WebRTCStreamer :: Cannot apply microphone state - stream not available");
5352
- return;
5353
- }
5742
+ if (!this._stream) return;
5354
5743
  const audioTracks = this._stream.getAudioTracks();
5355
5744
  if (audioTracks && audioTracks.length > 0) {
5356
- this._logger.success(this, `WebRTCStreamer :: ${enabled ? 'Unmuting' : 'Muting'} microphone`);
5357
5745
  audioTracks.forEach(track => track.enabled = enabled);
5358
- } else {
5359
- this._logger.warning(this, "WebRTCStreamer :: No audio tracks found in stream");
5360
5746
  }
5361
5747
  }
5362
- /**
5363
- * This methods is a final check whenever we're ready to publish a stream
5364
- *
5365
- * @param requireVideo - whenever video track is required
5366
- * @param requireAudio - whenever audio track is required
5367
- * @returns {boolean} true if stream is ready for publishing
5368
- */
5369
5748
  isStreamReady(requireVideo = true, requireAudio = true) {
5370
- if (!this._stream) {
5371
- return false;
5372
- }
5749
+ if (!this._stream) return false;
5373
5750
  const videoTracks = this._stream.getVideoTracks();
5374
5751
  const audioTracks = this._stream.getAudioTracks();
5375
5752
  const videoReady = !requireVideo || videoTracks.length > 0 && videoTracks[0].readyState === 'live';
@@ -5378,7 +5755,15 @@
5378
5755
  }
5379
5756
  closeWebRTCConnection() {
5380
5757
  if (this._peerConnection) {
5381
- this._peerConnection.close();
5758
+ this._logger.info(this, "📡 [WEBRTC] closeWebRTCConnection() - closing peer connection");
5759
+ this._peerConnection.onicecandidate = null;
5760
+ this._peerConnection.onconnectionstatechange = null;
5761
+ this._peerConnection.onnegotiationneeded = null;
5762
+ try {
5763
+ this._peerConnection.close();
5764
+ } catch (e) {
5765
+ // Ignore
5766
+ }
5382
5767
  this._peerConnection = null;
5383
5768
  }
5384
5769
  }
@@ -5393,6 +5778,7 @@
5393
5778
  //------------------------------------------------------------------------//
5394
5779
  createStatusConnection() {
5395
5780
  var _a, _b, _c;
5781
+ if (this._isDestroyed) return;
5396
5782
  const serverItem = (_c = (_b = (_a = this._main) === null || _a === void 0 ? void 0 : _a.getNetworkController()) === null || _b === void 0 ? void 0 : _b.getConnection()) === null || _c === void 0 ? void 0 : _c.getCurrentServer();
5397
5783
  if (!serverItem) return;
5398
5784
  if (this._statusConnection) {
@@ -5413,6 +5799,7 @@
5413
5799
  }
5414
5800
  setPublishState(newState) {
5415
5801
  if (this._publishState == newState) return;
5802
+ if (this._isDestroyed) return;
5416
5803
  if (this._debug) this._logger.decoratedLog("Publish State: " + newState, "dark-blue");
5417
5804
  this._logger.info(this, "Publish State: " + newState);
5418
5805
  if (newState == exports.PublishState.PUBLISHED) this._publishTime = new Date().getTime();
@@ -5425,9 +5812,9 @@
5425
5812
  getPublishTime() {
5426
5813
  return this._publishState == exports.PublishState.PUBLISHED ? this._publishTime : 0;
5427
5814
  }
5428
- // DEVICE STATE
5429
5815
  setInputDeviceState(newState) {
5430
5816
  if (this._inputDeviceState == newState) return;
5817
+ if (this._isDestroyed) return;
5431
5818
  this._inputDeviceState = newState;
5432
5819
  this._main.dispatchEvent("deviceStateChange", {
5433
5820
  ref: this._main,
@@ -5439,9 +5826,9 @@
5439
5826
  getInputDeviceState() {
5440
5827
  return this._inputDeviceState;
5441
5828
  }
5442
- // CAMERA STATE
5443
5829
  setCameraState(newState) {
5444
5830
  if (this._cameraState == newState) return;
5831
+ if (this._isDestroyed) return;
5445
5832
  this._cameraState = newState;
5446
5833
  this._main.dispatchEvent("cameraDeviceStateChange", {
5447
5834
  ref: this._main,
@@ -5452,9 +5839,9 @@
5452
5839
  getCameraState() {
5453
5840
  return this._cameraState;
5454
5841
  }
5455
- // MICROPHONE STATE
5456
5842
  setMicrophoneState(newState) {
5457
5843
  if (this._microphoneState == newState) return;
5844
+ if (this._isDestroyed) return;
5458
5845
  this._microphoneState = newState;
5459
5846
  this._main.dispatchEvent("microphoneDeviceStateChange", {
5460
5847
  ref: this._main,
@@ -5475,130 +5862,82 @@
5475
5862
  return this._publishState;
5476
5863
  }
5477
5864
  //------------------------------------------------------------------------//
5865
+ // DEBUG
5866
+ //------------------------------------------------------------------------//
5867
+ debugMediaState() {
5868
+ var _a, _b;
5869
+ console.group("🎥 Media Debug State");
5870
+ console.log("=== STREAM INFO ===");
5871
+ console.log("this._stream:", this._stream);
5872
+ console.log("Active stream count:", this._activeStreamCount);
5873
+ console.log("Is destroyed:", this._isDestroyed);
5874
+ if (this._stream) {
5875
+ console.log("Stream ID:", this._stream.id);
5876
+ console.log("Stream active:", this._stream.active);
5877
+ console.log("--- Video Tracks ---");
5878
+ this._stream.getVideoTracks().forEach((track, index) => {
5879
+ console.log(` Track ${index}:`, {
5880
+ id: track.id,
5881
+ enabled: track.enabled,
5882
+ readyState: track.readyState,
5883
+ muted: track.muted
5884
+ });
5885
+ });
5886
+ console.log("--- Audio Tracks ---");
5887
+ this._stream.getAudioTracks().forEach((track, index) => {
5888
+ console.log(` Track ${index}:`, {
5889
+ id: track.id,
5890
+ enabled: track.enabled,
5891
+ readyState: track.readyState,
5892
+ muted: track.muted
5893
+ });
5894
+ });
5895
+ }
5896
+ console.log("=== VIDEO ELEMENT ===");
5897
+ const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5898
+ if ((videoElement === null || videoElement === void 0 ? void 0 : videoElement.srcObject) instanceof MediaStream) {
5899
+ const srcStream = videoElement.srcObject;
5900
+ console.log("Video srcObject stream ID:", srcStream.id);
5901
+ console.log("Video srcObject active:", srcStream.active);
5902
+ console.log("Same as this._stream:", srcStream === this._stream);
5903
+ }
5904
+ console.groupEnd();
5905
+ }
5906
+ //------------------------------------------------------------------------//
5478
5907
  // DESTROY & DELETE
5479
5908
  //------------------------------------------------------------------------//
5480
- /**
5481
- * Method used to stop camera from streaming
5482
- * @private
5483
- */
5484
5909
  stopCameraStream() {
5485
5910
  var _a, _b;
5486
5911
  if (this._stream) {
5487
- this._stream.getTracks().forEach(track => track.stop());
5912
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - stopping stream, id: ${this._stream.id}`);
5913
+ this._stream.getTracks().forEach(track => {
5914
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - stopping track: ${track.kind}, id: ${track.id}`);
5915
+ track.stop();
5916
+ });
5488
5917
  const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5489
5918
  if (videoElement) {
5490
5919
  videoElement.srcObject = null;
5491
5920
  }
5492
5921
  this._soundMeter.detach();
5493
5922
  this._stream = null;
5923
+ this._activeStreamCount--;
5924
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - complete, active streams: ${this._activeStreamCount}`);
5494
5925
  }
5495
5926
  }
5496
- /**
5497
- * Method stops streaming for all streams
5498
- * @private
5499
- */
5500
- forceStopAllStreams() {
5501
- var _a, _b;
5502
- // 1. First, detach the sound meter
5503
- if (this._soundMeter) {
5504
- this._soundMeter.detach();
5927
+ stop() {
5928
+ this._logger.info(this, "📹 [STOP] stop() - stopping all operations");
5929
+ if (this._cameraAbortController) {
5930
+ this._cameraAbortController.abort();
5931
+ this._cameraAbortController = null;
5505
5932
  }
5506
- // 2. Stop the main stream if it exists
5507
- if (this._stream) {
5508
- try {
5509
- const tracks = this._stream.getTracks();
5510
- this._logger.info(this, `Stopping ${tracks.length} tracks from main stream`);
5511
- tracks.forEach(track => {
5512
- try {
5513
- track.enabled = false;
5514
- track.stop();
5515
- this._logger.info(this, `Stopped ${track.kind} track: ${track.id}`);
5516
- } catch (e) {
5517
- this._logger.error(this, `Error stopping ${track.kind} track: ${e}`);
5518
- }
5519
- });
5520
- this._stream = null;
5521
- } catch (e) {
5522
- this._logger.error(this, 'Error stopping main stream');
5523
- }
5933
+ if (this._microphoneAbortController) {
5934
+ this._microphoneAbortController.abort();
5935
+ this._microphoneAbortController = null;
5524
5936
  }
5525
- // 3. Clean up video element
5526
- try {
5527
- const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5528
- if (videoElement && videoElement.srcObject instanceof MediaStream) {
5529
- const videoTracks = videoElement.srcObject.getTracks();
5530
- if (videoTracks.length > 0) {
5531
- this._logger.info(this, `Stopping ${videoTracks.length} tracks from video element`);
5532
- videoTracks.forEach(track => {
5533
- try {
5534
- track.enabled = false;
5535
- track.stop();
5536
- } catch (e) {
5537
- this._logger.error(this, `Error stopping video element track: ${e}`);
5538
- }
5539
- });
5540
- }
5541
- videoElement.srcObject = null;
5542
- videoElement.removeAttribute('src');
5543
- videoElement.load();
5544
- }
5545
- } catch (e) {
5546
- this._logger.error(this, 'Error cleaning video element');
5937
+ if (this._startCameraAbortController) {
5938
+ this._startCameraAbortController.abort();
5939
+ this._startCameraAbortController = null;
5547
5940
  }
5548
- // 4. Handle RTCPeerConnection last, with proper state checking
5549
- if (this._peerConnection) {
5550
- try {
5551
- // Only try to remove tracks if the connection isn't already closed
5552
- if (this._peerConnection.signalingState !== 'closed') {
5553
- const senders = this._peerConnection.getSenders();
5554
- this._logger.info(this, `Cleaning up ${senders.length} senders from peer connection`);
5555
- senders.forEach(sender => {
5556
- try {
5557
- if (sender.track) {
5558
- sender.track.enabled = false;
5559
- sender.track.stop();
5560
- // Only try to remove the track if connection is still open
5561
- if (this._peerConnection && this._peerConnection.signalingState !== 'closed') {
5562
- this._peerConnection.removeTrack(sender);
5563
- }
5564
- }
5565
- } catch (e) {
5566
- this._logger.error(this, `Error stopping sender track: ${e}`);
5567
- }
5568
- });
5569
- }
5570
- // Now close the peer connection
5571
- this._peerConnection.close();
5572
- this._peerConnection = null;
5573
- } catch (e) {
5574
- this._logger.error(this, 'Error closing peer connection');
5575
- }
5576
- }
5577
- // 5. Ensure we properly null out our device references
5578
- this._selectedCamera = null;
5579
- this._selectedMicrophone = null;
5580
- // 6. Make a final check for any active tracks at the global level
5581
- try {
5582
- const allTracks = [];
5583
- // Try to find any tracks that might still be active by querying devices
5584
- navigator.mediaDevices.getUserMedia({
5585
- audio: false,
5586
- video: false
5587
- }).then(() => {
5588
- // This is just to trigger a device check
5589
- this._logger.info(this, "Performed final device check");
5590
- }).catch(() => {
5591
- // Ignore errors from this check
5592
- });
5593
- } catch (e) {
5594
- // Ignore errors from final check
5595
- }
5596
- }
5597
- /**
5598
- * Stops all streaming operations and cleans up resources
5599
- */
5600
- stop() {
5601
- // Stop status connection and clear timer
5602
5941
  if (this._statusConnection) {
5603
5942
  this._statusConnection.destroy();
5604
5943
  this._statusConnection = null;
@@ -5608,16 +5947,12 @@
5608
5947
  this._statusTimer = null;
5609
5948
  }
5610
5949
  this._main.getConfigManager().getStreamData().streamKey = null;
5611
- // Close WebRTC connection
5612
5950
  this.closeWebRTCConnection();
5613
- // Stop all media streams
5614
5951
  this.stopCameraStream();
5615
- // Reset states
5616
5952
  this.setPublishState(exports.PublishState.STOPPED);
5617
5953
  this.setInputDeviceState(exports.InputDevicesState.STOPPED);
5618
5954
  this.setCameraState(exports.DeviceState.STOPPED);
5619
5955
  this.setMicrophoneState(exports.DeviceState.STOPPED);
5620
- // Clear restart timer if exists
5621
5956
  if (this._restartTimer) {
5622
5957
  clearInterval(this._restartTimer);
5623
5958
  this._restartTimer = null;
@@ -5625,41 +5960,62 @@
5625
5960
  this._restartTimerCount = 0;
5626
5961
  clearTimeout(this._publishTimer);
5627
5962
  }
5628
- /**
5629
- * Reinitializes the streaming setup
5630
- */
5631
5963
  start() {
5632
5964
  var _a, _b, _c;
5633
5965
  return __awaiter(this, void 0, void 0, function* () {
5966
+ if (this._isDestroyed) return;
5967
+ this._logger.info(this, "📹 [START] start() - reinitializing streaming");
5634
5968
  try {
5635
- // Reset states
5636
5969
  this._publishState = exports.PublishState.NOT_INITIALIZED;
5637
5970
  this._inputDeviceState = exports.InputDevicesState.NOT_INITIALIZED;
5638
5971
  this._cameraState = exports.DeviceState.NOT_INITIALIZED;
5639
5972
  this._microphoneState = exports.DeviceState.NOT_INITIALIZED;
5640
- // Reinitialize devices and stream
5641
5973
  yield this.initializeDevices();
5974
+ if (this._isDestroyed) return;
5642
5975
  yield this.startCamera();
5643
- // If autoConnect is enabled, initialize network
5976
+ if (this._isDestroyed) return;
5644
5977
  if ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getSettingsData().autoConnect) {
5645
5978
  (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.initialize();
5646
5979
  }
5647
- // Reinitialize status connection if needed
5648
5980
  if ((_c = this._main.getConfigManager()) === null || _c === void 0 ? void 0 : _c.getStreamData().streamKey) {
5649
5981
  this.createStatusConnection();
5650
5982
  }
5651
5983
  } catch (error) {
5652
- this._logger.error(this, "Start failed: " + JSON.stringify(error));
5984
+ if (this._isDestroyed) return;
5985
+ this._logger.error(this, "📹 [START] start() - failed: " + JSON.stringify(error));
5653
5986
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5654
5987
  throw error;
5655
5988
  }
5656
5989
  });
5657
5990
  }
5658
5991
  /**
5659
- * Method used for destroying everything (one-time use)
5992
+ * SYNCHRONOUS destroy - sets flag immediately, cleanup happens in background
5993
+ * This ensures that even if called without await, all async operations will abort
5660
5994
  */
5661
5995
  destroy() {
5662
- // Stop any ongoing timers and intervals first
5996
+ var _a, _b, _c, _d;
5997
+ // Prevent double destroy
5998
+ if (this._isDestroyed) {
5999
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warning(this, "🔴 [DESTROY] Already destroyed, skipping");
6000
+ return;
6001
+ }
6002
+ this._logger.info(this, "🔴 [DESTROY] Starting StreamerController destroy (sync)...");
6003
+ // CRITICAL: Set flag IMMEDIATELY - this will cause all pending async operations to abort
6004
+ this._isDestroyed = true;
6005
+ // Cancel all abort controllers immediately
6006
+ if (this._cameraAbortController) {
6007
+ this._cameraAbortController.abort();
6008
+ this._cameraAbortController = null;
6009
+ }
6010
+ if (this._microphoneAbortController) {
6011
+ this._microphoneAbortController.abort();
6012
+ this._microphoneAbortController = null;
6013
+ }
6014
+ if (this._startCameraAbortController) {
6015
+ this._startCameraAbortController.abort();
6016
+ this._startCameraAbortController = null;
6017
+ }
6018
+ // Stop all timers immediately
5663
6019
  if (this._statusTimer != null) {
5664
6020
  clearInterval(this._statusTimer);
5665
6021
  this._statusTimer = null;
@@ -5669,13 +6025,23 @@
5669
6025
  this._restartTimer = null;
5670
6026
  }
5671
6027
  clearTimeout(this._publishTimer);
5672
- // Remove orientation change listeners
5673
- if (window.screen && window.screen.orientation) {
5674
- window.screen.orientation.removeEventListener('change', this.handleOrientationChange);
5675
- } else {
5676
- window.removeEventListener('orientationchange', this.handleOrientationChange);
6028
+ // Remove permission listeners
6029
+ this.removePermissionListeners();
6030
+ // Remove device change listener
6031
+ if (this._deviceChangeHandler) {
6032
+ navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeHandler);
6033
+ this._deviceChangeHandler = null;
6034
+ }
6035
+ // Remove orientation listener
6036
+ if (this._orientationChangeHandler) {
6037
+ if (window.screen && window.screen.orientation) {
6038
+ window.screen.orientation.removeEventListener('change', this._orientationChangeHandler);
6039
+ } else {
6040
+ window.removeEventListener('orientationchange', this._orientationChangeHandler);
6041
+ }
6042
+ this._orientationChangeHandler = null;
5677
6043
  }
5678
- // Remove other event listeners
6044
+ // Remove event listeners
5679
6045
  try {
5680
6046
  this._main.removeEventListener("serverConnect", this.onServerConnect);
5681
6047
  this._main.removeEventListener("serverDisconnect", this.onServerDisconnect);
@@ -5688,28 +6054,46 @@
5688
6054
  window.removeEventListener("blur", this.onWindowBlur);
5689
6055
  window.removeEventListener("focus", this.onWindowFocus);
5690
6056
  } catch (e) {
5691
- this._logger.error(this, 'Error removing event listeners');
6057
+ // Ignore errors
5692
6058
  }
5693
- // Make sure we destroy the status connection
6059
+ // Destroy status connection
5694
6060
  if (this._statusConnection) {
5695
6061
  this._statusConnection.destroy();
5696
6062
  this._statusConnection = null;
5697
6063
  }
5698
- // Stop all media streams and clean up the WebRTC connection
5699
- this.forceStopAllStreams();
5700
- // Reset all state variables
5701
- this._pendingMicrophoneState = null;
5702
- this._cameraList = new InputDeviceList();
5703
- this._microphoneList = new InputDeviceList();
5704
- this._permissionChecked = false;
5705
- this._isWindowActive = false;
5706
- this._isMicrophoneMuted = false;
5707
- this._publishState = exports.PublishState.NOT_INITIALIZED;
5708
- this._inputDeviceState = exports.InputDevicesState.NOT_INITIALIZED;
5709
- this._cameraState = exports.DeviceState.NOT_INITIALIZED;
5710
- this._microphoneState = exports.DeviceState.NOT_INITIALIZED;
5711
- // Log completion
5712
- this._logger.success(this, "StreamerController successfully destroyed and all resources released");
6064
+ // Close WebRTC
6065
+ this.closeWebRTCConnection();
6066
+ // Stop camera stream
6067
+ if (this._stream) {
6068
+ this._logger.info(this, "📹 [FORCE_STOP] Stopping main stream");
6069
+ this._stream.getTracks().forEach(track => {
6070
+ track.stop();
6071
+ });
6072
+ this._stream = null;
6073
+ }
6074
+ // Clean video element
6075
+ try {
6076
+ const videoElement = (_c = (_b = this._main.getStageController()) === null || _b === void 0 ? void 0 : _b.getScreenElement()) === null || _c === void 0 ? void 0 : _c.getVideoElement();
6077
+ if (videoElement) {
6078
+ if (videoElement.srcObject instanceof MediaStream) {
6079
+ videoElement.srcObject.getTracks().forEach(track => track.stop());
6080
+ }
6081
+ videoElement.srcObject = null;
6082
+ }
6083
+ } catch (e) {
6084
+ // Ignore
6085
+ }
6086
+ // Destroy sound meter
6087
+ try {
6088
+ (_d = this._soundMeter) === null || _d === void 0 ? void 0 : _d.destroy();
6089
+ } catch (e) {
6090
+ // Ignore
6091
+ }
6092
+ // Reset variables
6093
+ this._selectedCamera = null;
6094
+ this._selectedMicrophone = null;
6095
+ this._activeStreamCount = 0;
6096
+ this._logger.success(this, "🔴 [DESTROY] StreamerController destroyed (sync)");
5713
6097
  }
5714
6098
  }
5715
6099
 
@@ -6294,7 +6678,6 @@
6294
6678
  this.onStreamStatsUpdate = event => {
6295
6679
  var _a;
6296
6680
  (_a = this._graph) === null || _a === void 0 ? void 0 : _a.addEntry(event.high * 500);
6297
- console.log(event.high * 200);
6298
6681
  };
6299
6682
  this._main = main;
6300
6683
  this._object = container;
@@ -6322,52 +6705,19 @@
6322
6705
  * Main class of the player. The player itself has no GUI, but can be controlled via provided API.
6323
6706
  */
6324
6707
  class StormStreamer extends EventDispatcher {
6325
- //------------------------------------------------------------------------//
6326
- // CONSTRUCTOR
6327
- //------------------------------------------------------------------------//
6328
- /**
6329
- * Constructor - creates a new StormStreamer instance
6330
- *
6331
- * @param streamConfig - Configuration object for the streamer
6332
- * @param autoInitialize - Whether to automatically initialize the streamer after creation
6333
- */
6334
6708
  constructor(streamConfig, autoInitialize = false) {
6335
6709
  super();
6336
- /**
6337
- * Indicates whether the streamer object is in development mode (provides more debug options)
6338
- * @private
6339
- */
6340
6710
  this.DEV_MODE = true;
6341
- /**
6342
- * Version of this streamer in SemVer format (Major.Minor.Patch).
6343
- * @private
6344
- */
6345
- this.STREAMER_VERSION = "1.0.0-rc.0";
6346
- /**
6347
- * Compile date for this streamer
6348
- * @private
6349
- */
6350
- this.COMPILE_DATE = "3/28/2025, 9:18:36 AM";
6351
- /**
6352
- * Defines from which branch this streamer comes from e.g. "Main", "Experimental"
6353
- * @private
6354
- */
6711
+ this.STREAMER_VERSION = "1.0.0";
6712
+ this.COMPILE_DATE = "2/7/2026, 6:37:16 PM";
6355
6713
  this.STREAMER_BRANCH = "Experimental";
6356
- /**
6357
- * Defines number of streamer protocol that is required on server-side
6358
- * @private
6359
- */
6360
6714
  this.STREAMER_PROTOCOL_VERSION = 1;
6361
- /**
6362
- * Indicates whether streamer was initialized or not
6363
- * @private
6364
- */
6365
6715
  this._initialized = false;
6716
+ this._isDestroyed = false;
6366
6717
  if (typeof window === 'undefined' || !window.document || !window.document.createElement) {
6367
6718
  console.error(`StormStreamer Creation Error - No "window" element in the provided context!`);
6368
6719
  return;
6369
6720
  }
6370
- // WINDOW.StormStreamerArray
6371
6721
  if (this.DEV_MODE && !('StormStreamerArray' in window)) {
6372
6722
  window.StormStreamerArray = [];
6373
6723
  }
@@ -6377,17 +6727,13 @@
6377
6727
  this.setStreamConfig(streamConfig);
6378
6728
  if (autoInitialize) this.initialize();
6379
6729
  }
6380
- /**
6381
- * Initializes the streamer object. From this point, a connection to the server is established and authentication occurs.
6382
- * It is recommended to add all event listeners before calling this method to ensure they can be properly captured.
6383
- */
6384
6730
  initialize() {
6385
- if (this._isRemoved) return;
6386
- if (this._configManager == null) throw Error("Stream Config was not provided for this streamer! A properly configured object must be provided through the constructor or via the setConfig method before using the initialize() method.");
6387
- this._storageManager = new StorageManager(this); // Storing user data
6388
- this._stageController = new StageController(this); // Visual elements like VideoElement
6389
- this._networkController = new NetworkController(this); // Networking and connection with a server
6390
- this._streamerController = new StreamerController(this); // Video Playback
6731
+ if (this._isRemoved || this._isDestroyed) return;
6732
+ if (this._configManager == null) throw Error("Stream Config was not provided for this streamer!");
6733
+ this._storageManager = new StorageManager(this);
6734
+ this._stageController = new StageController(this);
6735
+ this._networkController = new NetworkController(this);
6736
+ this._streamerController = new StreamerController(this);
6391
6737
  this._statsController = new StatsController(this);
6392
6738
  this._graphs = [];
6393
6739
  this._initialized = true;
@@ -6395,16 +6741,8 @@
6395
6741
  ref: this
6396
6742
  });
6397
6743
  }
6398
- /**
6399
- * Sets stream config for the streamer (or overwrites an existing one).
6400
- *
6401
- * @param streamConfig - New configuration object for the streamer
6402
- */
6403
6744
  setStreamConfig(streamConfig) {
6404
- if (this._isRemoved) return;
6405
- /**
6406
- * In case the original streamConfig is modified elsewhere we have to create a separate copy and store it ourselves
6407
- */
6745
+ if (this._isRemoved || this._isDestroyed) return;
6408
6746
  const copiedStreamConfig = JSON.parse(JSON.stringify(streamConfig));
6409
6747
  if (this._configManager == null) {
6410
6748
  this._configManager = new ConfigManager(copiedStreamConfig);
@@ -6429,289 +6767,133 @@
6429
6767
  });
6430
6768
  }
6431
6769
  }
6432
- //------------------------------------------------------------------------//
6433
6770
  // PLAYBACK / STREAMING
6434
- //------------------------------------------------------------------------//
6435
- /**
6436
- * Returns true if this streamer instance is currently connected to a Storm Server/Cloud instance.
6437
- *
6438
- * @returns Boolean indicating connection status
6439
- */
6440
6771
  isConnected() {
6441
6772
  var _a, _b;
6442
6773
  return (_b = (_a = this._networkController) === null || _a === void 0 ? void 0 : _a.getConnection().isConnectionActive()) !== null && _b !== void 0 ? _b : false;
6443
6774
  }
6444
- /**
6445
- * Mutes the streamer's video object. Audio output will be silenced.
6446
- */
6447
6775
  mute() {
6448
- if (this._stageController != null) {
6449
- if (this._stageController.getScreenElement() != null) {
6450
- this._stageController.getScreenElement().setMuted(true);
6451
- return;
6452
- }
6776
+ var _a;
6777
+ if (((_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) != null) {
6778
+ this._stageController.getScreenElement().setMuted(true);
6779
+ return;
6453
6780
  }
6454
6781
  this._configManager.getSettingsData().getAudioData().muted = true;
6455
6782
  }
6456
- /**
6457
- * Unmutes the streamer's video object. Audio output will be restored.
6458
- */
6459
6783
  unmute() {
6460
- if (this._stageController != null) {
6461
- if (this._stageController.getScreenElement() != null) {
6462
- this._stageController.getScreenElement().setMuted(false);
6463
- return;
6464
- }
6784
+ var _a;
6785
+ if (((_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) != null) {
6786
+ this._stageController.getScreenElement().setMuted(false);
6787
+ return;
6465
6788
  }
6466
6789
  this._configManager.getSettingsData().getAudioData().muted = false;
6467
6790
  }
6468
- /**
6469
- * Checks whether the streamer audio is currently muted.
6470
- *
6471
- * @returns Boolean indicating mute status
6472
- */
6473
6791
  isMute() {
6474
6792
  var _a, _b, _c, _d;
6475
6793
  return (_d = (_c = (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getIfMuted()) !== null && _c !== void 0 ? _c : this._configManager.getSettingsData().getAudioData().muted) !== null && _d !== void 0 ? _d : false;
6476
6794
  }
6477
- /**
6478
- * Toggles between mute and unmute states. Returns the new mute state.
6479
- *
6480
- * @returns New mute state (true = muted, false = unmuted)
6481
- */
6482
6795
  toggleMute() {
6483
6796
  const isMuted = this.isMute();
6484
- if (isMuted) {
6485
- this.unmute();
6486
- } else {
6487
- this.mute();
6488
- }
6797
+ if (isMuted) this.unmute();else this.mute();
6489
6798
  return !isMuted;
6490
6799
  }
6491
- /**
6492
- * Sets new volume for the streamer (0-100). Once the method is performed, the volumeChange event will be triggered.
6493
- * If the video was muted prior to the volume change, it will be automatically unmuted.
6494
- *
6495
- * @param newVolume - Volume level (0-100)
6496
- */
6497
6800
  setVolume(newVolume) {
6498
6801
  var _a, _b;
6499
- if (((_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.setVolume(newVolume)) !== undefined) {
6500
- return;
6501
- }
6802
+ if (((_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.setVolume(newVolume)) !== undefined) return;
6502
6803
  this._configManager.getSettingsData().getAudioData().startVolume = newVolume;
6503
6804
  }
6504
- /**
6505
- * Returns current streamer volume (0-100).
6506
- *
6507
- * @returns Current volume level
6508
- */
6509
6805
  getVolume() {
6510
6806
  var _a, _b, _c;
6511
6807
  return (_c = (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVolume()) !== null && _c !== void 0 ? _c : this._configManager.getSettingsData().getAudioData().startVolume;
6512
6808
  }
6513
- /**
6514
- * Returns the list of available camera devices.
6515
- *
6516
- * @returns Array of camera input devices
6517
- */
6518
6809
  getCameraList() {
6519
6810
  var _a, _b;
6520
6811
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCameraList()) !== null && _b !== void 0 ? _b : [];
6521
6812
  }
6522
- /**
6523
- * Returns the list of available microphone devices.
6524
- *
6525
- * @returns Array of microphone input devices
6526
- */
6527
6813
  getMicrophoneList() {
6528
6814
  var _a, _b;
6529
6815
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getMicrophoneList()) !== null && _b !== void 0 ? _b : [];
6530
6816
  }
6531
- /**
6532
- * Sets the active camera device by ID.
6533
- *
6534
- * @param cameraID - ID of the camera device to use
6535
- */
6536
6817
  setCamera(cameraID) {
6537
6818
  var _a;
6538
6819
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.selectCamera(cameraID);
6539
6820
  }
6540
- /**
6541
- * Sets the active microphone device by ID.
6542
- *
6543
- * @param microphoneID - ID of the microphone device to use
6544
- */
6545
6821
  setMicrophone(microphoneID) {
6546
6822
  var _a;
6547
6823
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.selectMicrophone(microphoneID);
6548
6824
  }
6549
- /**
6550
- * Returns the currently active camera device.
6551
- *
6552
- * @returns Current camera device or null if none is active
6553
- */
6554
6825
  getCurrentCamera() {
6555
- return this._streamerController.getCurrentCamera();
6826
+ var _a, _b;
6827
+ return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCurrentCamera()) !== null && _b !== void 0 ? _b : null;
6556
6828
  }
6557
- /**
6558
- * Returns the currently active microphone device.
6559
- *
6560
- * @returns Current microphone device or null if none is active
6561
- */
6562
6829
  getCurrentMicrophone() {
6563
- return this._streamerController.getCurrentMicrophone();
6830
+ var _a, _b;
6831
+ return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCurrentMicrophone()) !== null && _b !== void 0 ? _b : null;
6564
6832
  }
6565
- /**
6566
- * Mutes or unmutes the microphone.
6567
- *
6568
- * @param microphoneState - True to mute, false to unmute
6569
- */
6570
6833
  muteMicrophone(microphoneState) {
6571
6834
  var _a;
6572
6835
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.muteMicrophone(microphoneState);
6573
6836
  }
6574
- /**
6575
- * Checks if the microphone is currently muted.
6576
- *
6577
- * @returns Boolean indicating if microphone is muted
6578
- */
6579
6837
  isMicrophoneMuted() {
6580
6838
  var _a, _b;
6581
6839
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.isMicrophoneMuted()) !== null && _b !== void 0 ? _b : false;
6582
6840
  }
6583
- /**
6584
- * Returns the current publishing state of the streamer.
6585
- *
6586
- * @returns Current publishing state
6587
- */
6588
6841
  getPublishState() {
6589
6842
  var _a, _b;
6590
6843
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getPublishState()) !== null && _b !== void 0 ? _b : exports.PublishState.NOT_INITIALIZED;
6591
6844
  }
6592
- /**
6593
- * Returns the total time the stream has been publishing in milliseconds.
6594
- *
6595
- * @returns Publishing time in milliseconds
6596
- */
6597
6845
  getPublishTime() {
6598
6846
  var _a, _b;
6599
6847
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getPublishTime()) !== null && _b !== void 0 ? _b : 0;
6600
6848
  }
6601
- /**
6602
- * Starts publishing a stream with the given stream key.
6603
- *
6604
- * @param streamKey - Key identifying the stream to publish
6605
- * @returns Boolean indicating if publishing was successfully initiated
6606
- */
6607
6849
  publish(streamKey) {
6608
6850
  var _a, _b;
6609
6851
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.publish(streamKey)) !== null && _b !== void 0 ? _b : false;
6610
6852
  }
6611
- /**
6612
- * Stops publishing the current stream.
6613
- */
6614
6853
  unpublish() {
6615
6854
  var _a;
6616
6855
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.unpublish();
6617
6856
  }
6618
- /**
6619
- * Returns the current state of input devices (camera and microphone).
6620
- *
6621
- * @returns Current state of input devices
6622
- */
6623
6857
  getInputDevicesState() {
6624
6858
  var _a, _b;
6625
6859
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getInputDeviceState()) !== null && _b !== void 0 ? _b : exports.InputDevicesState.NOT_INITIALIZED;
6626
6860
  }
6627
- /**
6628
- * Returns the current state of the camera device.
6629
- *
6630
- * @returns Current camera device state
6631
- */
6632
6861
  getCameraState() {
6633
6862
  var _a, _b;
6634
6863
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCameraState()) !== null && _b !== void 0 ? _b : exports.DeviceState.NOT_INITIALIZED;
6635
6864
  }
6636
- /**
6637
- * Returns the current state of the microphone device.
6638
- *
6639
- * @returns Current microphone device state
6640
- */
6641
6865
  getMicrophoneState() {
6642
6866
  var _a, _b;
6643
6867
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getMicrophoneState()) !== null && _b !== void 0 ? _b : exports.DeviceState.NOT_INITIALIZED;
6644
6868
  }
6645
- /**
6646
- * Clears saved device preferences from storage.
6647
- */
6648
6869
  clearSavedDevices() {
6649
6870
  var _a;
6650
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.clearSavedDevices();
6871
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.clearSavedDevices();
6651
6872
  }
6652
- /**
6653
- * Randomizes saved device preferences (for testing purposes).
6654
- */
6655
6873
  messSavedDevices() {
6656
6874
  var _a;
6657
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.messSavedDevices();
6875
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.messSavedDevices();
6658
6876
  }
6659
- /**
6660
- * Checks if the stream is ready for publishing.
6661
- *
6662
- * @returns Boolean indicating if stream is ready
6663
- */
6664
6877
  isStreamReady() {
6665
6878
  var _a, _b;
6666
6879
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.isStreamReady()) !== null && _b !== void 0 ? _b : false;
6667
6880
  }
6668
- //------------------------------------------------------------------------//
6669
6881
  // CONTAINER
6670
- //------------------------------------------------------------------------//
6671
- /**
6672
- * Attaches the streamer to a new parent container using either a container ID (string) or a reference to an HTMLElement.
6673
- * If the instance is already attached, it will be moved to a new parent.
6674
- *
6675
- * @param container - Container ID (string) or HTMLElement reference
6676
- * @returns Boolean indicating if attachment was successful
6677
- */
6678
6882
  attachToContainer(container) {
6679
6883
  var _a, _b;
6680
- let result = false;
6681
6884
  if (this._initialized) return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.attachToParent(container)) !== null && _b !== void 0 ? _b : false;
6682
- return result;
6885
+ return false;
6683
6886
  }
6684
- /**
6685
- * Detaches the streamer from the current parent element, if possible.
6686
- *
6687
- * @returns Boolean indicating if detachment was successful
6688
- */
6689
6887
  detachFromContainer() {
6690
6888
  var _a, _b;
6691
- let result = false;
6692
6889
  if (this._initialized) return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.detachFromParent()) !== null && _b !== void 0 ? _b : false;
6693
- return result;
6890
+ return false;
6694
6891
  }
6695
- /**
6696
- * Returns the current parent element of the streamer, or null if none exists.
6697
- *
6698
- * @returns Current container element or null
6699
- */
6700
6892
  getContainer() {
6701
6893
  var _a, _b;
6702
6894
  return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getParentElement()) !== null && _b !== void 0 ? _b : null;
6703
6895
  }
6704
- //------------------------------------------------------------------------//
6705
6896
  // SIZE & RESIZE
6706
- //------------------------------------------------------------------------//
6707
- /**
6708
- * Sets a new width and height for the streamer. The values can be given as a number (in which case they are
6709
- * treated as the number of pixels), or as a string ending with "px" (this will also be the number of pixels) or "%",
6710
- * where the number is treated as a percentage of the parent container's value.
6711
- *
6712
- * @param width - Can be provided as number or a string with "%" or "px" suffix
6713
- * @param height - Can be provided as number or a string with "%" or "px" suffix
6714
- */
6715
6897
  setSize(width, height) {
6716
6898
  if (this._initialized) this._stageController.setSize(width, height);else {
6717
6899
  const parsedWidth = NumberUtilities.parseValue(width);
@@ -6722,13 +6904,6 @@
6722
6904
  this._configManager.getSettingsData().getVideoData().videoHeightInPixels = parsedHeight.isPixels;
6723
6905
  }
6724
6906
  }
6725
- /**
6726
- * Sets a new width for the streamer. The value can be given as a number (in which case it is treated as the
6727
- * number of pixels), or as a string ending with "px" (this will also be the number of pixels) or "%", where the
6728
- * number is treated as a percentage of the parent container's value.
6729
- *
6730
- * @param width - Can be provided as number or a string with "%" or "px" suffix
6731
- */
6732
6907
  setWidth(width) {
6733
6908
  if (this._initialized) this._stageController.setWidth(width);else {
6734
6909
  const parsedWidth = NumberUtilities.parseValue(width);
@@ -6736,13 +6911,6 @@
6736
6911
  this._configManager.getSettingsData().getVideoData().videoWidthInPixels = parsedWidth.isPixels;
6737
6912
  }
6738
6913
  }
6739
- /**
6740
- * Sets a new height for the streamer. The value can be given as a number (in which case it is treated as the
6741
- * number of pixels), or as a string ending with "px" (this will also be the number of pixels) or "%", where the
6742
- * number is treated as a percentage of the parent container's value.
6743
- *
6744
- * @param height - Can be provided as number or a string with "%" or "px" suffix
6745
- */
6746
6914
  setHeight(height) {
6747
6915
  if (this._initialized) this._stageController.setHeight(height);else {
6748
6916
  const parsedHeight = NumberUtilities.parseValue(height);
@@ -6750,66 +6918,26 @@
6750
6918
  this._configManager.getSettingsData().getVideoData().videoHeightInPixels = parsedHeight.isPixels;
6751
6919
  }
6752
6920
  }
6753
- /**
6754
- * Returns current streamer width in pixels.
6755
- *
6756
- * @returns Current width in pixels
6757
- */
6758
6921
  getWidth() {
6759
- if (this._initialized) return this._stageController.getContainerWidth();else {
6760
- if (this._configManager.getSettingsData().getVideoData().videoWidthInPixels) return this._configManager.getSettingsData().getVideoData().videoWidthValue;
6761
- }
6922
+ if (this._initialized) return this._stageController.getContainerWidth();
6923
+ if (this._configManager.getSettingsData().getVideoData().videoWidthInPixels) return this._configManager.getSettingsData().getVideoData().videoWidthValue;
6762
6924
  return 0;
6763
6925
  }
6764
- /**
6765
- * Returns current streamer height in pixels.
6766
- *
6767
- * @returns Current height in pixels
6768
- */
6769
6926
  getHeight() {
6770
- if (this._initialized) return this._stageController.getContainerHeight();else {
6771
- if (this._configManager.getSettingsData().getVideoData().videoHeightInPixels) return this._configManager.getSettingsData().getVideoData().videoHeightValue;
6772
- }
6927
+ if (this._initialized) return this._stageController.getContainerHeight();
6928
+ if (this._configManager.getSettingsData().getVideoData().videoHeightInPixels) return this._configManager.getSettingsData().getVideoData().videoHeightValue;
6773
6929
  return 0;
6774
6930
  }
6775
- /**
6776
- * Changes the streamer scaling mode. Available modes include fill, letterbox, original, and crop.
6777
- *
6778
- * @param newMode - New scaling mode name (fill, letterbox, original, crop)
6779
- */
6780
6931
  setScalingMode(newMode) {
6781
- if (this._stageController) {
6782
- this._stageController.setScalingMode(newMode);
6783
- } else {
6784
- this._configManager.getSettingsData().getVideoData().scalingMode = newMode;
6785
- }
6932
+ if (this._stageController) this._stageController.setScalingMode(newMode);else this._configManager.getSettingsData().getVideoData().scalingMode = newMode;
6786
6933
  }
6787
- /**
6788
- * Returns the current streamer scaling mode.
6789
- *
6790
- * @returns Current scaling mode
6791
- */
6792
6934
  getScalingMode() {
6793
- if (this._stageController) {
6794
- return this._stageController.getScalingMode();
6795
- } else {
6796
- return this._configManager.getSettingsData().getVideoData().scalingMode;
6797
- }
6935
+ if (this._stageController) return this._stageController.getScalingMode();
6936
+ return this._configManager.getSettingsData().getVideoData().scalingMode;
6798
6937
  }
6799
- /**
6800
- * Forces the streamer to recalculate its size based on parent internal dimensions.
6801
- */
6802
6938
  updateToSize() {
6803
- if (this._initialized) {
6804
- this._stageController.handleResize();
6805
- }
6939
+ if (this._initialized) this._stageController.handleResize();
6806
6940
  }
6807
- /**
6808
- * Returns a promise that resolves with a screenshot of the video element as a blob, or null if taking the
6809
- * screenshot was not possible.
6810
- *
6811
- * @returns Promise resolving to a Blob containing the screenshot or null
6812
- */
6813
6941
  makeScreenshot() {
6814
6942
  let canvas = document.createElement('canvas');
6815
6943
  let context = canvas.getContext('2d');
@@ -6820,64 +6948,24 @@
6820
6948
  let element = this._stageController.getScreenElement().getVideoElement();
6821
6949
  if (context) {
6822
6950
  context.drawImage(element, 0, 0, canvas.width, canvas.height);
6823
- canvas.toBlob(blob => {
6824
- resolve(blob);
6825
- }, 'image/png');
6826
- } else {
6827
- resolve(null);
6828
- }
6829
- } else {
6830
- resolve(null);
6831
- }
6951
+ canvas.toBlob(blob => resolve(blob), 'image/png');
6952
+ } else resolve(null);
6953
+ } else resolve(null);
6832
6954
  });
6833
6955
  }
6834
- //------------------------------------------------------------------------//
6835
6956
  // GRAPHS
6836
- //------------------------------------------------------------------------//
6837
- /**
6838
- * Creates a FPS performance graph in the specified location (container ID or reference). The graph is a
6839
- * separate object that must be started using its start() method and stopped using its stop() method. The dimensions
6840
- * of the graph depend on the dimensions of the specified container.
6841
- *
6842
- * @param container - Element ID or reference to HTMLElement
6843
- * @returns FPS graph instance
6844
- */
6845
6957
  createFPSGraph(container) {
6846
6958
  return new FPSGraph(this, container);
6847
6959
  }
6848
- /**
6849
- * Creates a bitrate performance graph in the specified location (container ID or reference). The graph is a
6850
- * separate object that must be started using its start() method and stopped using its stop() method. The dimensions
6851
- * of the graph depend on the dimensions of the specified container.
6852
- *
6853
- * @param container - Element ID or reference to HTMLElement
6854
- * @returns Bitrate graph instance
6855
- */
6856
6960
  createBitrateGraph(container) {
6857
6961
  return new BitrateGraph(this, container);
6858
6962
  }
6859
- /**
6860
- * Creates a microphone graph in the specified location (container ID or reference). The graph is a
6861
- * separate object that must be started using its start() method and stopped using its stop() method. The dimensions
6862
- * of the graph depend on the dimensions of the specified container.
6863
- *
6864
- * @param container - Element ID or reference to HTMLElement
6865
- * @returns Bitrate graph instance
6866
- */
6867
6963
  createMicrophoneGraph(container) {
6868
6964
  return new MicrophoneGraph(this, container);
6869
6965
  }
6870
- /**
6871
- * Adds new graph to the internal collection of active graphs.
6872
- *
6873
- * @param newGraph - Graph instance to add
6874
- */
6875
6966
  addGraph(newGraph) {
6876
6967
  if (this._graphs != null) this._graphs.push(newGraph);
6877
6968
  }
6878
- /**
6879
- * Stops all active performance graphs.
6880
- */
6881
6969
  stopAllGraphs() {
6882
6970
  if (this._graphs != null && this._graphs.length > 0) {
6883
6971
  for (let i = 0; i < this._graphs.length; i++) {
@@ -6885,198 +6973,136 @@
6885
6973
  }
6886
6974
  }
6887
6975
  }
6888
- //------------------------------------------------------------------------//
6889
6976
  // FULLSCREEN
6890
- //------------------------------------------------------------------------//
6891
- /**
6892
- * Enters fullscreen mode for the streamer container.
6893
- */
6894
6977
  enterFullScreen() {
6895
6978
  if (this._initialized && this._stageController) this._stageController.enterFullScreen();
6896
6979
  }
6897
- /**
6898
- * Exits fullscreen mode.
6899
- */
6900
6980
  exitFullScreen() {
6901
6981
  if (this._initialized && this._stageController) this._stageController.exitFullScreen();
6902
6982
  }
6903
- /**
6904
- * Returns true if the streamer is currently in fullscreen mode.
6905
- *
6906
- * @returns Boolean indicating fullscreen status
6907
- */
6908
6983
  isFullScreenMode() {
6909
6984
  if (this._initialized && this._stageController) return this._stageController.isFullScreenMode();
6910
6985
  return false;
6911
6986
  }
6912
- //------------------------------------------------------------------------//
6913
- // SIMPLE GETS & SETS
6914
- //------------------------------------------------------------------------//
6915
- /**
6916
- * Returns the current stream key or null if none is set.
6917
- *
6918
- * @returns Current stream key or null
6919
- */
6987
+ // GETTERS
6920
6988
  getStreamKey() {
6921
6989
  var _a, _b, _c;
6922
6990
  return (_c = (_b = (_a = this.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData()) === null || _b === void 0 ? void 0 : _b.streamKey) !== null && _c !== void 0 ? _c : null;
6923
6991
  }
6924
- /**
6925
- * Returns the Stats Controller instance which contains statistical data about streaming performance.
6926
- *
6927
- * @returns Stats controller instance or null
6928
- */
6929
6992
  getStatsController() {
6930
6993
  return this._statsController;
6931
6994
  }
6932
- /**
6933
- * Returns the unique ID of this streamer instance. Each subsequent instance has a higher number.
6934
- *
6935
- * @returns Streamer instance ID
6936
- */
6937
6995
  getStreamerID() {
6938
6996
  return this._streamerID;
6939
6997
  }
6940
- /**
6941
- * Returns the logger instance used by this streamer.
6942
- *
6943
- * @returns Logger instance
6944
- */
6945
6998
  getLogger() {
6946
6999
  return this._logger;
6947
7000
  }
6948
- /**
6949
- * Returns the configuration manager for this streamer.
6950
- *
6951
- * @returns Config manager instance or null
6952
- */
6953
7001
  getConfigManager() {
6954
7002
  return this._configManager;
6955
7003
  }
6956
- /**
6957
- * Returns the network controller which manages all server communication.
6958
- *
6959
- * @returns Network controller instance or null
6960
- */
6961
7004
  getNetworkController() {
6962
7005
  return this._networkController;
6963
7006
  }
6964
- /**
6965
- * Returns the streamer controller which manages media stream operations.
6966
- *
6967
- * @returns Streamer controller instance or null
6968
- */
6969
7007
  getStreamerController() {
6970
7008
  return this._streamerController;
6971
7009
  }
6972
- /**
6973
- * Returns the stage controller which manages visual presentation.
6974
- *
6975
- * @returns Stage controller instance or null
6976
- */
6977
7010
  getStageController() {
6978
7011
  return this._stageController;
6979
7012
  }
6980
- /**
6981
- * Returns the storage manager which handles persistent data storage.
6982
- *
6983
- * @returns Storage manager instance or null
6984
- */
6985
7013
  getStorageManager() {
6986
7014
  return this._storageManager;
6987
7015
  }
6988
- /**
6989
- * Returns the HTML video element used by this streamer instance.
6990
- *
6991
- * @returns Video element or null
6992
- */
6993
7016
  getVideoElement() {
6994
7017
  var _a, _b, _c;
6995
7018
  return (_c = (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement()) !== null && _c !== void 0 ? _c : null;
6996
7019
  }
6997
- /**
6998
- * Returns true if this streamer instance has already been initialized.
6999
- *
7000
- * @returns Boolean indicating initialization status
7001
- */
7002
7020
  isInitialized() {
7003
7021
  return this._initialized;
7004
7022
  }
7005
- /**
7006
- * Returns the version of this streamer instance. The version is returned in the SemVer format (Major.Minor.Patch).
7007
- *
7008
- * @returns Streamer version string
7009
- */
7023
+ isDestroyed() {
7024
+ return this._isDestroyed;
7025
+ }
7010
7026
  getVersion() {
7011
7027
  return this.STREAMER_VERSION;
7012
7028
  }
7013
- /**
7014
- * Returns the development branch of this streamer (e.g., main, experimental).
7015
- *
7016
- * @returns Branch name string
7017
- */
7018
7029
  getBranch() {
7019
7030
  return this.STREAMER_BRANCH;
7020
7031
  }
7021
- //------------------------------------------------------------------------//
7022
- // EVENT
7023
- //------------------------------------------------------------------------//
7024
- /**
7025
- * Dispatches an event with the specified name and data.
7026
- *
7027
- * @param eventName - Name of the event to dispatch
7028
- * @param event - Object containing event data
7029
- */
7032
+ // EVENTS
7030
7033
  dispatchEvent(eventName, event) {
7031
7034
  super.dispatchEvent(eventName, event);
7032
7035
  }
7033
- //------------------------------------------------------------------------//
7034
- // CLEAN UP
7035
- //------------------------------------------------------------------------//
7036
- /**
7037
- * Starts the streaming process.
7038
- *
7039
- * @returns Promise that resolves when streaming has started
7040
- */
7036
+ // START / STOP
7041
7037
  start() {
7042
7038
  var _a;
7043
7039
  return __awaiter(this, void 0, void 0, function* () {
7044
7040
  return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.start();
7045
7041
  });
7046
7042
  }
7047
- /**
7048
- * Stops the streaming process.
7049
- */
7050
7043
  stop() {
7051
7044
  var _a;
7052
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.stop();
7045
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.stop();
7046
+ }
7047
+ // DEBUG
7048
+ debugMediaState() {
7049
+ var _a;
7050
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.debugMediaState();
7053
7051
  }
7054
- //------------------------------------------------------------------------//
7055
- // CLEAN UP
7056
- //------------------------------------------------------------------------//
7057
7052
  /**
7058
- * Destroys this instance of StormStreamer, releasing all resources and disconnecting from the server.
7059
- * After calling this method, the instance should not be used anymore.
7053
+ * Destroys this instance of StormStreamer, releasing all resources.
7054
+ * This method is SYNCHRONOUS - it sets flags immediately to prevent race conditions.
7055
+ * All pending async operations will detect the destroyed state and abort.
7060
7056
  */
7061
7057
  destroy() {
7062
- var _a, _b, _c, _d;
7063
- this._logger.warning(this, "Destroying streamer instance, bye, bye!");
7064
- if (this.DEV_MODE && 'StormStreamerArray' in window) window.StormStreamerArray[this._streamerID] = null;
7065
- // part1
7058
+ var _a, _b, _c, _d, _e, _f, _g;
7059
+ // Prevent double destroy
7060
+ if (this._isDestroyed) {
7061
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warning(this, "🔴 [DESTROY] Already destroyed, skipping");
7062
+ return;
7063
+ }
7064
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.warning(this, "🔴 [DESTROY] Starting streamer instance destruction (sync)...");
7065
+ // CRITICAL: Set flag IMMEDIATELY
7066
+ this._isDestroyed = true;
7066
7067
  this._initialized = false;
7067
7068
  this._isRemoved = true;
7068
- // part3
7069
- (_b = (_a = this._networkController) === null || _a === void 0 ? void 0 : _a.getConnection()) === null || _b === void 0 ? void 0 : _b.destroy();
7070
- (_c = this._streamerController) === null || _c === void 0 ? void 0 : _c.destroy();
7071
- (_d = this._stageController) === null || _d === void 0 ? void 0 : _d.destroy();
7072
- // part2
7069
+ // Remove from global array
7070
+ if (this.DEV_MODE && 'StormStreamerArray' in window) {
7071
+ window.StormStreamerArray[this._streamerID] = null;
7072
+ }
7073
+ // Stop all graphs
7074
+ this.stopAllGraphs();
7075
+ this._graphs = [];
7076
+ // Destroy network controller
7077
+ if (this._networkController) {
7078
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.info(this, "🔴 [DESTROY] Destroying NetworkController...");
7079
+ try {
7080
+ (_d = this._networkController.getConnection()) === null || _d === void 0 ? void 0 : _d.destroy();
7081
+ } catch (e) {/* ignore */}
7082
+ this._networkController = null;
7083
+ }
7084
+ // Destroy streamer controller (this is also sync now)
7085
+ if (this._streamerController) {
7086
+ (_e = this._logger) === null || _e === void 0 ? void 0 : _e.info(this, "🔴 [DESTROY] Destroying StreamerController...");
7087
+ this._streamerController.destroy();
7088
+ this._streamerController = null;
7089
+ }
7090
+ // Destroy stage controller
7091
+ if (this._stageController) {
7092
+ (_f = this._logger) === null || _f === void 0 ? void 0 : _f.info(this, "🔴 [DESTROY] Destroying StageController...");
7093
+ try {
7094
+ this._stageController.destroy();
7095
+ } catch (e) {/* ignore */}
7096
+ this._stageController = null;
7097
+ }
7098
+ // Clear other references
7099
+ this._storageManager = null;
7100
+ this._statsController = null;
7101
+ // Remove all event listeners
7073
7102
  this.removeAllEventListeners();
7103
+ (_g = this._logger) === null || _g === void 0 ? void 0 : _g.success(this, "🔴 [DESTROY] Streamer instance destroyed successfully (sync)");
7074
7104
  }
7075
7105
  }
7076
- /**
7077
- * Next ID for the streamer instance. Each subsequent instance has a higher number.
7078
- * @private
7079
- */
7080
7106
  StormStreamer.NEXT_STREAMER_ID = 0;
7081
7107
 
7082
7108
  function create(config) {