@stormstreaming/stormstreamer 1.0.0-rc.1 → 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.1
8
- * Version: 11/17/2025, 10:30:25 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
@@ -3672,13 +3672,13 @@
3672
3672
  this._lastEventTime = 0;
3673
3673
  this.THROTTLE_INTERVAL = 100; // ms between updates
3674
3674
  this._isMonitoring = false;
3675
- // POPRAWKA: Dodane pole do przechowywania ID animacji
3676
3675
  this._animationFrameId = null;
3677
- // Moving average variables for smoothing
3678
- this._instant = 0.0;
3679
- this._slow = 0.0;
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;
3680
3681
  this._main = main;
3681
- console.log('SoundMeter: Created new instance');
3682
3682
  }
3683
3683
  attach(stream) {
3684
3684
  if (!stream.getAudioTracks().length) {
@@ -3693,30 +3693,24 @@
3693
3693
  this._microphone = this._audioContext.createMediaStreamSource(stream);
3694
3694
  this._analyser = this._audioContext.createAnalyser();
3695
3695
  this._analyser.fftSize = 2048;
3696
- this._analyser.smoothingTimeConstant = 0.3;
3697
3696
  // Connect nodes
3698
3697
  this._microphone.connect(this._analyser);
3699
3698
  // Start monitoring
3700
3699
  this.startMonitoring();
3701
3700
  } catch (error) {
3702
3701
  console.error('SoundMeter: Error during attach:', error);
3703
- this.detach(); // Cleanup on error
3702
+ this.detach();
3704
3703
  }
3705
3704
  }
3706
-
3707
3705
  detach() {
3708
- // POPRAWKA: Zabezpieczenie przed wielokrotnym wywołaniem
3709
3706
  if (!this._audioContext && !this._analyser && !this._microphone) {
3710
- return; // Już wyczyszczone
3707
+ return;
3711
3708
  }
3712
- // Stop monitoring first
3713
3709
  this._isMonitoring = false;
3714
- // POPRAWKA: Anulowanie requestAnimationFrame
3715
3710
  if (this._animationFrameId !== null) {
3716
3711
  cancelAnimationFrame(this._animationFrameId);
3717
3712
  this._animationFrameId = null;
3718
3713
  }
3719
- // Disconnect and cleanup nodes in reverse order
3720
3714
  if (this._microphone) {
3721
3715
  try {
3722
3716
  this._microphone.disconnect();
@@ -3735,13 +3729,9 @@
3735
3729
  }
3736
3730
  if (this._audioContext) {
3737
3731
  try {
3738
- // POPRAWKA: Sprawdzenie przed użyciem w Promise
3739
3732
  if (this._audioContext.state !== 'closed') {
3740
- // Zapisz referencję przed asynchroniczną operacją
3741
3733
  const audioContextRef = this._audioContext;
3742
- // Najpierw ustaw na null, żeby uniknąć podwójnego wywołania
3743
3734
  this._audioContext = null;
3744
- // Teraz bezpiecznie zamknij
3745
3735
  audioContextRef.suspend().then(() => audioContextRef.close()).catch(e => {
3746
3736
  console.warn('SoundMeter: Error closing audio context:', e);
3747
3737
  });
@@ -3765,28 +3755,29 @@
3765
3755
  this._isMonitoring = true;
3766
3756
  const dataArray = new Float32Array(this._analyser.frequencyBinCount);
3767
3757
  const analyze = () => {
3768
- // POPRAWKA: Dodatkowe sprawdzenie przed kontynuacją
3769
3758
  if (!this._analyser || !this._isMonitoring || !this._audioContext) {
3770
3759
  this._animationFrameId = null;
3771
3760
  return;
3772
3761
  }
3773
3762
  const now = Date.now();
3774
3763
  try {
3775
- // Read time-domain data
3776
3764
  this._analyser.getFloatTimeDomainData(dataArray);
3777
- // Calculate levels
3778
- let sum = 0.0;
3779
- let clipcount = 0;
3765
+ // Peak detection - find maximum amplitude
3766
+ let peak = 0.0;
3780
3767
  for (let i = 0; i < dataArray.length; ++i) {
3781
- const amplitude = dataArray[i];
3782
- sum += amplitude * amplitude;
3783
- if (Math.abs(amplitude) > 0.99) {
3784
- clipcount += 1;
3768
+ const amplitude = Math.abs(dataArray[i]);
3769
+ if (amplitude > peak) {
3770
+ peak = amplitude;
3785
3771
  }
3786
3772
  }
3787
- // Update metrics
3788
- this._instant = Math.sqrt(sum / dataArray.length);
3789
- 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
+ }
3790
3781
  // Throttle event dispatch
3791
3782
  if (now - this._lastEventTime >= this.THROTTLE_INTERVAL) {
3792
3783
  this._lastEventTime = now;
@@ -3802,17 +3793,13 @@
3802
3793
  this._animationFrameId = null;
3803
3794
  return;
3804
3795
  }
3805
- // Schedule next analysis only if still monitoring
3806
3796
  if (this._isMonitoring) {
3807
3797
  this._animationFrameId = requestAnimationFrame(analyze);
3808
3798
  }
3809
3799
  };
3810
- // Start analysis loop
3811
3800
  this._animationFrameId = requestAnimationFrame(analyze);
3812
3801
  }
3813
- // POPRAWKA: Dodana metoda destroy
3814
3802
  destroy() {
3815
- console.log('SoundMeter: Destroying instance');
3816
3803
  this.detach();
3817
3804
  }
3818
3805
  }
@@ -4331,7 +4318,7 @@
4331
4318
  * @param config
4332
4319
  */
4333
4320
  constructor(main) {
4334
- var _a, _b, _c;
4321
+ var _a, _b, _c, _d;
4335
4322
  /**
4336
4323
  * Whenever current window is active or not
4337
4324
  * @private
@@ -4345,7 +4332,7 @@
4345
4332
  'iceServers': []
4346
4333
  };
4347
4334
  /**
4348
- * Whenever microphone is currenly muted
4335
+ * Whenever microphone is currently muted
4349
4336
  * @private
4350
4337
  */
4351
4338
  this._isMicrophoneMuted = false;
@@ -4355,7 +4342,7 @@
4355
4342
  */
4356
4343
  this._pendingMicrophoneState = null;
4357
4344
  /**
4358
- * Whenever we have cheched for permissions
4345
+ * Whenever we have checked for permissions
4359
4346
  * @private
4360
4347
  */
4361
4348
  this._permissionChecked = false;
@@ -4418,58 +4405,83 @@
4418
4405
  this._currentOrientation = ((_a = window.screen.orientation) === null || _a === void 0 ? void 0 : _a.type) || '';
4419
4406
  this._statusTimer = null;
4420
4407
  this._debug = false;
4421
- // NOWE POLA DLA ANULOWANIA OPERACJI
4408
+ // FIELDS FOR OPERATION CANCELLATION
4422
4409
  this._deviceChangeHandler = null;
4423
4410
  this._orientationChangeHandler = null;
4424
- this._isDestroying = false;
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;
4425
4417
  this._cameraAbortController = null;
4426
4418
  this._microphoneAbortController = null;
4427
4419
  this._startCameraAbortController = null;
4428
4420
  this._switchingCamera = false;
4429
4421
  this._switchingMicrophone = false;
4430
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;
4431
4440
  /**
4432
4441
  * Handles device state changes and initiates publishing if appropriate
4433
4442
  * @private
4434
4443
  */
4435
4444
  this.onDeviceStateChange = event => {
4436
4445
  var _a;
4446
+ if (this._isDestroyed) return;
4437
4447
  const usedStreamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
4438
4448
  if (event.state == exports.InputDevicesState.READY && usedStreamKey != null) {
4439
4449
  this.publish(usedStreamKey);
4440
4450
  }
4441
4451
  };
4442
4452
  /**
4443
- * This method is responsible for handing orientation changes for mobile devices
4453
+ * Handles orientation changes for mobile devices
4444
4454
  */
4445
4455
  this.handleOrientationChange = () => __awaiter(this, void 0, void 0, function* () {
4446
- var _d, _e;
4447
- if (this._isDestroying) return;
4448
- // Dajemy chwilę na ustabilizowanie się orientacji
4456
+ var _e, _f;
4457
+ if (this._isDestroyed) return;
4449
4458
  yield new Promise(resolve => setTimeout(resolve, 500));
4450
- 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) || '';
4451
4461
  if (this._currentOrientation !== newOrientation) {
4452
4462
  this._logger.info(this, `Orientation changed from ${this._currentOrientation} to ${newOrientation}`);
4453
4463
  this._currentOrientation = newOrientation;
4454
- // Zapamiętaj aktualny stream key i stan publikacji
4455
- 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;
4456
4465
  this._publishState === exports.PublishState.PUBLISHED;
4457
- // Zamknij istniejące połączenie i stream
4458
4466
  this.closeWebRTCConnection();
4459
4467
  if (this._stream) {
4468
+ this._logger.info(this, "📹 [RELEASE] handleOrientationChange() - releasing stream for orientation change");
4460
4469
  this._stream.getTracks().forEach(track => {
4461
4470
  track.stop();
4471
+ this._logger.info(this, `📹 [RELEASE] handleOrientationChange() - stopped track: ${track.kind}`);
4462
4472
  });
4463
4473
  this._stream = null;
4474
+ this._activeStreamCount--;
4464
4475
  }
4465
- // Rozpocznij wszystko od nowa
4476
+ if (this._isDestroyed) return;
4466
4477
  try {
4467
4478
  yield this.startCamera();
4468
- // Jeśli stream był opublikowany, publikujemy ponownie
4469
- if (streamKey && !this._isDestroying) {
4479
+ if (this._isDestroyed) return;
4480
+ if (streamKey) {
4470
4481
  this.publish(streamKey);
4471
4482
  }
4472
4483
  } catch (error) {
4484
+ if (this._isDestroyed) return;
4473
4485
  this._logger.error(this, "Error restarting stream after orientation change: " + JSON.stringify(error));
4474
4486
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4475
4487
  }
@@ -4478,10 +4490,8 @@
4478
4490
  this.onServerDisconnect = () => {
4479
4491
  // Implementation
4480
4492
  };
4481
- /**
4482
- * Method for handling a situation when a given streamKey is already in use.
4483
- */
4484
4493
  this.onStreamKeyTaken = () => {
4494
+ if (this._isDestroyed) return;
4485
4495
  if (this._restartTimer != null) {
4486
4496
  clearInterval(this._restartTimer);
4487
4497
  this._restartTimerCount = 0;
@@ -4490,6 +4500,10 @@
4490
4500
  this.setPublishState(exports.PublishState.ERROR);
4491
4501
  this._restartTimer = setInterval(() => {
4492
4502
  var _a, _b;
4503
+ if (this._isDestroyed) {
4504
+ if (this._restartTimer) clearInterval(this._restartTimer);
4505
+ return;
4506
+ }
4493
4507
  if (this._restartTimer != null) {
4494
4508
  if (this._restartTimerCount < this._restartTimerMaxCount) {
4495
4509
  this._logger.info(this, "WebRTCStreamer :: StreamKeyTaken Interval: " + this._restartTimerCount + "/" + this._restartTimerMaxCount);
@@ -4500,41 +4514,37 @@
4500
4514
  this._restartTimerCount = 0;
4501
4515
  const streamData = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData();
4502
4516
  const streamKey = streamData === null || streamData === void 0 ? void 0 : streamData.streamKey;
4503
- if (streamKey != null) {
4504
- const prevStreamKey = streamKey;
4505
- this.publish(prevStreamKey);
4517
+ if (streamKey != null && !this._isDestroyed) {
4518
+ this.publish(streamKey);
4506
4519
  }
4507
4520
  }
4508
- const usedStreamKey = (_b = this._main.getConfigManager().getStreamData().streamKey) !== null && _b !== void 0 ? _b : "unknown";
4509
- this._main.dispatchEvent("streamKeyInUseInterval", {
4510
- ref: this._main,
4511
- streamKey: usedStreamKey,
4512
- count: this._restartTimerCount,
4513
- maxCount: this._restartTimerMaxCount
4514
- });
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
+ }
4515
4530
  }
4516
4531
  }, 1000);
4517
4532
  };
4518
4533
  //------------------------------------------------------------------------//
4519
4534
  // NETWORK AND RTC
4520
4535
  //------------------------------------------------------------------------//
4521
- /**
4522
- * Method fires once a connection with wowza is established. It's the main connection where we exchange
4523
- * ice-candidates and SDP. We'll start setting up peer connections.
4524
- */
4525
4536
  this.onServerConnect = () => {
4537
+ if (this._isDestroyed) return;
4526
4538
  if (this._peerConnection) {
4527
4539
  this.closeWebRTCConnection();
4528
4540
  }
4529
4541
  this._peerConnection = new RTCPeerConnection(this._peerConnectionConfig);
4530
- // Najpierw dodaj tracki
4531
4542
  if (this._stream) {
4532
4543
  let localTracks = this._stream.getTracks();
4533
4544
  for (let localTrack in localTracks) {
4534
4545
  this._peerConnection.addTrack(localTracks[localTrack], this._stream);
4535
4546
  }
4536
4547
  }
4537
- // Potem dodaj event handlery
4538
4548
  this._peerConnection.onicecandidate = event => {
4539
4549
  this.onIceCandidate(event);
4540
4550
  };
@@ -4542,26 +4552,29 @@
4542
4552
  this.onConnectionStateChange(event);
4543
4553
  };
4544
4554
  this._peerConnection.onnegotiationneeded = event => __awaiter(this, void 0, void 0, function* () {
4545
- if (this._peerConnection) {
4555
+ if (this._peerConnection && !this._isDestroyed) {
4546
4556
  try {
4547
4557
  const description = yield this._peerConnection.createOffer();
4548
- yield this.onDescriptionSuccess(description);
4558
+ if (!this._isDestroyed) {
4559
+ yield this.onDescriptionSuccess(description);
4560
+ }
4549
4561
  } catch (error) {
4550
4562
  this.onDescriptionError(error);
4551
- console.error('Error creating offer:', error);
4552
4563
  }
4553
4564
  }
4554
4565
  });
4555
4566
  this.createStatusConnection();
4556
4567
  };
4557
- /**
4558
- * Method fires once a status connection is established. We'll set an interval for monitoring stream status.
4559
- */
4560
4568
  this.onStatusServerConnect = () => {
4569
+ if (this._isDestroyed) return;
4561
4570
  const usedStreamKey = this._main.getConfigManager().getStreamData().streamKey;
4562
4571
  if (this._statusTimer == null) {
4563
4572
  if (this._statusConnection != null && usedStreamKey != null) {
4564
4573
  this._statusTimer = setInterval(() => {
4574
+ if (this._isDestroyed) {
4575
+ if (this._statusTimer) clearInterval(this._statusTimer);
4576
+ return;
4577
+ }
4565
4578
  this.requestStatusData();
4566
4579
  }, 1000);
4567
4580
  }
@@ -4569,27 +4582,22 @@
4569
4582
  };
4570
4583
  this.requestStatusData = () => {
4571
4584
  var _a;
4585
+ if (this._isDestroyed) return;
4572
4586
  (_a = this._statusConnection) === null || _a === void 0 ? void 0 : _a.sendData('{"packetID":"STREAM_STATUS", "streamKey": "' + this._fullStreamName + '"}');
4573
4587
  };
4574
- /**
4575
- * If for some reason the status connection is disconnected we have to clean the interval
4576
- */
4577
4588
  this.onStatusServerDisconnect = () => {
4578
4589
  if (this._statusTimer != null) clearInterval(this._statusTimer);
4579
4590
  this._statusTimer = null;
4580
4591
  };
4581
- /**
4582
- * This event fires whenever "STREAM_STATUS_RESPONSE" packet form status connection reports stream status along some stream data. This gives
4583
- * us an insight into whenever our stream is ok (works) or not.
4584
- * @param event
4585
- */
4586
4592
  this.onStreamStatsUpdate = event => {
4593
+ if (this._isDestroyed) return;
4587
4594
  const update = event.streamStatus;
4588
4595
  if (this._publishState == exports.PublishState.PUBLISHED && update.publishState != exports.PublishState.PUBLISHED) this.setPublishState(exports.PublishState.UNPUBLISHED);
4589
4596
  if (this._publishState == exports.PublishState.CONNECTED && update.publishState == exports.PublishState.PUBLISHED) this.setPublishState(exports.PublishState.PUBLISHED);
4590
4597
  };
4591
4598
  this.onDescriptionSuccess = description => {
4592
4599
  var _a, _b, _c;
4600
+ if (this._isDestroyed) return;
4593
4601
  this._fullStreamName = ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey) + "_" + new Date().getTime();
4594
4602
  const streamInfo = {
4595
4603
  applicationName: (_c = (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.getConnection().getCurrentServer()) === null || _c === void 0 ? void 0 : _c.getApplication(),
@@ -4603,22 +4611,19 @@
4603
4611
  videoCodec: "42e01f",
4604
4612
  audioCodec: "opus"
4605
4613
  });
4606
- if (this._peerConnection) {
4614
+ if (this._peerConnection && !this._isDestroyed) {
4607
4615
  this._peerConnection.setLocalDescription(description).then(() => {
4608
4616
  var _a;
4617
+ if (this._isDestroyed) return;
4609
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) + '}');
4610
4619
  }).catch(error => {
4611
4620
  console.log(error);
4612
- //this.onWebRTCError(error, self);
4613
4621
  });
4614
4622
  }
4615
4623
  };
4616
4624
  //------------------------------------------------------------------------//
4617
4625
  // BLUR & FOCUS
4618
4626
  //------------------------------------------------------------------------//
4619
- /**
4620
- * Methods handles visibility change events
4621
- */
4622
4627
  this.visibilityChange = () => {
4623
4628
  if (document.visibilityState === 'hidden') {
4624
4629
  this.onWindowBlur();
@@ -4626,18 +4631,12 @@
4626
4631
  this.onWindowFocus();
4627
4632
  }
4628
4633
  };
4629
- /**
4630
- * Reacts to browser changing visibility of the document (or blur)
4631
- */
4632
4634
  this.onWindowBlur = () => {
4633
4635
  if (this._isWindowActive) {
4634
4636
  this._logger.warning(this, "Player window is no longer in focus!");
4635
4637
  }
4636
4638
  this._isWindowActive = false;
4637
4639
  };
4638
- /**
4639
- * Reacts to browser changing visibility of the document (or focus)
4640
- */
4641
4640
  this.onWindowFocus = () => {
4642
4641
  if (!this._isWindowActive) {
4643
4642
  this._logger.info(this, "Player window is focused again!");
@@ -4649,38 +4648,76 @@
4649
4648
  this._mungeSDP = new MungeSDP();
4650
4649
  this._soundMeter = new SoundMeter(this._main);
4651
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
+ }
4652
4657
  // Start initialization process
4653
4658
  this.initialize();
4654
4659
  }
4655
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
+ //------------------------------------------------------------------------//
4656
4679
  // MAIN METHODS
4657
4680
  //------------------------------------------------------------------------//
4658
4681
  /**
4659
4682
  * Initializes the PlaybackController by setting up event listeners and device handling
4660
- * This method orchestrates the initialization process by first checking device availability
4661
- * and permissions, then setting up necessary event listeners and configurations
4662
4683
  * @private
4663
4684
  */
4664
4685
  initialize() {
4665
4686
  var _a, _b;
4666
4687
  return __awaiter(this, void 0, void 0, function* () {
4688
+ this._logger.info(this, "📹 [INIT] initialize() - starting initialization");
4667
4689
  try {
4690
+ if (this.isDestroyedCheck("initialize start")) return;
4668
4691
  yield this.initializeDevices();
4692
+ if (this.isDestroyedCheck("after initializeDevices")) return;
4669
4693
  this.setupEventListeners();
4694
+ if (this.isDestroyedCheck("after setupEventListeners")) return;
4670
4695
  this.initializeStream();
4696
+ if (this.isDestroyedCheck("after initializeStream")) return;
4671
4697
  this.setupOrientationListener();
4672
- this.setupPermissionListeners();
4698
+ yield this.setupPermissionListeners();
4699
+ if (this.isDestroyedCheck("after setupPermissionListeners")) return;
4673
4700
  this.setupDeviceChangeListener();
4701
+ if (this.isDestroyedCheck("before network init")) return;
4674
4702
  if ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getSettingsData().autoConnect) {
4675
4703
  this._logger.info(this, "Initializing NetworkController (autoConnect is true)");
4676
4704
  (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.initialize();
4677
4705
  } else {
4678
4706
  this._logger.warning(this, "Warning - autoConnect is set to false, switching to standby mode!");
4679
4707
  }
4708
+ this._logger.success(this, "📹 [INIT] initialize() - completed successfully");
4680
4709
  } catch (error) {
4710
+ if (this._isDestroyed) {
4711
+ this._logger.warning(this, "📹 [INIT] initialize() - aborted by destroy()");
4712
+ return;
4713
+ }
4681
4714
  this._logger.error(this, "Initialization failed: " + JSON.stringify(error));
4682
4715
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4683
4716
  }
4717
+ this._main.dispatchEvent("microphoneStateChange", {
4718
+ ref: this._main,
4719
+ isMuted: this._isMicrophoneMuted
4720
+ });
4684
4721
  });
4685
4722
  }
4686
4723
  /**
@@ -4688,6 +4725,7 @@
4688
4725
  * @private
4689
4726
  */
4690
4727
  setupEventListeners() {
4728
+ if (this._isDestroyed) return;
4691
4729
  this._main.addEventListener("serverConnect", this.onServerConnect, false);
4692
4730
  this._main.addEventListener("serverDisconnect", this.onServerDisconnect, false);
4693
4731
  this._main.addEventListener("streamKeyInUse", this.onStreamKeyTaken, false);
@@ -4706,19 +4744,38 @@
4706
4744
  initializeDevices() {
4707
4745
  return __awaiter(this, void 0, void 0, function* () {
4708
4746
  try {
4747
+ this._logger.info(this, "📹 [ACQUIRE] initializeDevices() - requesting test getUserMedia for permissions");
4748
+ if (this.isDestroyedCheck("initializeDevices before getUserMedia")) return;
4709
4749
  // Request initial device permissions
4710
4750
  const stream = yield navigator.mediaDevices.getUserMedia({
4711
4751
  video: true,
4712
4752
  audio: true
4713
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");
4714
4764
  // Stop the test stream
4715
- 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;
4716
4770
  // Initialize device lists
4717
4771
  yield this.grabDevices();
4718
4772
  } catch (error) {
4773
+ if (this._isDestroyed) return;
4719
4774
  console.log(error);
4720
4775
  this._logger.error(this, "Error initializing devices: " + JSON.stringify(error));
4721
- yield this.grabDevices();
4776
+ if (!this._isDestroyed) {
4777
+ yield this.grabDevices();
4778
+ }
4722
4779
  }
4723
4780
  });
4724
4781
  }
@@ -4727,38 +4784,73 @@
4727
4784
  * @private
4728
4785
  */
4729
4786
  initializeStream() {
4787
+ if (this._isDestroyed) return;
4730
4788
  if (this._selectedCamera || this._selectedMicrophone) {
4731
4789
  this.startCamera();
4732
4790
  }
4733
4791
  }
4792
+ /**
4793
+ * Sets up permission listeners with proper cleanup tracking
4794
+ * @private
4795
+ */
4734
4796
  setupPermissionListeners() {
4735
- const cameraQuery = {
4736
- name: 'camera'
4737
- };
4738
- const microphoneQuery = {
4739
- name: 'microphone'
4740
- };
4741
- navigator.permissions.query(cameraQuery).then(permissionStatus => {
4742
- permissionStatus.onchange = () => {
4743
- this._permissionChecked = false;
4744
- this.handlePermissionChange('camera', permissionStatus.state);
4745
- };
4746
- });
4747
- navigator.permissions.query(microphoneQuery).then(permissionStatus => {
4748
- permissionStatus.onchange = () => {
4749
- this._permissionChecked = false;
4750
- this.handlePermissionChange('microphone', permissionStatus.state);
4751
- };
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
+ }
4752
4827
  });
4753
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
+ }
4754
4845
  /**
4755
4846
  * Sets up event listener for device changes and handles them appropriately
4756
4847
  * @private
4757
4848
  */
4758
4849
  setupDeviceChangeListener() {
4850
+ if (this._isDestroyed) return;
4759
4851
  this._deviceChangeHandler = () => __awaiter(this, void 0, void 0, function* () {
4760
4852
  var _a;
4761
- if (this._isDestroying) return;
4853
+ if (this._isDestroyed) return;
4762
4854
  if (this._publishState === exports.PublishState.PUBLISHED) {
4763
4855
  this._logger.info(this, "Device change detected, but already publish - no restarting streamer");
4764
4856
  return;
@@ -4769,37 +4861,49 @@
4769
4861
  try {
4770
4862
  this.stop();
4771
4863
  yield new Promise(resolve => setTimeout(resolve, 500));
4772
- if (!this._isDestroying) {
4773
- yield this.start();
4774
- if (wasPublishing && streamKey && !this._isDestroying) {
4775
- this._logger.info(this, "Resuming publishing after device change");
4776
- if (this.isStreamReady(true, true)) {
4777
- this.publish(streamKey);
4778
- } else {
4779
- this._logger.warning(this, "Cannot resume publishing - stream not ready after device change");
4780
- this._main.dispatchEvent("inputDeviceError", {
4781
- ref: this._main
4782
- });
4783
- }
4864
+ if (this._isDestroyed) return;
4865
+ yield this.start();
4866
+ if (this._isDestroyed) return;
4867
+ if (wasPublishing && streamKey) {
4868
+ this._logger.info(this, "Resuming publishing after device change");
4869
+ if (this.isStreamReady(true, true)) {
4870
+ this.publish(streamKey);
4871
+ } else {
4872
+ this._logger.warning(this, "Cannot resume publishing - stream not ready after device change");
4873
+ this._main.dispatchEvent("inputDeviceError", {
4874
+ ref: this._main
4875
+ });
4784
4876
  }
4785
4877
  }
4786
4878
  this._logger.success(this, "Successfully handled device change");
4787
4879
  } catch (error) {
4880
+ if (this._isDestroyed) return;
4788
4881
  this._logger.error(this, "Error handling device change: " + JSON.stringify(error));
4789
4882
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4790
4883
  }
4791
4884
  });
4792
4885
  navigator.mediaDevices.addEventListener('devicechange', this._deviceChangeHandler);
4793
4886
  }
4887
+ /**
4888
+ * Handles permission changes for camera or microphone
4889
+ * @private
4890
+ */
4794
4891
  handlePermissionChange(device, state) {
4795
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}`);
4796
4895
  if (state === 'denied') {
4797
4896
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
4798
- if (this._publishState == exports.PublishState.CONNECTED) {
4799
- 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();
4800
4902
  }
4801
4903
  }
4904
+ if (this._isDestroyed) return;
4802
4905
  yield this.grabDevices();
4906
+ if (this._isDestroyed) return;
4803
4907
  if (state === 'granted') {
4804
4908
  yield this.startCamera();
4805
4909
  }
@@ -4809,7 +4913,17 @@
4809
4913
  * Handles successful camera stream initialization
4810
4914
  */
4811
4915
  onCameraStreamSuccess(stream) {
4812
- 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}`);
4813
4927
  // Get actual stream dimensions
4814
4928
  const videoTrack = stream.getVideoTracks()[0];
4815
4929
  const audioTrack = stream.getAudioTracks()[0];
@@ -4818,7 +4932,6 @@
4818
4932
  const settings = videoTrack.getSettings();
4819
4933
  let width = settings.width;
4820
4934
  let height = settings.height;
4821
- // Na urządzeniach mobilnych potrzebujemy skorygować wymiary
4822
4935
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
4823
4936
  if (isMobile) {
4824
4937
  if (width > height && window.innerWidth < window.innerHeight || width < height && window.innerWidth > window.innerHeight) {
@@ -4828,12 +4941,10 @@
4828
4941
  streamData.streamWidth = width;
4829
4942
  streamData.streamHeight = height;
4830
4943
  streamData.videoTrackPresent = true;
4831
- // Obliczanie proporcji i konwersja na string
4832
4944
  const gcd = (a, b) => b ? gcd(b, a % b) : a;
4833
4945
  const divisor = gcd(width, height);
4834
4946
  const widthRatio = width / divisor;
4835
4947
  const heightRatio = height / divisor;
4836
- // Sprawdzamy typowe proporcje z pewną tolerancją
4837
4948
  const ratio = width / height;
4838
4949
  let aspectRatioString = `${widthRatio}:${heightRatio}`;
4839
4950
  if (Math.abs(ratio - 16 / 9) < 0.1) {
@@ -4842,11 +4953,22 @@
4842
4953
  aspectRatioString = width > height ? "4:3" : "3:4";
4843
4954
  }
4844
4955
  streamData.aspectRatio = aspectRatioString;
4956
+ this._logger.info(this, `📹 [INFO] Video track - id: ${videoTrack.id}, enabled: ${videoTrack.enabled}, readyState: ${videoTrack.readyState}`);
4845
4957
  } else {
4846
4958
  streamData.videoTrackPresent = false;
4847
4959
  }
4960
+ if (audioTrack) {
4961
+ this._logger.info(this, `📹 [INFO] Audio track - id: ${audioTrack.id}, enabled: ${audioTrack.enabled}, readyState: ${audioTrack.readyState}`);
4962
+ }
4848
4963
  streamData.audioTrackPresent = !!audioTrack;
4849
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
+ }
4850
4972
  // Dispatch event with stream data
4851
4973
  this._main.dispatchEvent("publishMetadataUpdate", {
4852
4974
  ref: this._main,
@@ -4865,7 +4987,7 @@
4865
4987
  if (this._cameraList == null || this._microphoneList == null) {
4866
4988
  this.grabDevices();
4867
4989
  }
4868
- const videoElement = this._main.getStageController().getScreenElement().getVideoElement();
4990
+ const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
4869
4991
  if (videoElement) {
4870
4992
  videoElement.srcObject = stream;
4871
4993
  videoElement.autoplay = true;
@@ -4881,6 +5003,7 @@
4881
5003
  */
4882
5004
  initializeWebRTC() {
4883
5005
  var _a;
5006
+ if (this._isDestroyed) return;
4884
5007
  if (!this._stream) {
4885
5008
  this._logger.error(this, "Cannot initialize WebRTC - no camera stream available");
4886
5009
  return;
@@ -4894,12 +5017,10 @@
4894
5017
  }
4895
5018
  }
4896
5019
  /**
4897
- * Modified publish method to handle both camera and WebRTC
4898
- *
4899
- * @param streamKey - klucz streamu
4900
- * @returns {boolean} - true jeśli udało się rozpocząć publikowanie
5020
+ * Publish method
4901
5021
  */
4902
5022
  publish(streamKey) {
5023
+ if (this._isDestroyed) return false;
4903
5024
  if (this._debug) this._logger.decoratedLog("Publishing: " + streamKey, "dark-red");
4904
5025
  this._logger.info(this, "Publish: " + streamKey);
4905
5026
  if (this._statusTimer != null) clearInterval(this._statusTimer);
@@ -4920,8 +5041,12 @@
4920
5041
  this._firstPublish = false;
4921
5042
  return true;
4922
5043
  }
5044
+ /**
5045
+ * Stops publishing and cleans up WebRTC connection
5046
+ */
4923
5047
  unpublish() {
4924
5048
  if (this._debug) this._logger.decoratedLog("Unpublish", "dark-red");
5049
+ this._logger.info(this, "📹 [UNPUBLISH] unpublish() - stopping WebRTC but keeping camera preview");
4925
5050
  if (this._statusConnection != null) {
4926
5051
  this._statusConnection.destroy();
4927
5052
  this._statusConnection = null;
@@ -4935,13 +5060,23 @@
4935
5060
  this._main.dispatchEvent("unpublish", {
4936
5061
  ref: this._main
4937
5062
  });
4938
- this.closeStream();
5063
+ this.closeWebRTCConnection();
5064
+ this.setPublishState(exports.PublishState.UNPUBLISHED);
5065
+ }
5066
+ /**
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();
4939
5073
  }
4940
5074
  /**
4941
- * This method
5075
+ * Sets up orientation listener
4942
5076
  * @private
4943
5077
  */
4944
5078
  setupOrientationListener() {
5079
+ if (this._isDestroyed) return;
4945
5080
  this._orientationChangeHandler = this.handleOrientationChange;
4946
5081
  if (window.screen && window.screen.orientation) {
4947
5082
  window.screen.orientation.addEventListener('change', this._orientationChangeHandler);
@@ -4953,21 +5088,20 @@
4953
5088
  // USER MEDIA
4954
5089
  //------------------------------------------------------------------------//
4955
5090
  /**
4956
- * Error on trying to grab video stream (it usually means that browser does not support WebRTC streaming)
4957
- *
4958
- * @param error
5091
+ * Error handler for getUserMedia
4959
5092
  */
4960
5093
  onUserMediaError(error) {
4961
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}`);
4962
5097
  yield this.grabDevices();
4963
- // Dodatkowa obsługa specyficznych błędów getUserMedia, jeśli potrzebna
4964
5098
  if (error.name === "OverconstrainedError") {
4965
5099
  this._logger.warning(this, "Device constraints not satisfied");
4966
5100
  }
4967
5101
  });
4968
5102
  }
4969
5103
  /**
4970
- * This method is used for checking individual access to output devices
5104
+ * Checks individual device access
4971
5105
  * @private
4972
5106
  */
4973
5107
  checkIndividualDeviceAccess() {
@@ -4982,44 +5116,47 @@
4982
5116
  available: false
4983
5117
  }
4984
5118
  };
4985
- // Sprawdzamy fizyczną dostępność urządzeń
5119
+ if (this._isDestroyed) return results;
4986
5120
  try {
4987
5121
  const devices = yield navigator.mediaDevices.enumerateDevices();
5122
+ if (this._isDestroyed) return results;
4988
5123
  results.camera.available = devices.some(device => device.kind === 'videoinput');
4989
5124
  results.microphone.available = devices.some(device => device.kind === 'audioinput');
4990
- // Sprawdzamy czy mamy etykiety urządzeń - ich brak może oznaczać brak uprawnień
4991
5125
  const hasLabels = devices.some(device => device.label !== '');
4992
5126
  if (!hasLabels) {
4993
- // 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;
4994
5129
  try {
4995
5130
  const stream = yield navigator.mediaDevices.getUserMedia({
4996
5131
  video: results.camera.available,
4997
5132
  audio: results.microphone.available
4998
5133
  });
4999
- // 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
+ }
5000
5140
  results.camera.allowed = stream.getVideoTracks().length > 0;
5001
5141
  results.microphone.allowed = stream.getAudioTracks().length > 0;
5002
- // Zatrzymujemy strumień testowy
5003
- stream.getTracks().forEach(track => track.stop());
5004
- // 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;
5005
5148
  yield this.grabDevices();
5006
5149
  } catch (error) {
5007
5150
  console.error('Error requesting permissions:', error);
5008
- // Nie udało się uzyskać uprawnień
5009
5151
  results.camera.allowed = false;
5010
5152
  results.microphone.allowed = false;
5011
5153
  }
5012
5154
  } else {
5013
- // Jeśli mamy etykiety, prawdopodobnie mamy już uprawnienia
5014
5155
  results.camera.allowed = devices.some(device => device.kind === 'videoinput' && device.label !== '');
5015
5156
  results.microphone.allowed = devices.some(device => device.kind === 'audioinput' && device.label !== '');
5016
5157
  }
5017
5158
  } catch (error) {
5018
5159
  console.error('Error checking devices:', error);
5019
- results.camera.available = false;
5020
- results.microphone.available = false;
5021
- results.camera.allowed = false;
5022
- results.microphone.allowed = false;
5023
5160
  }
5024
5161
  return results;
5025
5162
  });
@@ -5028,21 +5165,18 @@
5028
5165
  // SOCKETS & SDP
5029
5166
  //------------------------------------------------------------------------//
5030
5167
  /**
5031
- * This method handles basic SDP/ICE-Candidate exchange with a Wowza Server
5032
- *
5033
- * @param data
5168
+ * Handles SDP/ICE-Candidate exchange
5034
5169
  */
5035
5170
  onSocketMessage(data) {
5036
5171
  var _a;
5172
+ if (this._isDestroyed) return;
5037
5173
  let msgJSON = JSON.parse(data);
5038
5174
  let msgStatus = Number(msgJSON["status"]);
5039
5175
  switch (msgStatus) {
5040
5176
  case 200:
5041
- // OK
5042
5177
  this._logger.info(this, "SDP Exchange Successful");
5043
5178
  let sdpData = msgJSON['sdp'];
5044
- if (sdpData !== undefined) {
5045
- // @ts-ignore
5179
+ if (sdpData !== undefined && this._peerConnection) {
5046
5180
  this._peerConnection.setRemoteDescription(new RTCSessionDescription(sdpData), () => {}, () => {});
5047
5181
  }
5048
5182
  let iceCandidates = msgJSON['iceCandidates'];
@@ -5053,7 +5187,6 @@
5053
5187
  }
5054
5188
  break;
5055
5189
  case 503:
5056
- // NOT OK
5057
5190
  this._logger.error(this, "StreamKey already use");
5058
5191
  const usedStreamKey = (_a = this._main.getConfigManager().getStreamData().streamKey) !== null && _a !== void 0 ? _a : "unknown";
5059
5192
  this._main.dispatchEvent("streamKeyInUse", {
@@ -5067,14 +5200,8 @@
5067
5200
  //------------------------------------------------------------------------//
5068
5201
  // EVENTS
5069
5202
  //------------------------------------------------------------------------//
5070
- /**
5071
- * Recives events related to peerConnection (change of state)
5072
- *
5073
- * @param event event with its data
5074
- * @param thisRef reference to player classonConnectionStateChange
5075
- * @private
5076
- */
5077
5203
  onConnectionStateChange(event) {
5204
+ if (this._isDestroyed) return;
5078
5205
  this._logger.info(this, "Connection State Change: " + JSON.stringify(event));
5079
5206
  if (event !== null) {
5080
5207
  switch (event.currentTarget.connectionState) {
@@ -5105,16 +5232,16 @@
5105
5232
  // DEVICES
5106
5233
  //------------------------------------------------------------------------//
5107
5234
  /**
5108
- * Returns list od devices (cameras, microphones) available for user's device
5235
+ * Grabs available devices
5109
5236
  */
5110
5237
  grabDevices() {
5111
5238
  return __awaiter(this, void 0, void 0, function* () {
5239
+ if (this._isDestroyed) return;
5112
5240
  try {
5113
5241
  const deviceAccess = yield this.checkIndividualDeviceAccess();
5242
+ if (this._isDestroyed) return;
5114
5243
  this._cameraList = new InputDeviceList();
5115
5244
  this._microphoneList = new InputDeviceList();
5116
- // Wysyłamy eventy tylko jeśli nie sprawdzaliśmy wcześniej uprawnień
5117
- // lub jeśli zmienił się stan uprawnień (resetowana flaga)
5118
5245
  if (!this._permissionChecked) {
5119
5246
  if (!deviceAccess.camera.allowed) {
5120
5247
  this._main.dispatchEvent("cameraAccessDenied", {
@@ -5143,8 +5270,9 @@
5143
5270
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5144
5271
  }
5145
5272
  }
5146
- // Wypełnianie list dostępnymi urządzeniami
5273
+ if (this._isDestroyed) return;
5147
5274
  const devices = yield navigator.mediaDevices.enumerateDevices();
5275
+ if (this._isDestroyed) return;
5148
5276
  for (const device of devices) {
5149
5277
  if (device.deviceId && device.label) {
5150
5278
  if (device.kind === 'videoinput' && deviceAccess.camera.allowed) {
@@ -5156,8 +5284,8 @@
5156
5284
  }
5157
5285
  }
5158
5286
  }
5287
+ if (this._isDestroyed) return;
5159
5288
  try {
5160
- // Aktualizacja wybranych urządzeń
5161
5289
  if (deviceAccess.camera.allowed) {
5162
5290
  this._selectedCamera = this.pickCamera();
5163
5291
  }
@@ -5167,38 +5295,41 @@
5167
5295
  } catch (error) {
5168
5296
  console.log(error);
5169
5297
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5170
- 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
+ });
5171
5306
  }
5172
- // Zawsze wysyłamy aktualizację list urządzeń
5173
- this._main.dispatchEvent("deviceListUpdate", {
5174
- ref: this._main,
5175
- cameraList: this._cameraList.getArray(),
5176
- microphoneList: this._microphoneList.getArray()
5177
- });
5178
5307
  this._permissionChecked = true;
5179
5308
  } catch (error) {
5309
+ if (this._isDestroyed) return;
5180
5310
  console.error("Error in grabDevices:", error);
5181
5311
  this._cameraList = new InputDeviceList();
5182
5312
  this._microphoneList = new InputDeviceList();
5183
- this._main.dispatchEvent("deviceListUpdate", {
5184
- ref: this._main,
5185
- cameraList: this._cameraList.getArray(),
5186
- microphoneList: this._microphoneList.getArray()
5187
- });
5188
- this._main.dispatchEvent("inputDeviceError", {
5189
- ref: this._main
5190
- });
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
+ }
5191
5323
  }
5192
5324
  });
5193
5325
  }
5194
5326
  /**
5195
- * Selects camera based on camera device ID with abort support
5196
- * @param cameraID
5327
+ * Selects camera by ID
5197
5328
  */
5198
5329
  selectCamera(cameraID) {
5199
5330
  var _a, _b, _c;
5200
5331
  return __awaiter(this, void 0, void 0, function* () {
5201
- // Anuluj poprzednie przełączanie kamery
5332
+ if (this._isDestroyed) return;
5202
5333
  if (this._cameraAbortController) {
5203
5334
  this._cameraAbortController.abort();
5204
5335
  }
@@ -5206,13 +5337,13 @@
5206
5337
  const signal = this._cameraAbortController.signal;
5207
5338
  try {
5208
5339
  this._switchingCamera = true;
5340
+ this._logger.info(this, `📹 [SWITCH] selectCamera() - switching to camera: ${cameraID}`);
5209
5341
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5210
5342
  this._cameraList.get(i).isSelected = false;
5211
5343
  }
5212
5344
  this._selectedCamera = null;
5213
5345
  this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5214
5346
  this.setCameraState(exports.DeviceState.NOT_INITIALIZED);
5215
- // Zapamiętaj aktualny stream key i stan publikacji
5216
5347
  const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5217
5348
  const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5218
5349
  let found = false;
@@ -5230,14 +5361,11 @@
5230
5361
  cameraList: this._cameraList.getArray(),
5231
5362
  microphoneList: this._microphoneList.getArray()
5232
5363
  });
5233
- if (signal.aborted) return;
5364
+ if (signal.aborted || this._isDestroyed) return;
5234
5365
  this.stopCameraStream();
5235
5366
  if (this._selectedCamera != null) {
5236
- // Update constraints with new device
5237
5367
  this._constraints.video.deviceId = this._selectedCamera.id;
5238
- // Sprawdź czy nie anulowano
5239
- if (signal.aborted) return;
5240
- // Poczekaj z możliwością anulowania
5368
+ if (signal.aborted || this._isDestroyed) return;
5241
5369
  yield new Promise((resolve, reject) => {
5242
5370
  const timeout = setTimeout(resolve, 500);
5243
5371
  signal.addEventListener('abort', () => {
@@ -5245,20 +5373,20 @@
5245
5373
  reject(new Error('Aborted'));
5246
5374
  });
5247
5375
  });
5248
- if (signal.aborted) return;
5249
- // Restart camera stream
5376
+ if (signal.aborted || this._isDestroyed) return;
5250
5377
  yield this.startCamera();
5378
+ if (this._isDestroyed) return;
5251
5379
  this.setCameraState(exports.DeviceState.ENABLED);
5252
5380
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) this.setInputDeviceState(exports.InputDevicesState.READY);else this.setInputDeviceState(exports.InputDevicesState.INVALID);
5253
- if (wasPublished && streamKey && !signal.aborted) {
5381
+ if (wasPublished && streamKey && !signal.aborted && !this._isDestroyed) {
5254
5382
  this.publish(streamKey);
5255
5383
  }
5256
5384
  } else {
5257
5385
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5258
5386
  }
5259
5387
  } catch (error) {
5260
- if (error.message !== 'Aborted') {
5261
- this._logger.error(this, 'Error switching camera: ' + error);
5388
+ if (error.message !== 'Aborted' && !this._isDestroyed) {
5389
+ this._logger.error(this, '📹 [ERROR] selectCamera() - Error switching camera: ' + error);
5262
5390
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5263
5391
  }
5264
5392
  } finally {
@@ -5270,13 +5398,12 @@
5270
5398
  });
5271
5399
  }
5272
5400
  /**
5273
- * Method tries to select (change) microphone based on its system ID with abort support
5274
- * @param micID
5401
+ * Selects microphone by ID
5275
5402
  */
5276
5403
  selectMicrophone(micID) {
5277
5404
  var _a, _b, _c;
5278
5405
  return __awaiter(this, void 0, void 0, function* () {
5279
- // Anuluj poprzednie przełączanie mikrofonu
5406
+ if (this._isDestroyed) return;
5280
5407
  if (this._microphoneAbortController) {
5281
5408
  this._microphoneAbortController.abort();
5282
5409
  }
@@ -5284,17 +5411,15 @@
5284
5411
  const signal = this._microphoneAbortController.signal;
5285
5412
  try {
5286
5413
  this._switchingMicrophone = true;
5414
+ this._logger.info(this, `📹 [SWITCH] selectMicrophone() - switching to microphone: ${micID}`);
5287
5415
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5288
5416
  this._microphoneList.get(i).isSelected = false;
5289
5417
  }
5290
5418
  this._selectedMicrophone = null;
5291
5419
  this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5292
5420
  this.setMicrophoneState(exports.DeviceState.NOT_INITIALIZED);
5293
- this._logger.info(this, "Selecting microphone: " + micID);
5294
- // Zapamiętaj aktualny stream key i stan publikacji
5295
5421
  const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5296
5422
  const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5297
- // Znajdź i zapisz wybrany mikrofon
5298
5423
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5299
5424
  if (this._microphoneList.get(i).id == micID) {
5300
5425
  this._selectedMicrophone = this._microphoneList.get(i);
@@ -5303,26 +5428,23 @@
5303
5428
  break;
5304
5429
  }
5305
5430
  }
5306
- // Zawsze wysyłamy aktualizację list urządzeń
5307
5431
  this._main.dispatchEvent("deviceListUpdate", {
5308
5432
  ref: this._main,
5309
5433
  cameraList: this._cameraList.getArray(),
5310
5434
  microphoneList: this._microphoneList.getArray()
5311
5435
  });
5312
- if (signal.aborted) return;
5313
- // Odłącz SoundMeter przed zmianą strumienia
5436
+ if (signal.aborted || this._isDestroyed) return;
5314
5437
  this._soundMeter.detach();
5315
- // Zamknij istniejące połączenie WebRTC
5316
5438
  this.closeWebRTCConnection();
5317
- // Zatrzymaj obecny strumień
5318
5439
  if (this._stream) {
5440
+ this._logger.info(this, "📹 [RELEASE] selectMicrophone() - stopping current stream");
5319
5441
  this._stream.getTracks().forEach(track => {
5320
5442
  track.stop();
5321
5443
  });
5322
5444
  this._stream = null;
5445
+ this._activeStreamCount--;
5323
5446
  }
5324
- if (signal.aborted) return;
5325
- // Poczekaj z możliwością anulowania
5447
+ if (signal.aborted || this._isDestroyed) return;
5326
5448
  yield new Promise((resolve, reject) => {
5327
5449
  const timeout = setTimeout(resolve, 500);
5328
5450
  signal.addEventListener('abort', () => {
@@ -5330,21 +5452,20 @@
5330
5452
  reject(new Error('Aborted'));
5331
5453
  });
5332
5454
  });
5333
- if (signal.aborted) return;
5334
- // Rozpocznij wszystko od nowa
5455
+ if (signal.aborted || this._isDestroyed) return;
5335
5456
  yield this.startCamera();
5457
+ if (this._isDestroyed) return;
5336
5458
  this.setMicrophoneState(exports.DeviceState.ENABLED);
5337
5459
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) {
5338
5460
  this.setInputDeviceState(exports.InputDevicesState.READY);
5339
5461
  } else {
5340
5462
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5341
5463
  }
5342
- // Jeśli stream był opublikowany, publikujemy ponownie
5343
- if (wasPublished && streamKey && !signal.aborted) {
5464
+ if (wasPublished && streamKey && !signal.aborted && !this._isDestroyed) {
5344
5465
  this.publish(streamKey);
5345
5466
  }
5346
5467
  } catch (error) {
5347
- if (error.message !== 'Aborted') {
5468
+ if (error.message !== 'Aborted' && !this._isDestroyed) {
5348
5469
  console.error("Error changing microphone:", error);
5349
5470
  this._main.dispatchEvent("inputDeviceError", {
5350
5471
  ref: this._main
@@ -5360,23 +5481,29 @@
5360
5481
  });
5361
5482
  }
5362
5483
  /**
5363
- * This method tries to start a camera with abort support
5484
+ * Starts camera with abort support
5364
5485
  * @private
5365
5486
  */
5366
5487
  startCamera() {
5367
5488
  var _a;
5368
5489
  return __awaiter(this, void 0, void 0, function* () {
5369
- // Anuluj poprzednie uruchamianie kamery
5490
+ if (this._isDestroyed) {
5491
+ this._logger.warning(this, "📹 [ACQUIRE] startCamera() - aborted, instance is destroyed");
5492
+ return;
5493
+ }
5370
5494
  if (this._startCameraAbortController) {
5371
5495
  this._startCameraAbortController.abort();
5372
5496
  }
5373
5497
  this._startCameraAbortController = new AbortController();
5374
5498
  const signal = this._startCameraAbortController.signal;
5499
+ // Release existing stream first
5375
5500
  if (this._stream) {
5501
+ this._logger.info(this, "📹 [RELEASE] startCamera() - releasing existing stream before acquiring new one");
5376
5502
  this._stream.getTracks().forEach(track => {
5377
5503
  track.stop();
5378
5504
  });
5379
5505
  this._stream = null;
5506
+ this._activeStreamCount--;
5380
5507
  }
5381
5508
  try {
5382
5509
  const constraints = {
@@ -5391,18 +5518,24 @@
5391
5518
  }
5392
5519
  } : false
5393
5520
  };
5394
- if (signal.aborted) return;
5521
+ if (signal.aborted || this._isDestroyed) return;
5522
+ this._logger.info(this, `📹 [ACQUIRE] startCamera() - requesting getUserMedia`);
5395
5523
  try {
5396
5524
  const stream = yield navigator.mediaDevices.getUserMedia(constraints);
5397
- if (signal.aborted) {
5398
- // Jeśli anulowano, zatrzymaj nowo utworzony strumień
5399
- stream.getTracks().forEach(track => track.stop());
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
+ });
5400
5532
  return;
5401
5533
  }
5402
5534
  this._stream = stream;
5403
5535
  this.onCameraStreamSuccess(this._stream);
5404
5536
  } catch (error) {
5405
- if (signal.aborted) return;
5537
+ if (signal.aborted || this._isDestroyed) return;
5538
+ this._logger.error(this, `📹 [ERROR] startCamera() - getUserMedia failed: ${error.name}: ${error.message}`);
5406
5539
  if (constraints.video) {
5407
5540
  this.onUserMediaError({
5408
5541
  name: error.name || 'Error',
@@ -5420,6 +5553,7 @@
5420
5553
  }
5421
5554
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) this.setInputDeviceState(exports.InputDevicesState.READY);
5422
5555
  } catch (error) {
5556
+ if (this._isDestroyed) return;
5423
5557
  console.error("Error in startCamera:", error);
5424
5558
  yield this.grabDevices();
5425
5559
  } finally {
@@ -5433,15 +5567,14 @@
5433
5567
  * Updates WebRTC connection with new stream
5434
5568
  */
5435
5569
  updateWebRTCStream() {
5570
+ if (this._isDestroyed) return;
5436
5571
  if (!this._peerConnection || !this._stream) {
5437
5572
  return;
5438
5573
  }
5439
- // Remove all existing tracks from the peer connection
5440
5574
  const senders = this._peerConnection.getSenders();
5441
5575
  senders.forEach(sender => {
5442
5576
  if (this._peerConnection) this._peerConnection.removeTrack(sender);
5443
5577
  });
5444
- // Add new tracks
5445
5578
  this._stream.getTracks().forEach(track => {
5446
5579
  if (this._stream != null && this._peerConnection) {
5447
5580
  this._peerConnection.addTrack(track, this._stream);
@@ -5449,32 +5582,24 @@
5449
5582
  });
5450
5583
  }
5451
5584
  /**
5452
- * Modified closeStream to handle both camera and WebRTC completely
5453
- */
5454
- closeStream() {
5455
- if (this._peerConnection !== undefined && this._peerConnection !== null) this._peerConnection.close();
5456
- this.setPublishState(exports.PublishState.UNPUBLISHED);
5457
- }
5458
- /**
5459
- * This method selects a camera based on previous uses or saved IDs
5585
+ * Picks camera based on saved ID or defaults
5460
5586
  * @private
5461
5587
  */
5462
5588
  pickCamera() {
5463
5589
  var _a, _b, _c, _d, _e, _f;
5590
+ if (this._isDestroyed) return null;
5464
5591
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5465
5592
  this._cameraList.get(i).isSelected = false;
5466
5593
  }
5467
5594
  let savedCameraID = (_b = (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.getField("cameraID")) !== null && _b !== void 0 ? _b : null;
5468
5595
  if (this._cameraList.getSize() > 0) {
5469
5596
  if (savedCameraID) {
5470
- // Szukamy zapisanej kamery
5471
5597
  let found = false;
5472
5598
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5473
5599
  if (this._cameraList.get(i).id === savedCameraID) {
5474
5600
  this._selectedCamera = this._cameraList.get(i);
5475
5601
  this._selectedCamera.isSelected = true;
5476
5602
  this.setCameraState(exports.DeviceState.ENABLED);
5477
- // Ustaw deviceId w constraints
5478
5603
  found = true;
5479
5604
  this._constraints.video.deviceId = this._selectedCamera.id;
5480
5605
  break;
@@ -5491,10 +5616,8 @@
5491
5616
  this._logger.info(this, "Canceling Publish!");
5492
5617
  this._main.getConfigManager().getStreamData().streamKey = null;
5493
5618
  }
5494
- return null;
5495
5619
  }
5496
5620
  }
5497
- // Jeśli nie znaleziono zapisanej kamery, używamy pierwszej
5498
5621
  if (!this._selectedCamera) {
5499
5622
  this._main.dispatchEvent("savedCameraNotFound", {
5500
5623
  ref: this._main,
@@ -5517,19 +5640,22 @@
5517
5640
  }
5518
5641
  }
5519
5642
  }
5520
- this._main.dispatchEvent("deviceListUpdate", {
5521
- ref: this._main,
5522
- cameraList: this._cameraList.getArray(),
5523
- microphoneList: this._microphoneList.getArray()
5524
- });
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
+ }
5525
5650
  return this._selectedCamera;
5526
5651
  }
5527
5652
  /**
5528
- * This method selects a microphone based on previous uses or saved IDs
5653
+ * Picks microphone based on saved ID or defaults
5529
5654
  * @private
5530
5655
  */
5531
5656
  pickMicrophone() {
5532
5657
  var _a, _b, _c, _d, _e, _f;
5658
+ if (this._isDestroyed) return null;
5533
5659
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5534
5660
  this._microphoneList.get(i).isSelected = false;
5535
5661
  }
@@ -5556,7 +5682,6 @@
5556
5682
  this._logger.info(this, "Canceling Publish!");
5557
5683
  this._main.getConfigManager().getStreamData().streamKey = null;
5558
5684
  }
5559
- return null;
5560
5685
  }
5561
5686
  }
5562
5687
  if (!this._selectedMicrophone) {
@@ -5585,73 +5710,43 @@
5585
5710
  }
5586
5711
  return this._selectedMicrophone;
5587
5712
  }
5588
- /**
5589
- * Cleans all saved cameras and microphones IDs.
5590
- */
5591
5713
  clearSavedDevices() {
5592
5714
  var _a, _b;
5593
5715
  (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.removeField("cameraID");
5594
5716
  (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.removeField("microphoneID");
5595
5717
  }
5596
- /**
5597
- * Messes up camera's and microphone's id (for testing only)
5598
- */
5599
5718
  messSavedDevices() {
5600
5719
  var _a, _b;
5601
5720
  (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.saveField("cameraID", "a");
5602
5721
  (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("microphoneID", "b");
5603
5722
  }
5604
- /**
5605
- * Handles microphone muting state
5606
- * @param microphoneState true to unmute, false to mute
5607
- */
5608
5723
  muteMicrophone(shouldMute) {
5724
+ var _a;
5725
+ if (this._isDestroyed) return;
5609
5726
  if (this._isMicrophoneMuted === shouldMute) {
5610
- // State hasn't changed, no need to do anything
5611
5727
  return;
5612
5728
  }
5613
5729
  this._isMicrophoneMuted = shouldMute;
5730
+ (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.saveField("microphoneMuted", shouldMute ? "true" : "false");
5614
5731
  if (this._stream) {
5615
- this.applyMicrophoneState(!shouldMute); // Odwracamy wartość dla track.enabled
5732
+ this.applyMicrophoneState(!shouldMute);
5616
5733
  } else {
5617
- // Store the desired state to be applied when stream becomes available
5618
- this._pendingMicrophoneState = !shouldMute; // Odwracamy wartość dla przyszłego track.enabled
5619
- this._logger.info(this, `WebRTCStreamer :: Stream not yet available, storing microphone state (muted: ${shouldMute})`);
5734
+ this._pendingMicrophoneState = !shouldMute;
5620
5735
  }
5621
- // Always dispatch the event to keep UI in sync
5622
5736
  this._main.dispatchEvent("microphoneStateChange", {
5623
5737
  ref: this._main,
5624
5738
  isMuted: this._isMicrophoneMuted
5625
5739
  });
5626
5740
  }
5627
- /**
5628
- * Applies the microphone state to the actual stream tracks
5629
- * @param enabled true to enable tracks, false to disable
5630
- * @private
5631
- */
5632
5741
  applyMicrophoneState(enabled) {
5633
- if (!this._stream) {
5634
- this._logger.warning(this, "WebRTCStreamer :: Cannot apply microphone state - stream not available");
5635
- return;
5636
- }
5742
+ if (!this._stream) return;
5637
5743
  const audioTracks = this._stream.getAudioTracks();
5638
5744
  if (audioTracks && audioTracks.length > 0) {
5639
- this._logger.success(this, `WebRTCStreamer :: ${enabled ? 'Unmuting' : 'Muting'} microphone`);
5640
5745
  audioTracks.forEach(track => track.enabled = enabled);
5641
- } else {
5642
- this._logger.warning(this, "WebRTCStreamer :: No audio tracks found in stream");
5643
5746
  }
5644
5747
  }
5645
- /**
5646
- * This methods is a final check whenever we're ready to publish a stream
5647
- * @param requireVideo - whenever video track is required
5648
- * @param requireAudio - whenever audio track is required
5649
- * @returns {boolean} true if stream is ready for publishing
5650
- */
5651
5748
  isStreamReady(requireVideo = true, requireAudio = true) {
5652
- if (!this._stream) {
5653
- return false;
5654
- }
5749
+ if (!this._stream) return false;
5655
5750
  const videoTracks = this._stream.getVideoTracks();
5656
5751
  const audioTracks = this._stream.getAudioTracks();
5657
5752
  const videoReady = !requireVideo || videoTracks.length > 0 && videoTracks[0].readyState === 'live';
@@ -5660,51 +5755,18 @@
5660
5755
  }
5661
5756
  closeWebRTCConnection() {
5662
5757
  if (this._peerConnection) {
5663
- 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
+ }
5664
5767
  this._peerConnection = null;
5665
5768
  }
5666
5769
  }
5667
- // Asynchroniczna wersja do użycia w destroy
5668
- closeWebRTCConnectionAsync() {
5669
- return __awaiter(this, void 0, void 0, function* () {
5670
- if (this._peerConnection) {
5671
- try {
5672
- // Usuń event handlery
5673
- this._peerConnection.onicecandidate = null;
5674
- this._peerConnection.onconnectionstatechange = null;
5675
- this._peerConnection.onnegotiationneeded = null;
5676
- this._peerConnection.oniceconnectionstatechange = null;
5677
- this._peerConnection.onicegatheringstatechange = null;
5678
- this._peerConnection.onsignalingstatechange = null;
5679
- this._peerConnection.ontrack = null;
5680
- // Zatrzymaj wszystkie transceivery
5681
- const transceivers = this._peerConnection.getTransceivers();
5682
- for (const transceiver of transceivers) {
5683
- if (transceiver.stop) {
5684
- transceiver.stop();
5685
- }
5686
- }
5687
- // Usuń wszystkie tracks
5688
- const senders = this._peerConnection.getSenders();
5689
- for (const sender of senders) {
5690
- if (sender.track) {
5691
- sender.track.enabled = false;
5692
- sender.track.stop();
5693
- }
5694
- this._peerConnection.removeTrack(sender);
5695
- }
5696
- // Zamknij połączenie
5697
- this._peerConnection.close();
5698
- // Poczekaj na zamknięcie
5699
- yield new Promise(resolve => setTimeout(resolve, 100));
5700
- } catch (e) {
5701
- this._logger.error(this, 'Error closing peer connection: ' + e);
5702
- } finally {
5703
- this._peerConnection = null;
5704
- }
5705
- }
5706
- });
5707
- }
5708
5770
  onDescriptionError(error) {
5709
5771
  this._logger.info(this, "WebRTCStreamer :: onDescriptionError: " + JSON.stringify(error));
5710
5772
  }
@@ -5716,6 +5778,7 @@
5716
5778
  //------------------------------------------------------------------------//
5717
5779
  createStatusConnection() {
5718
5780
  var _a, _b, _c;
5781
+ if (this._isDestroyed) return;
5719
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();
5720
5783
  if (!serverItem) return;
5721
5784
  if (this._statusConnection) {
@@ -5736,6 +5799,7 @@
5736
5799
  }
5737
5800
  setPublishState(newState) {
5738
5801
  if (this._publishState == newState) return;
5802
+ if (this._isDestroyed) return;
5739
5803
  if (this._debug) this._logger.decoratedLog("Publish State: " + newState, "dark-blue");
5740
5804
  this._logger.info(this, "Publish State: " + newState);
5741
5805
  if (newState == exports.PublishState.PUBLISHED) this._publishTime = new Date().getTime();
@@ -5748,9 +5812,9 @@
5748
5812
  getPublishTime() {
5749
5813
  return this._publishState == exports.PublishState.PUBLISHED ? this._publishTime : 0;
5750
5814
  }
5751
- // DEVICE STATE
5752
5815
  setInputDeviceState(newState) {
5753
5816
  if (this._inputDeviceState == newState) return;
5817
+ if (this._isDestroyed) return;
5754
5818
  this._inputDeviceState = newState;
5755
5819
  this._main.dispatchEvent("deviceStateChange", {
5756
5820
  ref: this._main,
@@ -5762,9 +5826,9 @@
5762
5826
  getInputDeviceState() {
5763
5827
  return this._inputDeviceState;
5764
5828
  }
5765
- // CAMERA STATE
5766
5829
  setCameraState(newState) {
5767
5830
  if (this._cameraState == newState) return;
5831
+ if (this._isDestroyed) return;
5768
5832
  this._cameraState = newState;
5769
5833
  this._main.dispatchEvent("cameraDeviceStateChange", {
5770
5834
  ref: this._main,
@@ -5775,9 +5839,9 @@
5775
5839
  getCameraState() {
5776
5840
  return this._cameraState;
5777
5841
  }
5778
- // MICROPHONE STATE
5779
5842
  setMicrophoneState(newState) {
5780
5843
  if (this._microphoneState == newState) return;
5844
+ if (this._isDestroyed) return;
5781
5845
  this._microphoneState = newState;
5782
5846
  this._main.dispatchEvent("microphoneDeviceStateChange", {
5783
5847
  ref: this._main,
@@ -5798,108 +5862,70 @@
5798
5862
  return this._publishState;
5799
5863
  }
5800
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
+ //------------------------------------------------------------------------//
5801
5907
  // DESTROY & DELETE
5802
5908
  //------------------------------------------------------------------------//
5803
- /**
5804
- * Method used to stop camera from streaming
5805
- * @private
5806
- */
5807
5909
  stopCameraStream() {
5808
5910
  var _a, _b;
5809
5911
  if (this._stream) {
5810
- 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
+ });
5811
5917
  const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5812
5918
  if (videoElement) {
5813
5919
  videoElement.srcObject = null;
5814
5920
  }
5815
5921
  this._soundMeter.detach();
5816
5922
  this._stream = null;
5923
+ this._activeStreamCount--;
5924
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - complete, active streams: ${this._activeStreamCount}`);
5817
5925
  }
5818
5926
  }
5819
- /**
5820
- * Ulepszona metoda czyszczenia strumienia
5821
- * @private
5822
- */
5823
- cleanupMediaStream(stream) {
5824
- return __awaiter(this, void 0, void 0, function* () {
5825
- if (!stream) return;
5826
- const tracks = stream.getTracks();
5827
- for (const track of tracks) {
5828
- try {
5829
- // Usuń wszystkie event listenery
5830
- track.onended = null;
5831
- track.onmute = null;
5832
- track.onunmute = null;
5833
- // Wyłącz track
5834
- track.enabled = false;
5835
- // Zatrzymaj track
5836
- track.stop();
5837
- this._logger.info(this, `Track ${track.kind} stopped. ReadyState: ${track.readyState}`);
5838
- } catch (e) {
5839
- this._logger.error(this, `Error stopping track: ${e}`);
5840
- }
5841
- }
5842
- // Poczekaj na cleanup
5843
- yield new Promise(resolve => setTimeout(resolve, 50));
5844
- });
5845
- }
5846
- /**
5847
- * Method stops streaming for all streams with proper cleanup
5848
- * @private
5849
- */
5850
- forceStopAllStreams() {
5851
- var _a, _b;
5852
- return __awaiter(this, void 0, void 0, function* () {
5853
- this._logger.info(this, "Force stopping all streams...");
5854
- // 1. Odłączenie i zniszczenie SoundMeter
5855
- if (this._soundMeter) {
5856
- try {
5857
- this._soundMeter.destroy();
5858
- } catch (e) {
5859
- this._logger.error(this, 'Error destroying SoundMeter: ' + e);
5860
- }
5861
- }
5862
- // 2. Zatrzymanie głównego strumienia
5863
- if (this._stream) {
5864
- yield this.cleanupMediaStream(this._stream);
5865
- this._stream = null;
5866
- }
5867
- // 3. Czyszczenie elementu video
5868
- try {
5869
- const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5870
- if (videoElement) {
5871
- // Zatrzymaj strumień w elemencie video
5872
- if (videoElement.srcObject instanceof MediaStream) {
5873
- yield this.cleanupMediaStream(videoElement.srcObject);
5874
- }
5875
- // Wyczyść element video
5876
- videoElement.pause();
5877
- videoElement.removeAttribute('src');
5878
- videoElement.srcObject = null;
5879
- videoElement.load();
5880
- // Usuń event listenery
5881
- videoElement.onloadedmetadata = null;
5882
- videoElement.onloadeddata = null;
5883
- videoElement.oncanplay = null;
5884
- videoElement.onerror = null;
5885
- }
5886
- } catch (e) {
5887
- this._logger.error(this, 'Error cleaning video element: ' + e);
5888
- }
5889
- // 4. Zamknięcie WebRTC
5890
- yield this.closeWebRTCConnectionAsync();
5891
- // 5. Resetuj zmienne
5892
- this._selectedCamera = null;
5893
- this._selectedMicrophone = null;
5894
- this._pendingMicrophoneState = null;
5895
- this._logger.info(this, "Force stop all streams completed");
5896
- });
5897
- }
5898
- /**
5899
- * Stops all streaming operations and cleans up resources
5900
- */
5901
5927
  stop() {
5902
- // Anuluj wszystkie aktywne operacje
5928
+ this._logger.info(this, "📹 [STOP] stop() - stopping all operations");
5903
5929
  if (this._cameraAbortController) {
5904
5930
  this._cameraAbortController.abort();
5905
5931
  this._cameraAbortController = null;
@@ -5912,7 +5938,6 @@
5912
5938
  this._startCameraAbortController.abort();
5913
5939
  this._startCameraAbortController = null;
5914
5940
  }
5915
- // Stop status connection and clear timer
5916
5941
  if (this._statusConnection) {
5917
5942
  this._statusConnection.destroy();
5918
5943
  this._statusConnection = null;
@@ -5922,16 +5947,12 @@
5922
5947
  this._statusTimer = null;
5923
5948
  }
5924
5949
  this._main.getConfigManager().getStreamData().streamKey = null;
5925
- // Close WebRTC connection
5926
5950
  this.closeWebRTCConnection();
5927
- // Stop all media streams
5928
5951
  this.stopCameraStream();
5929
- // Reset states
5930
5952
  this.setPublishState(exports.PublishState.STOPPED);
5931
5953
  this.setInputDeviceState(exports.InputDevicesState.STOPPED);
5932
5954
  this.setCameraState(exports.DeviceState.STOPPED);
5933
5955
  this.setMicrophoneState(exports.DeviceState.STOPPED);
5934
- // Clear restart timer if exists
5935
5956
  if (this._restartTimer) {
5936
5957
  clearInterval(this._restartTimer);
5937
5958
  this._restartTimer = null;
@@ -5939,127 +5960,140 @@
5939
5960
  this._restartTimerCount = 0;
5940
5961
  clearTimeout(this._publishTimer);
5941
5962
  }
5942
- /**
5943
- * Reinitializes the streaming setup
5944
- */
5945
5963
  start() {
5946
5964
  var _a, _b, _c;
5947
5965
  return __awaiter(this, void 0, void 0, function* () {
5966
+ if (this._isDestroyed) return;
5967
+ this._logger.info(this, "📹 [START] start() - reinitializing streaming");
5948
5968
  try {
5949
- // Reset states
5950
5969
  this._publishState = exports.PublishState.NOT_INITIALIZED;
5951
5970
  this._inputDeviceState = exports.InputDevicesState.NOT_INITIALIZED;
5952
5971
  this._cameraState = exports.DeviceState.NOT_INITIALIZED;
5953
5972
  this._microphoneState = exports.DeviceState.NOT_INITIALIZED;
5954
- // Reinitialize devices and stream
5955
5973
  yield this.initializeDevices();
5974
+ if (this._isDestroyed) return;
5956
5975
  yield this.startCamera();
5957
- // If autoConnect is enabled, initialize network
5976
+ if (this._isDestroyed) return;
5958
5977
  if ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getSettingsData().autoConnect) {
5959
5978
  (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.initialize();
5960
5979
  }
5961
- // Reinitialize status connection if needed
5962
5980
  if ((_c = this._main.getConfigManager()) === null || _c === void 0 ? void 0 : _c.getStreamData().streamKey) {
5963
5981
  this.createStatusConnection();
5964
5982
  }
5965
5983
  } catch (error) {
5966
- 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));
5967
5986
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5968
5987
  throw error;
5969
5988
  }
5970
5989
  });
5971
5990
  }
5972
5991
  /**
5973
- * Method used for destroying everything (one-time use) with proper cleanup
5992
+ * SYNCHRONOUS destroy - sets flag immediately, cleanup happens in background
5993
+ * This ensures that even if called without await, all async operations will abort
5974
5994
  */
5975
5995
  destroy() {
5976
- return __awaiter(this, void 0, void 0, function* () {
5977
- this._logger.info(this, "Starting StreamerController destroy...");
5978
- this._isDestroying = true;
5979
- try {
5980
- // 1. Anuluj wszystkie aktywne operacje
5981
- if (this._cameraAbortController) {
5982
- this._cameraAbortController.abort();
5983
- this._cameraAbortController = null;
5984
- }
5985
- if (this._microphoneAbortController) {
5986
- this._microphoneAbortController.abort();
5987
- this._microphoneAbortController = null;
5988
- }
5989
- if (this._startCameraAbortController) {
5990
- this._startCameraAbortController.abort();
5991
- this._startCameraAbortController = null;
5992
- }
5993
- // 2. Zatrzymaj timery
5994
- if (this._statusTimer != null) {
5995
- clearInterval(this._statusTimer);
5996
- this._statusTimer = null;
5997
- }
5998
- if (this._restartTimer != null) {
5999
- clearInterval(this._restartTimer);
6000
- this._restartTimer = null;
6001
- }
6002
- clearTimeout(this._publishTimer);
6003
- // 3. Usuń event listenery urządzeń
6004
- if (this._deviceChangeHandler) {
6005
- navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeHandler);
6006
- this._deviceChangeHandler = null;
6007
- }
6008
- // 4. Usuń event listenery orientacji
6009
- if (this._orientationChangeHandler) {
6010
- if (window.screen && window.screen.orientation) {
6011
- window.screen.orientation.removeEventListener('change', this._orientationChangeHandler);
6012
- } else {
6013
- window.removeEventListener('orientationchange', this._orientationChangeHandler);
6014
- }
6015
- this._orientationChangeHandler = null;
6016
- }
6017
- // 5. Usuń pozostałe event listenery
6018
- try {
6019
- this._main.removeEventListener("serverConnect", this.onServerConnect);
6020
- this._main.removeEventListener("serverDisconnect", this.onServerDisconnect);
6021
- this._main.removeEventListener("streamKeyInUse", this.onStreamKeyTaken);
6022
- this._main.removeEventListener("statusServerConnect", this.onStatusServerConnect);
6023
- this._main.removeEventListener("statusServerDisconnect", this.onStatusServerDisconnect);
6024
- this._main.removeEventListener("streamStatusUpdate", this.onStreamStatsUpdate);
6025
- this._main.removeEventListener("deviceStateChange", this.onDeviceStateChange);
6026
- document.removeEventListener("visibilitychange", this.visibilityChange);
6027
- window.removeEventListener("blur", this.onWindowBlur);
6028
- window.removeEventListener("focus", this.onWindowFocus);
6029
- } catch (e) {
6030
- this._logger.error(this, 'Error removing event listeners: ' + e);
6031
- }
6032
- // 6. Zniszcz status connection
6033
- if (this._statusConnection) {
6034
- this._statusConnection.destroy();
6035
- this._statusConnection = null;
6036
- }
6037
- // 7. Zatrzymaj wszystkie strumienie
6038
- yield this.forceStopAllStreams();
6039
- // 8. Resetuj stany
6040
- this._permissionChecked = false;
6041
- this._isWindowActive = false;
6042
- this._isMicrophoneMuted = false;
6043
- this._publishState = exports.PublishState.NOT_INITIALIZED;
6044
- this._inputDeviceState = exports.InputDevicesState.NOT_INITIALIZED;
6045
- this._cameraState = exports.DeviceState.NOT_INITIALIZED;
6046
- this._microphoneState = exports.DeviceState.NOT_INITIALIZED;
6047
- this._switchingCamera = false;
6048
- this._switchingMicrophone = false;
6049
- // 9. Wyczyść listy urządzeń
6050
- if (this._cameraList) {
6051
- this._cameraList = new InputDeviceList();
6052
- }
6053
- if (this._microphoneList) {
6054
- this._microphoneList = new InputDeviceList();
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
6019
+ if (this._statusTimer != null) {
6020
+ clearInterval(this._statusTimer);
6021
+ this._statusTimer = null;
6022
+ }
6023
+ if (this._restartTimer != null) {
6024
+ clearInterval(this._restartTimer);
6025
+ this._restartTimer = null;
6026
+ }
6027
+ clearTimeout(this._publishTimer);
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;
6043
+ }
6044
+ // Remove event listeners
6045
+ try {
6046
+ this._main.removeEventListener("serverConnect", this.onServerConnect);
6047
+ this._main.removeEventListener("serverDisconnect", this.onServerDisconnect);
6048
+ this._main.removeEventListener("streamKeyInUse", this.onStreamKeyTaken);
6049
+ this._main.removeEventListener("statusServerConnect", this.onStatusServerConnect);
6050
+ this._main.removeEventListener("statusServerDisconnect", this.onStatusServerDisconnect);
6051
+ this._main.removeEventListener("streamStatusUpdate", this.onStreamStatsUpdate);
6052
+ this._main.removeEventListener("deviceStateChange", this.onDeviceStateChange);
6053
+ document.removeEventListener("visibilitychange", this.visibilityChange);
6054
+ window.removeEventListener("blur", this.onWindowBlur);
6055
+ window.removeEventListener("focus", this.onWindowFocus);
6056
+ } catch (e) {
6057
+ // Ignore errors
6058
+ }
6059
+ // Destroy status connection
6060
+ if (this._statusConnection) {
6061
+ this._statusConnection.destroy();
6062
+ this._statusConnection = null;
6063
+ }
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());
6055
6080
  }
6056
- this._logger.success(this, "StreamerController successfully destroyed");
6057
- } catch (error) {
6058
- this._logger.error(this, "Error during destroy: " + error);
6059
- } finally {
6060
- this._isDestroying = false;
6081
+ videoElement.srcObject = null;
6061
6082
  }
6062
- });
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)");
6063
6097
  }
6064
6098
  }
6065
6099
 
@@ -6671,52 +6705,19 @@
6671
6705
  * Main class of the player. The player itself has no GUI, but can be controlled via provided API.
6672
6706
  */
6673
6707
  class StormStreamer extends EventDispatcher {
6674
- //------------------------------------------------------------------------//
6675
- // CONSTRUCTOR
6676
- //------------------------------------------------------------------------//
6677
- /**
6678
- * Constructor - creates a new StormStreamer instance
6679
- *
6680
- * @param streamConfig - Configuration object for the streamer
6681
- * @param autoInitialize - Whether to automatically initialize the streamer after creation
6682
- */
6683
6708
  constructor(streamConfig, autoInitialize = false) {
6684
6709
  super();
6685
- /**
6686
- * Indicates whether the streamer object is in development mode (provides more debug options)
6687
- * @private
6688
- */
6689
6710
  this.DEV_MODE = true;
6690
- /**
6691
- * Version of this streamer in SemVer format (Major.Minor.Patch).
6692
- * @private
6693
- */
6694
- this.STREAMER_VERSION = "1.0.0-rc.1";
6695
- /**
6696
- * Compile date for this streamer
6697
- * @private
6698
- */
6699
- this.COMPILE_DATE = "11/17/2025, 10:30:23 AM";
6700
- /**
6701
- * Defines from which branch this streamer comes from e.g. "Main", "Experimental"
6702
- * @private
6703
- */
6711
+ this.STREAMER_VERSION = "1.0.0";
6712
+ this.COMPILE_DATE = "2/7/2026, 6:37:16 PM";
6704
6713
  this.STREAMER_BRANCH = "Experimental";
6705
- /**
6706
- * Defines number of streamer protocol that is required on server-side
6707
- * @private
6708
- */
6709
6714
  this.STREAMER_PROTOCOL_VERSION = 1;
6710
- /**
6711
- * Indicates whether streamer was initialized or not
6712
- * @private
6713
- */
6714
6715
  this._initialized = false;
6716
+ this._isDestroyed = false;
6715
6717
  if (typeof window === 'undefined' || !window.document || !window.document.createElement) {
6716
6718
  console.error(`StormStreamer Creation Error - No "window" element in the provided context!`);
6717
6719
  return;
6718
6720
  }
6719
- // WINDOW.StormStreamerArray
6720
6721
  if (this.DEV_MODE && !('StormStreamerArray' in window)) {
6721
6722
  window.StormStreamerArray = [];
6722
6723
  }
@@ -6726,17 +6727,13 @@
6726
6727
  this.setStreamConfig(streamConfig);
6727
6728
  if (autoInitialize) this.initialize();
6728
6729
  }
6729
- /**
6730
- * Initializes the streamer object. From this point, a connection to the server is established and authentication occurs.
6731
- * It is recommended to add all event listeners before calling this method to ensure they can be properly captured.
6732
- */
6733
6730
  initialize() {
6734
- if (this._isRemoved) return;
6735
- 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.");
6736
- this._storageManager = new StorageManager(this); // Storing user data
6737
- this._stageController = new StageController(this); // Visual elements like VideoElement
6738
- this._networkController = new NetworkController(this); // Networking and connection with a server
6739
- 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);
6740
6737
  this._statsController = new StatsController(this);
6741
6738
  this._graphs = [];
6742
6739
  this._initialized = true;
@@ -6744,16 +6741,8 @@
6744
6741
  ref: this
6745
6742
  });
6746
6743
  }
6747
- /**
6748
- * Sets stream config for the streamer (or overwrites an existing one).
6749
- *
6750
- * @param streamConfig - New configuration object for the streamer
6751
- */
6752
6744
  setStreamConfig(streamConfig) {
6753
- if (this._isRemoved) return;
6754
- /**
6755
- * In case the original streamConfig is modified elsewhere we have to create a separate copy and store it ourselves
6756
- */
6745
+ if (this._isRemoved || this._isDestroyed) return;
6757
6746
  const copiedStreamConfig = JSON.parse(JSON.stringify(streamConfig));
6758
6747
  if (this._configManager == null) {
6759
6748
  this._configManager = new ConfigManager(copiedStreamConfig);
@@ -6778,290 +6767,133 @@
6778
6767
  });
6779
6768
  }
6780
6769
  }
6781
- //------------------------------------------------------------------------//
6782
6770
  // PLAYBACK / STREAMING
6783
- //------------------------------------------------------------------------//
6784
- /**
6785
- * Returns true if this streamer instance is currently connected to a Storm Server/Cloud instance.
6786
- *
6787
- * @returns Boolean indicating connection status
6788
- */
6789
6771
  isConnected() {
6790
6772
  var _a, _b;
6791
6773
  return (_b = (_a = this._networkController) === null || _a === void 0 ? void 0 : _a.getConnection().isConnectionActive()) !== null && _b !== void 0 ? _b : false;
6792
6774
  }
6793
- /**
6794
- * Mutes the streamer's video object. Audio output will be silenced.
6795
- */
6796
6775
  mute() {
6797
- if (this._stageController != null) {
6798
- if (this._stageController.getScreenElement() != null) {
6799
- this._stageController.getScreenElement().setMuted(true);
6800
- return;
6801
- }
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;
6802
6780
  }
6803
6781
  this._configManager.getSettingsData().getAudioData().muted = true;
6804
6782
  }
6805
- /**
6806
- * Unmutes the streamer's video object. Audio output will be restored.
6807
- */
6808
6783
  unmute() {
6809
- if (this._stageController != null) {
6810
- if (this._stageController.getScreenElement() != null) {
6811
- this._stageController.getScreenElement().setMuted(false);
6812
- return;
6813
- }
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;
6814
6788
  }
6815
6789
  this._configManager.getSettingsData().getAudioData().muted = false;
6816
6790
  }
6817
- /**
6818
- * Checks whether the streamer audio is currently muted.
6819
- *
6820
- * @returns Boolean indicating mute status
6821
- */
6822
6791
  isMute() {
6823
6792
  var _a, _b, _c, _d;
6824
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;
6825
6794
  }
6826
- /**
6827
- * Toggles between mute and unmute states. Returns the new mute state.
6828
- *
6829
- * @returns New mute state (true = muted, false = unmuted)
6830
- */
6831
6795
  toggleMute() {
6832
6796
  const isMuted = this.isMute();
6833
- if (isMuted) {
6834
- this.unmute();
6835
- } else {
6836
- this.mute();
6837
- }
6797
+ if (isMuted) this.unmute();else this.mute();
6838
6798
  return !isMuted;
6839
6799
  }
6840
- /**
6841
- * Sets new volume for the streamer (0-100). Once the method is performed, the volumeChange event will be triggered.
6842
- * If the video was muted prior to the volume change, it will be automatically unmuted.
6843
- *
6844
- * @param newVolume - Volume level (0-100)
6845
- */
6846
6800
  setVolume(newVolume) {
6847
6801
  var _a, _b;
6848
- if (((_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.setVolume(newVolume)) !== undefined) {
6849
- return;
6850
- }
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;
6851
6803
  this._configManager.getSettingsData().getAudioData().startVolume = newVolume;
6852
6804
  }
6853
- /**
6854
- * Returns current streamer volume (0-100).
6855
- *
6856
- * @returns Current volume level
6857
- */
6858
6805
  getVolume() {
6859
6806
  var _a, _b, _c;
6860
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;
6861
6808
  }
6862
- /**
6863
- * Returns the list of available camera devices.
6864
- *
6865
- * @returns Array of camera input devices
6866
- */
6867
6809
  getCameraList() {
6868
6810
  var _a, _b;
6869
6811
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCameraList()) !== null && _b !== void 0 ? _b : [];
6870
6812
  }
6871
- /**
6872
- * Returns the list of available microphone devices.
6873
- *
6874
- * @returns Array of microphone input devices
6875
- */
6876
6813
  getMicrophoneList() {
6877
6814
  var _a, _b;
6878
6815
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getMicrophoneList()) !== null && _b !== void 0 ? _b : [];
6879
6816
  }
6880
- /**
6881
- * Sets the active camera device by ID.
6882
- *
6883
- * @param cameraID - ID of the camera device to use
6884
- */
6885
6817
  setCamera(cameraID) {
6886
6818
  var _a;
6887
6819
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.selectCamera(cameraID);
6888
6820
  }
6889
- /**
6890
- * Sets the active microphone device by ID.
6891
- *
6892
- * @param microphoneID - ID of the microphone device to use
6893
- */
6894
6821
  setMicrophone(microphoneID) {
6895
6822
  var _a;
6896
6823
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.selectMicrophone(microphoneID);
6897
6824
  }
6898
- /**
6899
- * Returns the currently active camera device.
6900
- *
6901
- * @returns Current camera device or null if none is active
6902
- */
6903
6825
  getCurrentCamera() {
6904
- 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;
6905
6828
  }
6906
- /**
6907
- * Returns the currently active microphone device.
6908
- *
6909
- * @returns Current microphone device or null if none is active
6910
- */
6911
6829
  getCurrentMicrophone() {
6912
- 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;
6913
6832
  }
6914
- /**
6915
- * Mutes or unmutes the microphone.
6916
- *
6917
- * @param microphoneState - True to mute, false to unmute
6918
- */
6919
6833
  muteMicrophone(microphoneState) {
6920
6834
  var _a;
6921
6835
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.muteMicrophone(microphoneState);
6922
6836
  }
6923
- /**
6924
- * Checks if the microphone is currently muted.
6925
- *
6926
- * @returns Boolean indicating if microphone is muted
6927
- */
6928
6837
  isMicrophoneMuted() {
6929
6838
  var _a, _b;
6930
6839
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.isMicrophoneMuted()) !== null && _b !== void 0 ? _b : false;
6931
6840
  }
6932
- /**
6933
- * Returns the current publishing state of the streamer.
6934
- *
6935
- * @returns Current publishing state
6936
- */
6937
6841
  getPublishState() {
6938
6842
  var _a, _b;
6939
6843
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getPublishState()) !== null && _b !== void 0 ? _b : exports.PublishState.NOT_INITIALIZED;
6940
6844
  }
6941
- /**
6942
- * Returns the total time the stream has been publishing in milliseconds.
6943
- *
6944
- * @returns Publishing time in milliseconds
6945
- */
6946
6845
  getPublishTime() {
6947
6846
  var _a, _b;
6948
6847
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getPublishTime()) !== null && _b !== void 0 ? _b : 0;
6949
6848
  }
6950
- /**
6951
- * Starts publishing a stream with the given stream key.
6952
- *
6953
- * @param streamKey - Key identifying the stream to publish
6954
- * @returns Boolean indicating if publishing was successfully initiated
6955
- */
6956
6849
  publish(streamKey) {
6957
6850
  var _a, _b;
6958
6851
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.publish(streamKey)) !== null && _b !== void 0 ? _b : false;
6959
6852
  }
6960
- /**
6961
- * Stops publishing the current stream.
6962
- */
6963
6853
  unpublish() {
6964
6854
  var _a;
6965
- console.log("kutas 1");
6966
6855
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.unpublish();
6967
6856
  }
6968
- /**
6969
- * Returns the current state of input devices (camera and microphone).
6970
- *
6971
- * @returns Current state of input devices
6972
- */
6973
6857
  getInputDevicesState() {
6974
6858
  var _a, _b;
6975
6859
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getInputDeviceState()) !== null && _b !== void 0 ? _b : exports.InputDevicesState.NOT_INITIALIZED;
6976
6860
  }
6977
- /**
6978
- * Returns the current state of the camera device.
6979
- *
6980
- * @returns Current camera device state
6981
- */
6982
6861
  getCameraState() {
6983
6862
  var _a, _b;
6984
6863
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCameraState()) !== null && _b !== void 0 ? _b : exports.DeviceState.NOT_INITIALIZED;
6985
6864
  }
6986
- /**
6987
- * Returns the current state of the microphone device.
6988
- *
6989
- * @returns Current microphone device state
6990
- */
6991
6865
  getMicrophoneState() {
6992
6866
  var _a, _b;
6993
6867
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getMicrophoneState()) !== null && _b !== void 0 ? _b : exports.DeviceState.NOT_INITIALIZED;
6994
6868
  }
6995
- /**
6996
- * Clears saved device preferences from storage.
6997
- */
6998
6869
  clearSavedDevices() {
6999
6870
  var _a;
7000
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.clearSavedDevices();
6871
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.clearSavedDevices();
7001
6872
  }
7002
- /**
7003
- * Randomizes saved device preferences (for testing purposes).
7004
- */
7005
6873
  messSavedDevices() {
7006
6874
  var _a;
7007
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.messSavedDevices();
6875
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.messSavedDevices();
7008
6876
  }
7009
- /**
7010
- * Checks if the stream is ready for publishing.
7011
- *
7012
- * @returns Boolean indicating if stream is ready
7013
- */
7014
6877
  isStreamReady() {
7015
6878
  var _a, _b;
7016
6879
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.isStreamReady()) !== null && _b !== void 0 ? _b : false;
7017
6880
  }
7018
- //------------------------------------------------------------------------//
7019
6881
  // CONTAINER
7020
- //------------------------------------------------------------------------//
7021
- /**
7022
- * Attaches the streamer to a new parent container using either a container ID (string) or a reference to an HTMLElement.
7023
- * If the instance is already attached, it will be moved to a new parent.
7024
- *
7025
- * @param container - Container ID (string) or HTMLElement reference
7026
- * @returns Boolean indicating if attachment was successful
7027
- */
7028
6882
  attachToContainer(container) {
7029
6883
  var _a, _b;
7030
- let result = false;
7031
6884
  if (this._initialized) return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.attachToParent(container)) !== null && _b !== void 0 ? _b : false;
7032
- return result;
6885
+ return false;
7033
6886
  }
7034
- /**
7035
- * Detaches the streamer from the current parent element, if possible.
7036
- *
7037
- * @returns Boolean indicating if detachment was successful
7038
- */
7039
6887
  detachFromContainer() {
7040
6888
  var _a, _b;
7041
- let result = false;
7042
6889
  if (this._initialized) return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.detachFromParent()) !== null && _b !== void 0 ? _b : false;
7043
- return result;
6890
+ return false;
7044
6891
  }
7045
- /**
7046
- * Returns the current parent element of the streamer, or null if none exists.
7047
- *
7048
- * @returns Current container element or null
7049
- */
7050
6892
  getContainer() {
7051
6893
  var _a, _b;
7052
6894
  return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getParentElement()) !== null && _b !== void 0 ? _b : null;
7053
6895
  }
7054
- //------------------------------------------------------------------------//
7055
6896
  // SIZE & RESIZE
7056
- //------------------------------------------------------------------------//
7057
- /**
7058
- * Sets a new width and height for the streamer. The values can be given as a number (in which case they are
7059
- * treated as the number of pixels), or as a string ending with "px" (this will also be the number of pixels) or "%",
7060
- * where the number is treated as a percentage of the parent container's value.
7061
- *
7062
- * @param width - Can be provided as number or a string with "%" or "px" suffix
7063
- * @param height - Can be provided as number or a string with "%" or "px" suffix
7064
- */
7065
6897
  setSize(width, height) {
7066
6898
  if (this._initialized) this._stageController.setSize(width, height);else {
7067
6899
  const parsedWidth = NumberUtilities.parseValue(width);
@@ -7072,13 +6904,6 @@
7072
6904
  this._configManager.getSettingsData().getVideoData().videoHeightInPixels = parsedHeight.isPixels;
7073
6905
  }
7074
6906
  }
7075
- /**
7076
- * Sets a new width for the streamer. The value can be given as a number (in which case it is treated as the
7077
- * number of pixels), or as a string ending with "px" (this will also be the number of pixels) or "%", where the
7078
- * number is treated as a percentage of the parent container's value.
7079
- *
7080
- * @param width - Can be provided as number or a string with "%" or "px" suffix
7081
- */
7082
6907
  setWidth(width) {
7083
6908
  if (this._initialized) this._stageController.setWidth(width);else {
7084
6909
  const parsedWidth = NumberUtilities.parseValue(width);
@@ -7086,13 +6911,6 @@
7086
6911
  this._configManager.getSettingsData().getVideoData().videoWidthInPixels = parsedWidth.isPixels;
7087
6912
  }
7088
6913
  }
7089
- /**
7090
- * Sets a new height for the streamer. The value can be given as a number (in which case it is treated as the
7091
- * number of pixels), or as a string ending with "px" (this will also be the number of pixels) or "%", where the
7092
- * number is treated as a percentage of the parent container's value.
7093
- *
7094
- * @param height - Can be provided as number or a string with "%" or "px" suffix
7095
- */
7096
6914
  setHeight(height) {
7097
6915
  if (this._initialized) this._stageController.setHeight(height);else {
7098
6916
  const parsedHeight = NumberUtilities.parseValue(height);
@@ -7100,66 +6918,26 @@
7100
6918
  this._configManager.getSettingsData().getVideoData().videoHeightInPixels = parsedHeight.isPixels;
7101
6919
  }
7102
6920
  }
7103
- /**
7104
- * Returns current streamer width in pixels.
7105
- *
7106
- * @returns Current width in pixels
7107
- */
7108
6921
  getWidth() {
7109
- if (this._initialized) return this._stageController.getContainerWidth();else {
7110
- if (this._configManager.getSettingsData().getVideoData().videoWidthInPixels) return this._configManager.getSettingsData().getVideoData().videoWidthValue;
7111
- }
6922
+ if (this._initialized) return this._stageController.getContainerWidth();
6923
+ if (this._configManager.getSettingsData().getVideoData().videoWidthInPixels) return this._configManager.getSettingsData().getVideoData().videoWidthValue;
7112
6924
  return 0;
7113
6925
  }
7114
- /**
7115
- * Returns current streamer height in pixels.
7116
- *
7117
- * @returns Current height in pixels
7118
- */
7119
6926
  getHeight() {
7120
- if (this._initialized) return this._stageController.getContainerHeight();else {
7121
- if (this._configManager.getSettingsData().getVideoData().videoHeightInPixels) return this._configManager.getSettingsData().getVideoData().videoHeightValue;
7122
- }
6927
+ if (this._initialized) return this._stageController.getContainerHeight();
6928
+ if (this._configManager.getSettingsData().getVideoData().videoHeightInPixels) return this._configManager.getSettingsData().getVideoData().videoHeightValue;
7123
6929
  return 0;
7124
6930
  }
7125
- /**
7126
- * Changes the streamer scaling mode. Available modes include fill, letterbox, original, and crop.
7127
- *
7128
- * @param newMode - New scaling mode name (fill, letterbox, original, crop)
7129
- */
7130
6931
  setScalingMode(newMode) {
7131
- if (this._stageController) {
7132
- this._stageController.setScalingMode(newMode);
7133
- } else {
7134
- this._configManager.getSettingsData().getVideoData().scalingMode = newMode;
7135
- }
6932
+ if (this._stageController) this._stageController.setScalingMode(newMode);else this._configManager.getSettingsData().getVideoData().scalingMode = newMode;
7136
6933
  }
7137
- /**
7138
- * Returns the current streamer scaling mode.
7139
- *
7140
- * @returns Current scaling mode
7141
- */
7142
6934
  getScalingMode() {
7143
- if (this._stageController) {
7144
- return this._stageController.getScalingMode();
7145
- } else {
7146
- return this._configManager.getSettingsData().getVideoData().scalingMode;
7147
- }
6935
+ if (this._stageController) return this._stageController.getScalingMode();
6936
+ return this._configManager.getSettingsData().getVideoData().scalingMode;
7148
6937
  }
7149
- /**
7150
- * Forces the streamer to recalculate its size based on parent internal dimensions.
7151
- */
7152
6938
  updateToSize() {
7153
- if (this._initialized) {
7154
- this._stageController.handleResize();
7155
- }
6939
+ if (this._initialized) this._stageController.handleResize();
7156
6940
  }
7157
- /**
7158
- * Returns a promise that resolves with a screenshot of the video element as a blob, or null if taking the
7159
- * screenshot was not possible.
7160
- *
7161
- * @returns Promise resolving to a Blob containing the screenshot or null
7162
- */
7163
6941
  makeScreenshot() {
7164
6942
  let canvas = document.createElement('canvas');
7165
6943
  let context = canvas.getContext('2d');
@@ -7170,64 +6948,24 @@
7170
6948
  let element = this._stageController.getScreenElement().getVideoElement();
7171
6949
  if (context) {
7172
6950
  context.drawImage(element, 0, 0, canvas.width, canvas.height);
7173
- canvas.toBlob(blob => {
7174
- resolve(blob);
7175
- }, 'image/png');
7176
- } else {
7177
- resolve(null);
7178
- }
7179
- } else {
7180
- resolve(null);
7181
- }
6951
+ canvas.toBlob(blob => resolve(blob), 'image/png');
6952
+ } else resolve(null);
6953
+ } else resolve(null);
7182
6954
  });
7183
6955
  }
7184
- //------------------------------------------------------------------------//
7185
6956
  // GRAPHS
7186
- //------------------------------------------------------------------------//
7187
- /**
7188
- * Creates a FPS performance graph in the specified location (container ID or reference). The graph is a
7189
- * separate object that must be started using its start() method and stopped using its stop() method. The dimensions
7190
- * of the graph depend on the dimensions of the specified container.
7191
- *
7192
- * @param container - Element ID or reference to HTMLElement
7193
- * @returns FPS graph instance
7194
- */
7195
6957
  createFPSGraph(container) {
7196
6958
  return new FPSGraph(this, container);
7197
6959
  }
7198
- /**
7199
- * Creates a bitrate performance graph in the specified location (container ID or reference). The graph is a
7200
- * separate object that must be started using its start() method and stopped using its stop() method. The dimensions
7201
- * of the graph depend on the dimensions of the specified container.
7202
- *
7203
- * @param container - Element ID or reference to HTMLElement
7204
- * @returns Bitrate graph instance
7205
- */
7206
6960
  createBitrateGraph(container) {
7207
6961
  return new BitrateGraph(this, container);
7208
6962
  }
7209
- /**
7210
- * Creates a microphone graph in the specified location (container ID or reference). The graph is a
7211
- * separate object that must be started using its start() method and stopped using its stop() method. The dimensions
7212
- * of the graph depend on the dimensions of the specified container.
7213
- *
7214
- * @param container - Element ID or reference to HTMLElement
7215
- * @returns Bitrate graph instance
7216
- */
7217
6963
  createMicrophoneGraph(container) {
7218
6964
  return new MicrophoneGraph(this, container);
7219
6965
  }
7220
- /**
7221
- * Adds new graph to the internal collection of active graphs.
7222
- *
7223
- * @param newGraph - Graph instance to add
7224
- */
7225
6966
  addGraph(newGraph) {
7226
6967
  if (this._graphs != null) this._graphs.push(newGraph);
7227
6968
  }
7228
- /**
7229
- * Stops all active performance graphs.
7230
- */
7231
6969
  stopAllGraphs() {
7232
6970
  if (this._graphs != null && this._graphs.length > 0) {
7233
6971
  for (let i = 0; i < this._graphs.length; i++) {
@@ -7235,198 +6973,136 @@
7235
6973
  }
7236
6974
  }
7237
6975
  }
7238
- //------------------------------------------------------------------------//
7239
6976
  // FULLSCREEN
7240
- //------------------------------------------------------------------------//
7241
- /**
7242
- * Enters fullscreen mode for the streamer container.
7243
- */
7244
6977
  enterFullScreen() {
7245
6978
  if (this._initialized && this._stageController) this._stageController.enterFullScreen();
7246
6979
  }
7247
- /**
7248
- * Exits fullscreen mode.
7249
- */
7250
6980
  exitFullScreen() {
7251
6981
  if (this._initialized && this._stageController) this._stageController.exitFullScreen();
7252
6982
  }
7253
- /**
7254
- * Returns true if the streamer is currently in fullscreen mode.
7255
- *
7256
- * @returns Boolean indicating fullscreen status
7257
- */
7258
6983
  isFullScreenMode() {
7259
6984
  if (this._initialized && this._stageController) return this._stageController.isFullScreenMode();
7260
6985
  return false;
7261
6986
  }
7262
- //------------------------------------------------------------------------//
7263
- // SIMPLE GETS & SETS
7264
- //------------------------------------------------------------------------//
7265
- /**
7266
- * Returns the current stream key or null if none is set.
7267
- *
7268
- * @returns Current stream key or null
7269
- */
6987
+ // GETTERS
7270
6988
  getStreamKey() {
7271
6989
  var _a, _b, _c;
7272
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;
7273
6991
  }
7274
- /**
7275
- * Returns the Stats Controller instance which contains statistical data about streaming performance.
7276
- *
7277
- * @returns Stats controller instance or null
7278
- */
7279
6992
  getStatsController() {
7280
6993
  return this._statsController;
7281
6994
  }
7282
- /**
7283
- * Returns the unique ID of this streamer instance. Each subsequent instance has a higher number.
7284
- *
7285
- * @returns Streamer instance ID
7286
- */
7287
6995
  getStreamerID() {
7288
6996
  return this._streamerID;
7289
6997
  }
7290
- /**
7291
- * Returns the logger instance used by this streamer.
7292
- *
7293
- * @returns Logger instance
7294
- */
7295
6998
  getLogger() {
7296
6999
  return this._logger;
7297
7000
  }
7298
- /**
7299
- * Returns the configuration manager for this streamer.
7300
- *
7301
- * @returns Config manager instance or null
7302
- */
7303
7001
  getConfigManager() {
7304
7002
  return this._configManager;
7305
7003
  }
7306
- /**
7307
- * Returns the network controller which manages all server communication.
7308
- *
7309
- * @returns Network controller instance or null
7310
- */
7311
7004
  getNetworkController() {
7312
7005
  return this._networkController;
7313
7006
  }
7314
- /**
7315
- * Returns the streamer controller which manages media stream operations.
7316
- *
7317
- * @returns Streamer controller instance or null
7318
- */
7319
7007
  getStreamerController() {
7320
7008
  return this._streamerController;
7321
7009
  }
7322
- /**
7323
- * Returns the stage controller which manages visual presentation.
7324
- *
7325
- * @returns Stage controller instance or null
7326
- */
7327
7010
  getStageController() {
7328
7011
  return this._stageController;
7329
7012
  }
7330
- /**
7331
- * Returns the storage manager which handles persistent data storage.
7332
- *
7333
- * @returns Storage manager instance or null
7334
- */
7335
7013
  getStorageManager() {
7336
7014
  return this._storageManager;
7337
7015
  }
7338
- /**
7339
- * Returns the HTML video element used by this streamer instance.
7340
- *
7341
- * @returns Video element or null
7342
- */
7343
7016
  getVideoElement() {
7344
7017
  var _a, _b, _c;
7345
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;
7346
7019
  }
7347
- /**
7348
- * Returns true if this streamer instance has already been initialized.
7349
- *
7350
- * @returns Boolean indicating initialization status
7351
- */
7352
7020
  isInitialized() {
7353
7021
  return this._initialized;
7354
7022
  }
7355
- /**
7356
- * Returns the version of this streamer instance. The version is returned in the SemVer format (Major.Minor.Patch).
7357
- *
7358
- * @returns Streamer version string
7359
- */
7023
+ isDestroyed() {
7024
+ return this._isDestroyed;
7025
+ }
7360
7026
  getVersion() {
7361
7027
  return this.STREAMER_VERSION;
7362
7028
  }
7363
- /**
7364
- * Returns the development branch of this streamer (e.g., main, experimental).
7365
- *
7366
- * @returns Branch name string
7367
- */
7368
7029
  getBranch() {
7369
7030
  return this.STREAMER_BRANCH;
7370
7031
  }
7371
- //------------------------------------------------------------------------//
7372
- // EVENT
7373
- //------------------------------------------------------------------------//
7374
- /**
7375
- * Dispatches an event with the specified name and data.
7376
- *
7377
- * @param eventName - Name of the event to dispatch
7378
- * @param event - Object containing event data
7379
- */
7032
+ // EVENTS
7380
7033
  dispatchEvent(eventName, event) {
7381
7034
  super.dispatchEvent(eventName, event);
7382
7035
  }
7383
- //------------------------------------------------------------------------//
7384
- // CLEAN UP
7385
- //------------------------------------------------------------------------//
7386
- /**
7387
- * Starts the streaming process.
7388
- *
7389
- * @returns Promise that resolves when streaming has started
7390
- */
7036
+ // START / STOP
7391
7037
  start() {
7392
7038
  var _a;
7393
7039
  return __awaiter(this, void 0, void 0, function* () {
7394
7040
  return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.start();
7395
7041
  });
7396
7042
  }
7397
- /**
7398
- * Stops the streaming process.
7399
- */
7400
7043
  stop() {
7401
7044
  var _a;
7402
- 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();
7403
7051
  }
7404
- //------------------------------------------------------------------------//
7405
- // CLEAN UP
7406
- //------------------------------------------------------------------------//
7407
7052
  /**
7408
- * Destroys this instance of StormStreamer, releasing all resources and disconnecting from the server.
7409
- * 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.
7410
7056
  */
7411
7057
  destroy() {
7412
- var _a, _b, _c, _d;
7413
- this._logger.warning(this, "Destroying streamer instance, bye, bye!");
7414
- if (this.DEV_MODE && 'StormStreamerArray' in window) window.StormStreamerArray[this._streamerID] = null;
7415
- // 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;
7416
7067
  this._initialized = false;
7417
7068
  this._isRemoved = true;
7418
- // part3
7419
- (_b = (_a = this._networkController) === null || _a === void 0 ? void 0 : _a.getConnection()) === null || _b === void 0 ? void 0 : _b.destroy();
7420
- (_c = this._streamerController) === null || _c === void 0 ? void 0 : _c.destroy();
7421
- (_d = this._stageController) === null || _d === void 0 ? void 0 : _d.destroy();
7422
- // 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
7423
7102
  this.removeAllEventListeners();
7103
+ (_g = this._logger) === null || _g === void 0 ? void 0 : _g.success(this, "🔴 [DESTROY] Streamer instance destroyed successfully (sync)");
7424
7104
  }
7425
7105
  }
7426
- /**
7427
- * Next ID for the streamer instance. Each subsequent instance has a higher number.
7428
- * @private
7429
- */
7430
7106
  StormStreamer.NEXT_STREAMER_ID = 0;
7431
7107
 
7432
7108
  function create(config) {