@stormstreaming/stormstreamer 1.0.0-rc.1 → 1.0.1

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.1
8
+ * Version: 2/18/2026, 5:26:15 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,16 +5017,20 @@
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);
5027
+ if (this._main.getConfigManager().getStreamData().streamKey == streamKey) {
5028
+ if (this._debug) this._logger.decoratedLog("Canceling publishing, already published " + streamKey, "dark-red");
5029
+ this._logger.info(this, "Canceling publishing, already published " + streamKey);
5030
+ return false;
5031
+ }
4906
5032
  if (this._main.getConfigManager().getStreamData().streamKey != null && !this._firstPublish) {
5033
+ if (this._debug) this._logger.decoratedLog("Unpublishing active session: " + this._main.getConfigManager().getStreamData().streamKey, "dark-red");
4907
5034
  this.unpublish();
4908
5035
  }
4909
5036
  this._main.getConfigManager().getStreamData().streamKey = streamKey;
@@ -4920,8 +5047,12 @@
4920
5047
  this._firstPublish = false;
4921
5048
  return true;
4922
5049
  }
5050
+ /**
5051
+ * Stops publishing and cleans up WebRTC connection
5052
+ */
4923
5053
  unpublish() {
4924
5054
  if (this._debug) this._logger.decoratedLog("Unpublish", "dark-red");
5055
+ this._logger.info(this, "📹 [UNPUBLISH] unpublish() - stopping WebRTC but keeping camera preview");
4925
5056
  if (this._statusConnection != null) {
4926
5057
  this._statusConnection.destroy();
4927
5058
  this._statusConnection = null;
@@ -4935,13 +5066,24 @@
4935
5066
  this._main.dispatchEvent("unpublish", {
4936
5067
  ref: this._main
4937
5068
  });
4938
- this.closeStream();
5069
+ this.closeWebRTCConnection();
5070
+ this.setPublishState(exports.PublishState.UNPUBLISHED);
4939
5071
  }
4940
5072
  /**
4941
- * This method
5073
+ * Stops publishing AND releases camera/microphone
5074
+ */
5075
+ unpublishAndRelease() {
5076
+ this._logger.info(this, "📹 [UNPUBLISH+RELEASE] unpublishAndRelease() - stopping everything");
5077
+ if (this._debug) this._logger.decoratedLog("Unpublish + Release", "dark-red");
5078
+ this.unpublish();
5079
+ this.stopCameraStream();
5080
+ }
5081
+ /**
5082
+ * Sets up orientation listener
4942
5083
  * @private
4943
5084
  */
4944
5085
  setupOrientationListener() {
5086
+ if (this._isDestroyed) return;
4945
5087
  this._orientationChangeHandler = this.handleOrientationChange;
4946
5088
  if (window.screen && window.screen.orientation) {
4947
5089
  window.screen.orientation.addEventListener('change', this._orientationChangeHandler);
@@ -4953,21 +5095,20 @@
4953
5095
  // USER MEDIA
4954
5096
  //------------------------------------------------------------------------//
4955
5097
  /**
4956
- * Error on trying to grab video stream (it usually means that browser does not support WebRTC streaming)
4957
- *
4958
- * @param error
5098
+ * Error handler for getUserMedia
4959
5099
  */
4960
5100
  onUserMediaError(error) {
4961
5101
  return __awaiter(this, void 0, void 0, function* () {
5102
+ if (this._isDestroyed) return;
5103
+ this._logger.error(this, `📹 [ERROR] onUserMediaError() - ${error.name}: ${error.message}`);
4962
5104
  yield this.grabDevices();
4963
- // Dodatkowa obsługa specyficznych błędów getUserMedia, jeśli potrzebna
4964
5105
  if (error.name === "OverconstrainedError") {
4965
5106
  this._logger.warning(this, "Device constraints not satisfied");
4966
5107
  }
4967
5108
  });
4968
5109
  }
4969
5110
  /**
4970
- * This method is used for checking individual access to output devices
5111
+ * Checks individual device access
4971
5112
  * @private
4972
5113
  */
4973
5114
  checkIndividualDeviceAccess() {
@@ -4982,44 +5123,47 @@
4982
5123
  available: false
4983
5124
  }
4984
5125
  };
4985
- // Sprawdzamy fizyczną dostępność urządzeń
5126
+ if (this._isDestroyed) return results;
4986
5127
  try {
4987
5128
  const devices = yield navigator.mediaDevices.enumerateDevices();
5129
+ if (this._isDestroyed) return results;
4988
5130
  results.camera.available = devices.some(device => device.kind === 'videoinput');
4989
5131
  results.microphone.available = devices.some(device => device.kind === 'audioinput');
4990
- // Sprawdzamy czy mamy etykiety urządzeń - ich brak może oznaczać brak uprawnień
4991
5132
  const hasLabels = devices.some(device => device.label !== '');
4992
5133
  if (!hasLabels) {
4993
- // Jeśli nie mamy etykiet, wymuszamy pytanie o uprawnienia
5134
+ this._logger.info(this, "📹 [ACQUIRE] checkIndividualDeviceAccess() - no labels, requesting permissions");
5135
+ if (this._isDestroyed) return results;
4994
5136
  try {
4995
5137
  const stream = yield navigator.mediaDevices.getUserMedia({
4996
5138
  video: results.camera.available,
4997
5139
  audio: results.microphone.available
4998
5140
  });
4999
- // Jeśli udało się uzyskać strumień, mamy uprawnienia
5141
+ // CRITICAL: Check IMMEDIATELY after getUserMedia
5142
+ if (this._isDestroyed) {
5143
+ this._logger.warning(this, "📹 [RELEASE] checkIndividualDeviceAccess() - destroyed, releasing orphan stream");
5144
+ stream.getTracks().forEach(track => track.stop());
5145
+ return results;
5146
+ }
5000
5147
  results.camera.allowed = stream.getVideoTracks().length > 0;
5001
5148
  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ń
5149
+ this._logger.info(this, "📹 [RELEASE] checkIndividualDeviceAccess() - stopping test stream");
5150
+ stream.getTracks().forEach(track => {
5151
+ track.stop();
5152
+ this._logger.info(this, `📹 [RELEASE] checkIndividualDeviceAccess() - stopped track: ${track.kind}`);
5153
+ });
5154
+ if (this._isDestroyed) return results;
5005
5155
  yield this.grabDevices();
5006
5156
  } catch (error) {
5007
5157
  console.error('Error requesting permissions:', error);
5008
- // Nie udało się uzyskać uprawnień
5009
5158
  results.camera.allowed = false;
5010
5159
  results.microphone.allowed = false;
5011
5160
  }
5012
5161
  } else {
5013
- // Jeśli mamy etykiety, prawdopodobnie mamy już uprawnienia
5014
5162
  results.camera.allowed = devices.some(device => device.kind === 'videoinput' && device.label !== '');
5015
5163
  results.microphone.allowed = devices.some(device => device.kind === 'audioinput' && device.label !== '');
5016
5164
  }
5017
5165
  } catch (error) {
5018
5166
  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
5167
  }
5024
5168
  return results;
5025
5169
  });
@@ -5028,21 +5172,18 @@
5028
5172
  // SOCKETS & SDP
5029
5173
  //------------------------------------------------------------------------//
5030
5174
  /**
5031
- * This method handles basic SDP/ICE-Candidate exchange with a Wowza Server
5032
- *
5033
- * @param data
5175
+ * Handles SDP/ICE-Candidate exchange
5034
5176
  */
5035
5177
  onSocketMessage(data) {
5036
5178
  var _a;
5179
+ if (this._isDestroyed) return;
5037
5180
  let msgJSON = JSON.parse(data);
5038
5181
  let msgStatus = Number(msgJSON["status"]);
5039
5182
  switch (msgStatus) {
5040
5183
  case 200:
5041
- // OK
5042
5184
  this._logger.info(this, "SDP Exchange Successful");
5043
5185
  let sdpData = msgJSON['sdp'];
5044
- if (sdpData !== undefined) {
5045
- // @ts-ignore
5186
+ if (sdpData !== undefined && this._peerConnection) {
5046
5187
  this._peerConnection.setRemoteDescription(new RTCSessionDescription(sdpData), () => {}, () => {});
5047
5188
  }
5048
5189
  let iceCandidates = msgJSON['iceCandidates'];
@@ -5053,7 +5194,6 @@
5053
5194
  }
5054
5195
  break;
5055
5196
  case 503:
5056
- // NOT OK
5057
5197
  this._logger.error(this, "StreamKey already use");
5058
5198
  const usedStreamKey = (_a = this._main.getConfigManager().getStreamData().streamKey) !== null && _a !== void 0 ? _a : "unknown";
5059
5199
  this._main.dispatchEvent("streamKeyInUse", {
@@ -5067,14 +5207,8 @@
5067
5207
  //------------------------------------------------------------------------//
5068
5208
  // EVENTS
5069
5209
  //------------------------------------------------------------------------//
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
5210
  onConnectionStateChange(event) {
5211
+ if (this._isDestroyed) return;
5078
5212
  this._logger.info(this, "Connection State Change: " + JSON.stringify(event));
5079
5213
  if (event !== null) {
5080
5214
  switch (event.currentTarget.connectionState) {
@@ -5105,16 +5239,16 @@
5105
5239
  // DEVICES
5106
5240
  //------------------------------------------------------------------------//
5107
5241
  /**
5108
- * Returns list od devices (cameras, microphones) available for user's device
5242
+ * Grabs available devices
5109
5243
  */
5110
5244
  grabDevices() {
5111
5245
  return __awaiter(this, void 0, void 0, function* () {
5246
+ if (this._isDestroyed) return;
5112
5247
  try {
5113
5248
  const deviceAccess = yield this.checkIndividualDeviceAccess();
5249
+ if (this._isDestroyed) return;
5114
5250
  this._cameraList = new InputDeviceList();
5115
5251
  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
5252
  if (!this._permissionChecked) {
5119
5253
  if (!deviceAccess.camera.allowed) {
5120
5254
  this._main.dispatchEvent("cameraAccessDenied", {
@@ -5143,8 +5277,9 @@
5143
5277
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5144
5278
  }
5145
5279
  }
5146
- // Wypełnianie list dostępnymi urządzeniami
5280
+ if (this._isDestroyed) return;
5147
5281
  const devices = yield navigator.mediaDevices.enumerateDevices();
5282
+ if (this._isDestroyed) return;
5148
5283
  for (const device of devices) {
5149
5284
  if (device.deviceId && device.label) {
5150
5285
  if (device.kind === 'videoinput' && deviceAccess.camera.allowed) {
@@ -5156,8 +5291,8 @@
5156
5291
  }
5157
5292
  }
5158
5293
  }
5294
+ if (this._isDestroyed) return;
5159
5295
  try {
5160
- // Aktualizacja wybranych urządzeń
5161
5296
  if (deviceAccess.camera.allowed) {
5162
5297
  this._selectedCamera = this.pickCamera();
5163
5298
  }
@@ -5167,38 +5302,41 @@
5167
5302
  } catch (error) {
5168
5303
  console.log(error);
5169
5304
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5170
- this._logger.error(this, "Errror on grab devices: " + JSON.stringify(error));
5305
+ this._logger.error(this, "Error on grab devices: " + JSON.stringify(error));
5306
+ }
5307
+ if (!this._isDestroyed) {
5308
+ this._main.dispatchEvent("deviceListUpdate", {
5309
+ ref: this._main,
5310
+ cameraList: this._cameraList.getArray(),
5311
+ microphoneList: this._microphoneList.getArray()
5312
+ });
5171
5313
  }
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
5314
  this._permissionChecked = true;
5179
5315
  } catch (error) {
5316
+ if (this._isDestroyed) return;
5180
5317
  console.error("Error in grabDevices:", error);
5181
5318
  this._cameraList = new InputDeviceList();
5182
5319
  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
- });
5320
+ if (!this._isDestroyed) {
5321
+ this._main.dispatchEvent("deviceListUpdate", {
5322
+ ref: this._main,
5323
+ cameraList: this._cameraList.getArray(),
5324
+ microphoneList: this._microphoneList.getArray()
5325
+ });
5326
+ this._main.dispatchEvent("inputDeviceError", {
5327
+ ref: this._main
5328
+ });
5329
+ }
5191
5330
  }
5192
5331
  });
5193
5332
  }
5194
5333
  /**
5195
- * Selects camera based on camera device ID with abort support
5196
- * @param cameraID
5334
+ * Selects camera by ID
5197
5335
  */
5198
5336
  selectCamera(cameraID) {
5199
5337
  var _a, _b, _c;
5200
5338
  return __awaiter(this, void 0, void 0, function* () {
5201
- // Anuluj poprzednie przełączanie kamery
5339
+ if (this._isDestroyed) return;
5202
5340
  if (this._cameraAbortController) {
5203
5341
  this._cameraAbortController.abort();
5204
5342
  }
@@ -5206,13 +5344,13 @@
5206
5344
  const signal = this._cameraAbortController.signal;
5207
5345
  try {
5208
5346
  this._switchingCamera = true;
5347
+ this._logger.info(this, `📹 [SWITCH] selectCamera() - switching to camera: ${cameraID}`);
5209
5348
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5210
5349
  this._cameraList.get(i).isSelected = false;
5211
5350
  }
5212
5351
  this._selectedCamera = null;
5213
5352
  this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5214
5353
  this.setCameraState(exports.DeviceState.NOT_INITIALIZED);
5215
- // Zapamiętaj aktualny stream key i stan publikacji
5216
5354
  const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5217
5355
  const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5218
5356
  let found = false;
@@ -5230,14 +5368,11 @@
5230
5368
  cameraList: this._cameraList.getArray(),
5231
5369
  microphoneList: this._microphoneList.getArray()
5232
5370
  });
5233
- if (signal.aborted) return;
5371
+ if (signal.aborted || this._isDestroyed) return;
5234
5372
  this.stopCameraStream();
5235
5373
  if (this._selectedCamera != null) {
5236
- // Update constraints with new device
5237
5374
  this._constraints.video.deviceId = this._selectedCamera.id;
5238
- // Sprawdź czy nie anulowano
5239
- if (signal.aborted) return;
5240
- // Poczekaj z możliwością anulowania
5375
+ if (signal.aborted || this._isDestroyed) return;
5241
5376
  yield new Promise((resolve, reject) => {
5242
5377
  const timeout = setTimeout(resolve, 500);
5243
5378
  signal.addEventListener('abort', () => {
@@ -5245,20 +5380,20 @@
5245
5380
  reject(new Error('Aborted'));
5246
5381
  });
5247
5382
  });
5248
- if (signal.aborted) return;
5249
- // Restart camera stream
5383
+ if (signal.aborted || this._isDestroyed) return;
5250
5384
  yield this.startCamera();
5385
+ if (this._isDestroyed) return;
5251
5386
  this.setCameraState(exports.DeviceState.ENABLED);
5252
5387
  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) {
5388
+ if (wasPublished && streamKey && !signal.aborted && !this._isDestroyed) {
5254
5389
  this.publish(streamKey);
5255
5390
  }
5256
5391
  } else {
5257
5392
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5258
5393
  }
5259
5394
  } catch (error) {
5260
- if (error.message !== 'Aborted') {
5261
- this._logger.error(this, 'Error switching camera: ' + error);
5395
+ if (error.message !== 'Aborted' && !this._isDestroyed) {
5396
+ this._logger.error(this, '📹 [ERROR] selectCamera() - Error switching camera: ' + error);
5262
5397
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5263
5398
  }
5264
5399
  } finally {
@@ -5270,13 +5405,12 @@
5270
5405
  });
5271
5406
  }
5272
5407
  /**
5273
- * Method tries to select (change) microphone based on its system ID with abort support
5274
- * @param micID
5408
+ * Selects microphone by ID
5275
5409
  */
5276
5410
  selectMicrophone(micID) {
5277
5411
  var _a, _b, _c;
5278
5412
  return __awaiter(this, void 0, void 0, function* () {
5279
- // Anuluj poprzednie przełączanie mikrofonu
5413
+ if (this._isDestroyed) return;
5280
5414
  if (this._microphoneAbortController) {
5281
5415
  this._microphoneAbortController.abort();
5282
5416
  }
@@ -5284,17 +5418,15 @@
5284
5418
  const signal = this._microphoneAbortController.signal;
5285
5419
  try {
5286
5420
  this._switchingMicrophone = true;
5421
+ this._logger.info(this, `📹 [SWITCH] selectMicrophone() - switching to microphone: ${micID}`);
5287
5422
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5288
5423
  this._microphoneList.get(i).isSelected = false;
5289
5424
  }
5290
5425
  this._selectedMicrophone = null;
5291
5426
  this.setInputDeviceState(exports.InputDevicesState.UPDATING);
5292
5427
  this.setMicrophoneState(exports.DeviceState.NOT_INITIALIZED);
5293
- this._logger.info(this, "Selecting microphone: " + micID);
5294
- // Zapamiętaj aktualny stream key i stan publikacji
5295
5428
  const streamKey = (_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getStreamData().streamKey;
5296
5429
  const wasPublished = this._publishState === exports.PublishState.CONNECTED;
5297
- // Znajdź i zapisz wybrany mikrofon
5298
5430
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5299
5431
  if (this._microphoneList.get(i).id == micID) {
5300
5432
  this._selectedMicrophone = this._microphoneList.get(i);
@@ -5303,26 +5435,23 @@
5303
5435
  break;
5304
5436
  }
5305
5437
  }
5306
- // Zawsze wysyłamy aktualizację list urządzeń
5307
5438
  this._main.dispatchEvent("deviceListUpdate", {
5308
5439
  ref: this._main,
5309
5440
  cameraList: this._cameraList.getArray(),
5310
5441
  microphoneList: this._microphoneList.getArray()
5311
5442
  });
5312
- if (signal.aborted) return;
5313
- // Odłącz SoundMeter przed zmianą strumienia
5443
+ if (signal.aborted || this._isDestroyed) return;
5314
5444
  this._soundMeter.detach();
5315
- // Zamknij istniejące połączenie WebRTC
5316
5445
  this.closeWebRTCConnection();
5317
- // Zatrzymaj obecny strumień
5318
5446
  if (this._stream) {
5447
+ this._logger.info(this, "📹 [RELEASE] selectMicrophone() - stopping current stream");
5319
5448
  this._stream.getTracks().forEach(track => {
5320
5449
  track.stop();
5321
5450
  });
5322
5451
  this._stream = null;
5452
+ this._activeStreamCount--;
5323
5453
  }
5324
- if (signal.aborted) return;
5325
- // Poczekaj z możliwością anulowania
5454
+ if (signal.aborted || this._isDestroyed) return;
5326
5455
  yield new Promise((resolve, reject) => {
5327
5456
  const timeout = setTimeout(resolve, 500);
5328
5457
  signal.addEventListener('abort', () => {
@@ -5330,21 +5459,20 @@
5330
5459
  reject(new Error('Aborted'));
5331
5460
  });
5332
5461
  });
5333
- if (signal.aborted) return;
5334
- // Rozpocznij wszystko od nowa
5462
+ if (signal.aborted || this._isDestroyed) return;
5335
5463
  yield this.startCamera();
5464
+ if (this._isDestroyed) return;
5336
5465
  this.setMicrophoneState(exports.DeviceState.ENABLED);
5337
5466
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) {
5338
5467
  this.setInputDeviceState(exports.InputDevicesState.READY);
5339
5468
  } else {
5340
5469
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5341
5470
  }
5342
- // Jeśli stream był opublikowany, publikujemy ponownie
5343
- if (wasPublished && streamKey && !signal.aborted) {
5471
+ if (wasPublished && streamKey && !signal.aborted && !this._isDestroyed) {
5344
5472
  this.publish(streamKey);
5345
5473
  }
5346
5474
  } catch (error) {
5347
- if (error.message !== 'Aborted') {
5475
+ if (error.message !== 'Aborted' && !this._isDestroyed) {
5348
5476
  console.error("Error changing microphone:", error);
5349
5477
  this._main.dispatchEvent("inputDeviceError", {
5350
5478
  ref: this._main
@@ -5360,23 +5488,29 @@
5360
5488
  });
5361
5489
  }
5362
5490
  /**
5363
- * This method tries to start a camera with abort support
5491
+ * Starts camera with abort support
5364
5492
  * @private
5365
5493
  */
5366
5494
  startCamera() {
5367
5495
  var _a;
5368
5496
  return __awaiter(this, void 0, void 0, function* () {
5369
- // Anuluj poprzednie uruchamianie kamery
5497
+ if (this._isDestroyed) {
5498
+ this._logger.warning(this, "📹 [ACQUIRE] startCamera() - aborted, instance is destroyed");
5499
+ return;
5500
+ }
5370
5501
  if (this._startCameraAbortController) {
5371
5502
  this._startCameraAbortController.abort();
5372
5503
  }
5373
5504
  this._startCameraAbortController = new AbortController();
5374
5505
  const signal = this._startCameraAbortController.signal;
5506
+ // Release existing stream first
5375
5507
  if (this._stream) {
5508
+ this._logger.info(this, "📹 [RELEASE] startCamera() - releasing existing stream before acquiring new one");
5376
5509
  this._stream.getTracks().forEach(track => {
5377
5510
  track.stop();
5378
5511
  });
5379
5512
  this._stream = null;
5513
+ this._activeStreamCount--;
5380
5514
  }
5381
5515
  try {
5382
5516
  const constraints = {
@@ -5391,18 +5525,24 @@
5391
5525
  }
5392
5526
  } : false
5393
5527
  };
5394
- if (signal.aborted) return;
5528
+ if (signal.aborted || this._isDestroyed) return;
5529
+ this._logger.info(this, `📹 [ACQUIRE] startCamera() - requesting getUserMedia`);
5395
5530
  try {
5396
5531
  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());
5532
+ // CRITICAL: Check IMMEDIATELY after getUserMedia returns
5533
+ if (signal.aborted || this._isDestroyed) {
5534
+ this._logger.warning(this, "📹 [RELEASE] startCamera() - destroyed during getUserMedia, releasing orphan stream");
5535
+ stream.getTracks().forEach(track => {
5536
+ track.stop();
5537
+ this._logger.info(this, `📹 [RELEASE] startCamera() - stopped orphan track: ${track.kind}, id: ${track.id}`);
5538
+ });
5400
5539
  return;
5401
5540
  }
5402
5541
  this._stream = stream;
5403
5542
  this.onCameraStreamSuccess(this._stream);
5404
5543
  } catch (error) {
5405
- if (signal.aborted) return;
5544
+ if (signal.aborted || this._isDestroyed) return;
5545
+ this._logger.error(this, `📹 [ERROR] startCamera() - getUserMedia failed: ${error.name}: ${error.message}`);
5406
5546
  if (constraints.video) {
5407
5547
  this.onUserMediaError({
5408
5548
  name: error.name || 'Error',
@@ -5420,6 +5560,7 @@
5420
5560
  }
5421
5561
  if (this._cameraState == exports.DeviceState.ENABLED && this._microphoneState == exports.DeviceState.ENABLED) this.setInputDeviceState(exports.InputDevicesState.READY);
5422
5562
  } catch (error) {
5563
+ if (this._isDestroyed) return;
5423
5564
  console.error("Error in startCamera:", error);
5424
5565
  yield this.grabDevices();
5425
5566
  } finally {
@@ -5433,15 +5574,14 @@
5433
5574
  * Updates WebRTC connection with new stream
5434
5575
  */
5435
5576
  updateWebRTCStream() {
5577
+ if (this._isDestroyed) return;
5436
5578
  if (!this._peerConnection || !this._stream) {
5437
5579
  return;
5438
5580
  }
5439
- // Remove all existing tracks from the peer connection
5440
5581
  const senders = this._peerConnection.getSenders();
5441
5582
  senders.forEach(sender => {
5442
5583
  if (this._peerConnection) this._peerConnection.removeTrack(sender);
5443
5584
  });
5444
- // Add new tracks
5445
5585
  this._stream.getTracks().forEach(track => {
5446
5586
  if (this._stream != null && this._peerConnection) {
5447
5587
  this._peerConnection.addTrack(track, this._stream);
@@ -5449,32 +5589,24 @@
5449
5589
  });
5450
5590
  }
5451
5591
  /**
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
5592
+ * Picks camera based on saved ID or defaults
5460
5593
  * @private
5461
5594
  */
5462
5595
  pickCamera() {
5463
5596
  var _a, _b, _c, _d, _e, _f;
5597
+ if (this._isDestroyed) return null;
5464
5598
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5465
5599
  this._cameraList.get(i).isSelected = false;
5466
5600
  }
5467
5601
  let savedCameraID = (_b = (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.getField("cameraID")) !== null && _b !== void 0 ? _b : null;
5468
5602
  if (this._cameraList.getSize() > 0) {
5469
5603
  if (savedCameraID) {
5470
- // Szukamy zapisanej kamery
5471
5604
  let found = false;
5472
5605
  for (let i = 0; i < this._cameraList.getSize(); i++) {
5473
5606
  if (this._cameraList.get(i).id === savedCameraID) {
5474
5607
  this._selectedCamera = this._cameraList.get(i);
5475
5608
  this._selectedCamera.isSelected = true;
5476
5609
  this.setCameraState(exports.DeviceState.ENABLED);
5477
- // Ustaw deviceId w constraints
5478
5610
  found = true;
5479
5611
  this._constraints.video.deviceId = this._selectedCamera.id;
5480
5612
  break;
@@ -5491,10 +5623,8 @@
5491
5623
  this._logger.info(this, "Canceling Publish!");
5492
5624
  this._main.getConfigManager().getStreamData().streamKey = null;
5493
5625
  }
5494
- return null;
5495
5626
  }
5496
5627
  }
5497
- // Jeśli nie znaleziono zapisanej kamery, używamy pierwszej
5498
5628
  if (!this._selectedCamera) {
5499
5629
  this._main.dispatchEvent("savedCameraNotFound", {
5500
5630
  ref: this._main,
@@ -5517,19 +5647,22 @@
5517
5647
  }
5518
5648
  }
5519
5649
  }
5520
- this._main.dispatchEvent("deviceListUpdate", {
5521
- ref: this._main,
5522
- cameraList: this._cameraList.getArray(),
5523
- microphoneList: this._microphoneList.getArray()
5524
- });
5650
+ if (!this._isDestroyed) {
5651
+ this._main.dispatchEvent("deviceListUpdate", {
5652
+ ref: this._main,
5653
+ cameraList: this._cameraList.getArray(),
5654
+ microphoneList: this._microphoneList.getArray()
5655
+ });
5656
+ }
5525
5657
  return this._selectedCamera;
5526
5658
  }
5527
5659
  /**
5528
- * This method selects a microphone based on previous uses or saved IDs
5660
+ * Picks microphone based on saved ID or defaults
5529
5661
  * @private
5530
5662
  */
5531
5663
  pickMicrophone() {
5532
5664
  var _a, _b, _c, _d, _e, _f;
5665
+ if (this._isDestroyed) return null;
5533
5666
  for (let i = 0; i < this._microphoneList.getSize(); i++) {
5534
5667
  this._microphoneList.get(i).isSelected = false;
5535
5668
  }
@@ -5556,7 +5689,6 @@
5556
5689
  this._logger.info(this, "Canceling Publish!");
5557
5690
  this._main.getConfigManager().getStreamData().streamKey = null;
5558
5691
  }
5559
- return null;
5560
5692
  }
5561
5693
  }
5562
5694
  if (!this._selectedMicrophone) {
@@ -5585,73 +5717,43 @@
5585
5717
  }
5586
5718
  return this._selectedMicrophone;
5587
5719
  }
5588
- /**
5589
- * Cleans all saved cameras and microphones IDs.
5590
- */
5591
5720
  clearSavedDevices() {
5592
5721
  var _a, _b;
5593
5722
  (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.removeField("cameraID");
5594
5723
  (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.removeField("microphoneID");
5595
5724
  }
5596
- /**
5597
- * Messes up camera's and microphone's id (for testing only)
5598
- */
5599
5725
  messSavedDevices() {
5600
5726
  var _a, _b;
5601
5727
  (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.saveField("cameraID", "a");
5602
5728
  (_b = this._main.getStorageManager()) === null || _b === void 0 ? void 0 : _b.saveField("microphoneID", "b");
5603
5729
  }
5604
- /**
5605
- * Handles microphone muting state
5606
- * @param microphoneState true to unmute, false to mute
5607
- */
5608
5730
  muteMicrophone(shouldMute) {
5731
+ var _a;
5732
+ if (this._isDestroyed) return;
5609
5733
  if (this._isMicrophoneMuted === shouldMute) {
5610
- // State hasn't changed, no need to do anything
5611
5734
  return;
5612
5735
  }
5613
5736
  this._isMicrophoneMuted = shouldMute;
5737
+ (_a = this._main.getStorageManager()) === null || _a === void 0 ? void 0 : _a.saveField("microphoneMuted", shouldMute ? "true" : "false");
5614
5738
  if (this._stream) {
5615
- this.applyMicrophoneState(!shouldMute); // Odwracamy wartość dla track.enabled
5739
+ this.applyMicrophoneState(!shouldMute);
5616
5740
  } 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})`);
5741
+ this._pendingMicrophoneState = !shouldMute;
5620
5742
  }
5621
- // Always dispatch the event to keep UI in sync
5622
5743
  this._main.dispatchEvent("microphoneStateChange", {
5623
5744
  ref: this._main,
5624
5745
  isMuted: this._isMicrophoneMuted
5625
5746
  });
5626
5747
  }
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
5748
  applyMicrophoneState(enabled) {
5633
- if (!this._stream) {
5634
- this._logger.warning(this, "WebRTCStreamer :: Cannot apply microphone state - stream not available");
5635
- return;
5636
- }
5749
+ if (!this._stream) return;
5637
5750
  const audioTracks = this._stream.getAudioTracks();
5638
5751
  if (audioTracks && audioTracks.length > 0) {
5639
- this._logger.success(this, `WebRTCStreamer :: ${enabled ? 'Unmuting' : 'Muting'} microphone`);
5640
5752
  audioTracks.forEach(track => track.enabled = enabled);
5641
- } else {
5642
- this._logger.warning(this, "WebRTCStreamer :: No audio tracks found in stream");
5643
5753
  }
5644
5754
  }
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
5755
  isStreamReady(requireVideo = true, requireAudio = true) {
5652
- if (!this._stream) {
5653
- return false;
5654
- }
5756
+ if (!this._stream) return false;
5655
5757
  const videoTracks = this._stream.getVideoTracks();
5656
5758
  const audioTracks = this._stream.getAudioTracks();
5657
5759
  const videoReady = !requireVideo || videoTracks.length > 0 && videoTracks[0].readyState === 'live';
@@ -5660,51 +5762,18 @@
5660
5762
  }
5661
5763
  closeWebRTCConnection() {
5662
5764
  if (this._peerConnection) {
5663
- this._peerConnection.close();
5765
+ this._logger.info(this, "📡 [WEBRTC] closeWebRTCConnection() - closing peer connection");
5766
+ this._peerConnection.onicecandidate = null;
5767
+ this._peerConnection.onconnectionstatechange = null;
5768
+ this._peerConnection.onnegotiationneeded = null;
5769
+ try {
5770
+ this._peerConnection.close();
5771
+ } catch (e) {
5772
+ // Ignore
5773
+ }
5664
5774
  this._peerConnection = null;
5665
5775
  }
5666
5776
  }
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
5777
  onDescriptionError(error) {
5709
5778
  this._logger.info(this, "WebRTCStreamer :: onDescriptionError: " + JSON.stringify(error));
5710
5779
  }
@@ -5716,6 +5785,7 @@
5716
5785
  //------------------------------------------------------------------------//
5717
5786
  createStatusConnection() {
5718
5787
  var _a, _b, _c;
5788
+ if (this._isDestroyed) return;
5719
5789
  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
5790
  if (!serverItem) return;
5721
5791
  if (this._statusConnection) {
@@ -5736,6 +5806,7 @@
5736
5806
  }
5737
5807
  setPublishState(newState) {
5738
5808
  if (this._publishState == newState) return;
5809
+ if (this._isDestroyed) return;
5739
5810
  if (this._debug) this._logger.decoratedLog("Publish State: " + newState, "dark-blue");
5740
5811
  this._logger.info(this, "Publish State: " + newState);
5741
5812
  if (newState == exports.PublishState.PUBLISHED) this._publishTime = new Date().getTime();
@@ -5748,9 +5819,9 @@
5748
5819
  getPublishTime() {
5749
5820
  return this._publishState == exports.PublishState.PUBLISHED ? this._publishTime : 0;
5750
5821
  }
5751
- // DEVICE STATE
5752
5822
  setInputDeviceState(newState) {
5753
5823
  if (this._inputDeviceState == newState) return;
5824
+ if (this._isDestroyed) return;
5754
5825
  this._inputDeviceState = newState;
5755
5826
  this._main.dispatchEvent("deviceStateChange", {
5756
5827
  ref: this._main,
@@ -5762,9 +5833,9 @@
5762
5833
  getInputDeviceState() {
5763
5834
  return this._inputDeviceState;
5764
5835
  }
5765
- // CAMERA STATE
5766
5836
  setCameraState(newState) {
5767
5837
  if (this._cameraState == newState) return;
5838
+ if (this._isDestroyed) return;
5768
5839
  this._cameraState = newState;
5769
5840
  this._main.dispatchEvent("cameraDeviceStateChange", {
5770
5841
  ref: this._main,
@@ -5775,9 +5846,9 @@
5775
5846
  getCameraState() {
5776
5847
  return this._cameraState;
5777
5848
  }
5778
- // MICROPHONE STATE
5779
5849
  setMicrophoneState(newState) {
5780
5850
  if (this._microphoneState == newState) return;
5851
+ if (this._isDestroyed) return;
5781
5852
  this._microphoneState = newState;
5782
5853
  this._main.dispatchEvent("microphoneDeviceStateChange", {
5783
5854
  ref: this._main,
@@ -5798,108 +5869,70 @@
5798
5869
  return this._publishState;
5799
5870
  }
5800
5871
  //------------------------------------------------------------------------//
5872
+ // DEBUG
5873
+ //------------------------------------------------------------------------//
5874
+ debugMediaState() {
5875
+ var _a, _b;
5876
+ console.group("🎥 Media Debug State");
5877
+ console.log("=== STREAM INFO ===");
5878
+ console.log("this._stream:", this._stream);
5879
+ console.log("Active stream count:", this._activeStreamCount);
5880
+ console.log("Is destroyed:", this._isDestroyed);
5881
+ if (this._stream) {
5882
+ console.log("Stream ID:", this._stream.id);
5883
+ console.log("Stream active:", this._stream.active);
5884
+ console.log("--- Video Tracks ---");
5885
+ this._stream.getVideoTracks().forEach((track, index) => {
5886
+ console.log(` Track ${index}:`, {
5887
+ id: track.id,
5888
+ enabled: track.enabled,
5889
+ readyState: track.readyState,
5890
+ muted: track.muted
5891
+ });
5892
+ });
5893
+ console.log("--- Audio Tracks ---");
5894
+ this._stream.getAudioTracks().forEach((track, index) => {
5895
+ console.log(` Track ${index}:`, {
5896
+ id: track.id,
5897
+ enabled: track.enabled,
5898
+ readyState: track.readyState,
5899
+ muted: track.muted
5900
+ });
5901
+ });
5902
+ }
5903
+ console.log("=== VIDEO ELEMENT ===");
5904
+ const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5905
+ if ((videoElement === null || videoElement === void 0 ? void 0 : videoElement.srcObject) instanceof MediaStream) {
5906
+ const srcStream = videoElement.srcObject;
5907
+ console.log("Video srcObject stream ID:", srcStream.id);
5908
+ console.log("Video srcObject active:", srcStream.active);
5909
+ console.log("Same as this._stream:", srcStream === this._stream);
5910
+ }
5911
+ console.groupEnd();
5912
+ }
5913
+ //------------------------------------------------------------------------//
5801
5914
  // DESTROY & DELETE
5802
5915
  //------------------------------------------------------------------------//
5803
- /**
5804
- * Method used to stop camera from streaming
5805
- * @private
5806
- */
5807
5916
  stopCameraStream() {
5808
5917
  var _a, _b;
5809
5918
  if (this._stream) {
5810
- this._stream.getTracks().forEach(track => track.stop());
5919
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - stopping stream, id: ${this._stream.id}`);
5920
+ this._stream.getTracks().forEach(track => {
5921
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - stopping track: ${track.kind}, id: ${track.id}`);
5922
+ track.stop();
5923
+ });
5811
5924
  const videoElement = (_b = (_a = this._main.getStageController()) === null || _a === void 0 ? void 0 : _a.getScreenElement()) === null || _b === void 0 ? void 0 : _b.getVideoElement();
5812
5925
  if (videoElement) {
5813
5926
  videoElement.srcObject = null;
5814
5927
  }
5815
5928
  this._soundMeter.detach();
5816
5929
  this._stream = null;
5930
+ this._activeStreamCount--;
5931
+ this._logger.info(this, `📹 [RELEASE] stopCameraStream() - complete, active streams: ${this._activeStreamCount}`);
5817
5932
  }
5818
5933
  }
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
5934
  stop() {
5902
- // Anuluj wszystkie aktywne operacje
5935
+ this._logger.info(this, "📹 [STOP] stop() - stopping all operations");
5903
5936
  if (this._cameraAbortController) {
5904
5937
  this._cameraAbortController.abort();
5905
5938
  this._cameraAbortController = null;
@@ -5912,7 +5945,6 @@
5912
5945
  this._startCameraAbortController.abort();
5913
5946
  this._startCameraAbortController = null;
5914
5947
  }
5915
- // Stop status connection and clear timer
5916
5948
  if (this._statusConnection) {
5917
5949
  this._statusConnection.destroy();
5918
5950
  this._statusConnection = null;
@@ -5922,16 +5954,12 @@
5922
5954
  this._statusTimer = null;
5923
5955
  }
5924
5956
  this._main.getConfigManager().getStreamData().streamKey = null;
5925
- // Close WebRTC connection
5926
5957
  this.closeWebRTCConnection();
5927
- // Stop all media streams
5928
5958
  this.stopCameraStream();
5929
- // Reset states
5930
5959
  this.setPublishState(exports.PublishState.STOPPED);
5931
5960
  this.setInputDeviceState(exports.InputDevicesState.STOPPED);
5932
5961
  this.setCameraState(exports.DeviceState.STOPPED);
5933
5962
  this.setMicrophoneState(exports.DeviceState.STOPPED);
5934
- // Clear restart timer if exists
5935
5963
  if (this._restartTimer) {
5936
5964
  clearInterval(this._restartTimer);
5937
5965
  this._restartTimer = null;
@@ -5939,127 +5967,140 @@
5939
5967
  this._restartTimerCount = 0;
5940
5968
  clearTimeout(this._publishTimer);
5941
5969
  }
5942
- /**
5943
- * Reinitializes the streaming setup
5944
- */
5945
5970
  start() {
5946
5971
  var _a, _b, _c;
5947
5972
  return __awaiter(this, void 0, void 0, function* () {
5973
+ if (this._isDestroyed) return;
5974
+ this._logger.info(this, "📹 [START] start() - reinitializing streaming");
5948
5975
  try {
5949
- // Reset states
5950
5976
  this._publishState = exports.PublishState.NOT_INITIALIZED;
5951
5977
  this._inputDeviceState = exports.InputDevicesState.NOT_INITIALIZED;
5952
5978
  this._cameraState = exports.DeviceState.NOT_INITIALIZED;
5953
5979
  this._microphoneState = exports.DeviceState.NOT_INITIALIZED;
5954
- // Reinitialize devices and stream
5955
5980
  yield this.initializeDevices();
5981
+ if (this._isDestroyed) return;
5956
5982
  yield this.startCamera();
5957
- // If autoConnect is enabled, initialize network
5983
+ if (this._isDestroyed) return;
5958
5984
  if ((_a = this._main.getConfigManager()) === null || _a === void 0 ? void 0 : _a.getSettingsData().autoConnect) {
5959
5985
  (_b = this._main.getNetworkController()) === null || _b === void 0 ? void 0 : _b.initialize();
5960
5986
  }
5961
- // Reinitialize status connection if needed
5962
5987
  if ((_c = this._main.getConfigManager()) === null || _c === void 0 ? void 0 : _c.getStreamData().streamKey) {
5963
5988
  this.createStatusConnection();
5964
5989
  }
5965
5990
  } catch (error) {
5966
- this._logger.error(this, "Start failed: " + JSON.stringify(error));
5991
+ if (this._isDestroyed) return;
5992
+ this._logger.error(this, "📹 [START] start() - failed: " + JSON.stringify(error));
5967
5993
  this.setInputDeviceState(exports.InputDevicesState.INVALID);
5968
5994
  throw error;
5969
5995
  }
5970
5996
  });
5971
5997
  }
5972
5998
  /**
5973
- * Method used for destroying everything (one-time use) with proper cleanup
5999
+ * SYNCHRONOUS destroy - sets flag immediately, cleanup happens in background
6000
+ * This ensures that even if called without await, all async operations will abort
5974
6001
  */
5975
6002
  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();
6003
+ var _a, _b, _c, _d;
6004
+ // Prevent double destroy
6005
+ if (this._isDestroyed) {
6006
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warning(this, "🔴 [DESTROY] Already destroyed, skipping");
6007
+ return;
6008
+ }
6009
+ this._logger.info(this, "🔴 [DESTROY] Starting StreamerController destroy (sync)...");
6010
+ // CRITICAL: Set flag IMMEDIATELY - this will cause all pending async operations to abort
6011
+ this._isDestroyed = true;
6012
+ // Cancel all abort controllers immediately
6013
+ if (this._cameraAbortController) {
6014
+ this._cameraAbortController.abort();
6015
+ this._cameraAbortController = null;
6016
+ }
6017
+ if (this._microphoneAbortController) {
6018
+ this._microphoneAbortController.abort();
6019
+ this._microphoneAbortController = null;
6020
+ }
6021
+ if (this._startCameraAbortController) {
6022
+ this._startCameraAbortController.abort();
6023
+ this._startCameraAbortController = null;
6024
+ }
6025
+ // Stop all timers immediately
6026
+ if (this._statusTimer != null) {
6027
+ clearInterval(this._statusTimer);
6028
+ this._statusTimer = null;
6029
+ }
6030
+ if (this._restartTimer != null) {
6031
+ clearInterval(this._restartTimer);
6032
+ this._restartTimer = null;
6033
+ }
6034
+ clearTimeout(this._publishTimer);
6035
+ // Remove permission listeners
6036
+ this.removePermissionListeners();
6037
+ // Remove device change listener
6038
+ if (this._deviceChangeHandler) {
6039
+ navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeHandler);
6040
+ this._deviceChangeHandler = null;
6041
+ }
6042
+ // Remove orientation listener
6043
+ if (this._orientationChangeHandler) {
6044
+ if (window.screen && window.screen.orientation) {
6045
+ window.screen.orientation.removeEventListener('change', this._orientationChangeHandler);
6046
+ } else {
6047
+ window.removeEventListener('orientationchange', this._orientationChangeHandler);
6048
+ }
6049
+ this._orientationChangeHandler = null;
6050
+ }
6051
+ // Remove event listeners
6052
+ try {
6053
+ this._main.removeEventListener("serverConnect", this.onServerConnect);
6054
+ this._main.removeEventListener("serverDisconnect", this.onServerDisconnect);
6055
+ this._main.removeEventListener("streamKeyInUse", this.onStreamKeyTaken);
6056
+ this._main.removeEventListener("statusServerConnect", this.onStatusServerConnect);
6057
+ this._main.removeEventListener("statusServerDisconnect", this.onStatusServerDisconnect);
6058
+ this._main.removeEventListener("streamStatusUpdate", this.onStreamStatsUpdate);
6059
+ this._main.removeEventListener("deviceStateChange", this.onDeviceStateChange);
6060
+ document.removeEventListener("visibilitychange", this.visibilityChange);
6061
+ window.removeEventListener("blur", this.onWindowBlur);
6062
+ window.removeEventListener("focus", this.onWindowFocus);
6063
+ } catch (e) {
6064
+ // Ignore errors
6065
+ }
6066
+ // Destroy status connection
6067
+ if (this._statusConnection) {
6068
+ this._statusConnection.destroy();
6069
+ this._statusConnection = null;
6070
+ }
6071
+ // Close WebRTC
6072
+ this.closeWebRTCConnection();
6073
+ // Stop camera stream
6074
+ if (this._stream) {
6075
+ this._logger.info(this, "📹 [FORCE_STOP] Stopping main stream");
6076
+ this._stream.getTracks().forEach(track => {
6077
+ track.stop();
6078
+ });
6079
+ this._stream = null;
6080
+ }
6081
+ // Clean video element
6082
+ try {
6083
+ const videoElement = (_c = (_b = this._main.getStageController()) === null || _b === void 0 ? void 0 : _b.getScreenElement()) === null || _c === void 0 ? void 0 : _c.getVideoElement();
6084
+ if (videoElement) {
6085
+ if (videoElement.srcObject instanceof MediaStream) {
6086
+ videoElement.srcObject.getTracks().forEach(track => track.stop());
6055
6087
  }
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;
6088
+ videoElement.srcObject = null;
6061
6089
  }
6062
- });
6090
+ } catch (e) {
6091
+ // Ignore
6092
+ }
6093
+ // Destroy sound meter
6094
+ try {
6095
+ (_d = this._soundMeter) === null || _d === void 0 ? void 0 : _d.destroy();
6096
+ } catch (e) {
6097
+ // Ignore
6098
+ }
6099
+ // Reset variables
6100
+ this._selectedCamera = null;
6101
+ this._selectedMicrophone = null;
6102
+ this._activeStreamCount = 0;
6103
+ this._logger.success(this, "🔴 [DESTROY] StreamerController destroyed (sync)");
6063
6104
  }
6064
6105
  }
6065
6106
 
@@ -6671,52 +6712,19 @@
6671
6712
  * Main class of the player. The player itself has no GUI, but can be controlled via provided API.
6672
6713
  */
6673
6714
  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
6715
  constructor(streamConfig, autoInitialize = false) {
6684
6716
  super();
6685
- /**
6686
- * Indicates whether the streamer object is in development mode (provides more debug options)
6687
- * @private
6688
- */
6689
6717
  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
- */
6718
+ this.STREAMER_VERSION = "1.0.1";
6719
+ this.COMPILE_DATE = "2/18/2026, 5:26:13 PM";
6704
6720
  this.STREAMER_BRANCH = "Experimental";
6705
- /**
6706
- * Defines number of streamer protocol that is required on server-side
6707
- * @private
6708
- */
6709
6721
  this.STREAMER_PROTOCOL_VERSION = 1;
6710
- /**
6711
- * Indicates whether streamer was initialized or not
6712
- * @private
6713
- */
6714
6722
  this._initialized = false;
6723
+ this._isDestroyed = false;
6715
6724
  if (typeof window === 'undefined' || !window.document || !window.document.createElement) {
6716
6725
  console.error(`StormStreamer Creation Error - No "window" element in the provided context!`);
6717
6726
  return;
6718
6727
  }
6719
- // WINDOW.StormStreamerArray
6720
6728
  if (this.DEV_MODE && !('StormStreamerArray' in window)) {
6721
6729
  window.StormStreamerArray = [];
6722
6730
  }
@@ -6726,17 +6734,13 @@
6726
6734
  this.setStreamConfig(streamConfig);
6727
6735
  if (autoInitialize) this.initialize();
6728
6736
  }
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
6737
  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
6738
+ if (this._isRemoved || this._isDestroyed) return;
6739
+ if (this._configManager == null) throw Error("Stream Config was not provided for this streamer!");
6740
+ this._storageManager = new StorageManager(this);
6741
+ this._stageController = new StageController(this);
6742
+ this._networkController = new NetworkController(this);
6743
+ this._streamerController = new StreamerController(this);
6740
6744
  this._statsController = new StatsController(this);
6741
6745
  this._graphs = [];
6742
6746
  this._initialized = true;
@@ -6744,16 +6748,8 @@
6744
6748
  ref: this
6745
6749
  });
6746
6750
  }
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
6751
  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
- */
6752
+ if (this._isRemoved || this._isDestroyed) return;
6757
6753
  const copiedStreamConfig = JSON.parse(JSON.stringify(streamConfig));
6758
6754
  if (this._configManager == null) {
6759
6755
  this._configManager = new ConfigManager(copiedStreamConfig);
@@ -6778,290 +6774,133 @@
6778
6774
  });
6779
6775
  }
6780
6776
  }
6781
- //------------------------------------------------------------------------//
6782
6777
  // 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
6778
  isConnected() {
6790
6779
  var _a, _b;
6791
6780
  return (_b = (_a = this._networkController) === null || _a === void 0 ? void 0 : _a.getConnection().isConnectionActive()) !== null && _b !== void 0 ? _b : false;
6792
6781
  }
6793
- /**
6794
- * Mutes the streamer's video object. Audio output will be silenced.
6795
- */
6796
6782
  mute() {
6797
- if (this._stageController != null) {
6798
- if (this._stageController.getScreenElement() != null) {
6799
- this._stageController.getScreenElement().setMuted(true);
6800
- return;
6801
- }
6783
+ var _a;
6784
+ if (((_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) != null) {
6785
+ this._stageController.getScreenElement().setMuted(true);
6786
+ return;
6802
6787
  }
6803
6788
  this._configManager.getSettingsData().getAudioData().muted = true;
6804
6789
  }
6805
- /**
6806
- * Unmutes the streamer's video object. Audio output will be restored.
6807
- */
6808
6790
  unmute() {
6809
- if (this._stageController != null) {
6810
- if (this._stageController.getScreenElement() != null) {
6811
- this._stageController.getScreenElement().setMuted(false);
6812
- return;
6813
- }
6791
+ var _a;
6792
+ if (((_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getScreenElement()) != null) {
6793
+ this._stageController.getScreenElement().setMuted(false);
6794
+ return;
6814
6795
  }
6815
6796
  this._configManager.getSettingsData().getAudioData().muted = false;
6816
6797
  }
6817
- /**
6818
- * Checks whether the streamer audio is currently muted.
6819
- *
6820
- * @returns Boolean indicating mute status
6821
- */
6822
6798
  isMute() {
6823
6799
  var _a, _b, _c, _d;
6824
6800
  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
6801
  }
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
6802
  toggleMute() {
6832
6803
  const isMuted = this.isMute();
6833
- if (isMuted) {
6834
- this.unmute();
6835
- } else {
6836
- this.mute();
6837
- }
6804
+ if (isMuted) this.unmute();else this.mute();
6838
6805
  return !isMuted;
6839
6806
  }
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
6807
  setVolume(newVolume) {
6847
6808
  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
- }
6809
+ 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
6810
  this._configManager.getSettingsData().getAudioData().startVolume = newVolume;
6852
6811
  }
6853
- /**
6854
- * Returns current streamer volume (0-100).
6855
- *
6856
- * @returns Current volume level
6857
- */
6858
6812
  getVolume() {
6859
6813
  var _a, _b, _c;
6860
6814
  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
6815
  }
6862
- /**
6863
- * Returns the list of available camera devices.
6864
- *
6865
- * @returns Array of camera input devices
6866
- */
6867
6816
  getCameraList() {
6868
6817
  var _a, _b;
6869
6818
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCameraList()) !== null && _b !== void 0 ? _b : [];
6870
6819
  }
6871
- /**
6872
- * Returns the list of available microphone devices.
6873
- *
6874
- * @returns Array of microphone input devices
6875
- */
6876
6820
  getMicrophoneList() {
6877
6821
  var _a, _b;
6878
6822
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getMicrophoneList()) !== null && _b !== void 0 ? _b : [];
6879
6823
  }
6880
- /**
6881
- * Sets the active camera device by ID.
6882
- *
6883
- * @param cameraID - ID of the camera device to use
6884
- */
6885
6824
  setCamera(cameraID) {
6886
6825
  var _a;
6887
6826
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.selectCamera(cameraID);
6888
6827
  }
6889
- /**
6890
- * Sets the active microphone device by ID.
6891
- *
6892
- * @param microphoneID - ID of the microphone device to use
6893
- */
6894
6828
  setMicrophone(microphoneID) {
6895
6829
  var _a;
6896
6830
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.selectMicrophone(microphoneID);
6897
6831
  }
6898
- /**
6899
- * Returns the currently active camera device.
6900
- *
6901
- * @returns Current camera device or null if none is active
6902
- */
6903
6832
  getCurrentCamera() {
6904
- return this._streamerController.getCurrentCamera();
6833
+ var _a, _b;
6834
+ return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCurrentCamera()) !== null && _b !== void 0 ? _b : null;
6905
6835
  }
6906
- /**
6907
- * Returns the currently active microphone device.
6908
- *
6909
- * @returns Current microphone device or null if none is active
6910
- */
6911
6836
  getCurrentMicrophone() {
6912
- return this._streamerController.getCurrentMicrophone();
6837
+ var _a, _b;
6838
+ return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCurrentMicrophone()) !== null && _b !== void 0 ? _b : null;
6913
6839
  }
6914
- /**
6915
- * Mutes or unmutes the microphone.
6916
- *
6917
- * @param microphoneState - True to mute, false to unmute
6918
- */
6919
6840
  muteMicrophone(microphoneState) {
6920
6841
  var _a;
6921
6842
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.muteMicrophone(microphoneState);
6922
6843
  }
6923
- /**
6924
- * Checks if the microphone is currently muted.
6925
- *
6926
- * @returns Boolean indicating if microphone is muted
6927
- */
6928
6844
  isMicrophoneMuted() {
6929
6845
  var _a, _b;
6930
6846
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.isMicrophoneMuted()) !== null && _b !== void 0 ? _b : false;
6931
6847
  }
6932
- /**
6933
- * Returns the current publishing state of the streamer.
6934
- *
6935
- * @returns Current publishing state
6936
- */
6937
6848
  getPublishState() {
6938
6849
  var _a, _b;
6939
6850
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getPublishState()) !== null && _b !== void 0 ? _b : exports.PublishState.NOT_INITIALIZED;
6940
6851
  }
6941
- /**
6942
- * Returns the total time the stream has been publishing in milliseconds.
6943
- *
6944
- * @returns Publishing time in milliseconds
6945
- */
6946
6852
  getPublishTime() {
6947
6853
  var _a, _b;
6948
6854
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getPublishTime()) !== null && _b !== void 0 ? _b : 0;
6949
6855
  }
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
6856
  publish(streamKey) {
6957
6857
  var _a, _b;
6958
6858
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.publish(streamKey)) !== null && _b !== void 0 ? _b : false;
6959
6859
  }
6960
- /**
6961
- * Stops publishing the current stream.
6962
- */
6963
6860
  unpublish() {
6964
6861
  var _a;
6965
- console.log("kutas 1");
6966
6862
  (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.unpublish();
6967
6863
  }
6968
- /**
6969
- * Returns the current state of input devices (camera and microphone).
6970
- *
6971
- * @returns Current state of input devices
6972
- */
6973
6864
  getInputDevicesState() {
6974
6865
  var _a, _b;
6975
6866
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getInputDeviceState()) !== null && _b !== void 0 ? _b : exports.InputDevicesState.NOT_INITIALIZED;
6976
6867
  }
6977
- /**
6978
- * Returns the current state of the camera device.
6979
- *
6980
- * @returns Current camera device state
6981
- */
6982
6868
  getCameraState() {
6983
6869
  var _a, _b;
6984
6870
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getCameraState()) !== null && _b !== void 0 ? _b : exports.DeviceState.NOT_INITIALIZED;
6985
6871
  }
6986
- /**
6987
- * Returns the current state of the microphone device.
6988
- *
6989
- * @returns Current microphone device state
6990
- */
6991
6872
  getMicrophoneState() {
6992
6873
  var _a, _b;
6993
6874
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.getMicrophoneState()) !== null && _b !== void 0 ? _b : exports.DeviceState.NOT_INITIALIZED;
6994
6875
  }
6995
- /**
6996
- * Clears saved device preferences from storage.
6997
- */
6998
6876
  clearSavedDevices() {
6999
6877
  var _a;
7000
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.clearSavedDevices();
6878
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.clearSavedDevices();
7001
6879
  }
7002
- /**
7003
- * Randomizes saved device preferences (for testing purposes).
7004
- */
7005
6880
  messSavedDevices() {
7006
6881
  var _a;
7007
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.messSavedDevices();
6882
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.messSavedDevices();
7008
6883
  }
7009
- /**
7010
- * Checks if the stream is ready for publishing.
7011
- *
7012
- * @returns Boolean indicating if stream is ready
7013
- */
7014
6884
  isStreamReady() {
7015
6885
  var _a, _b;
7016
6886
  return (_b = (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.isStreamReady()) !== null && _b !== void 0 ? _b : false;
7017
6887
  }
7018
- //------------------------------------------------------------------------//
7019
6888
  // 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
6889
  attachToContainer(container) {
7029
6890
  var _a, _b;
7030
- let result = false;
7031
6891
  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;
6892
+ return false;
7033
6893
  }
7034
- /**
7035
- * Detaches the streamer from the current parent element, if possible.
7036
- *
7037
- * @returns Boolean indicating if detachment was successful
7038
- */
7039
6894
  detachFromContainer() {
7040
6895
  var _a, _b;
7041
- let result = false;
7042
6896
  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;
6897
+ return false;
7044
6898
  }
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
6899
  getContainer() {
7051
6900
  var _a, _b;
7052
6901
  return (_b = (_a = this._stageController) === null || _a === void 0 ? void 0 : _a.getParentElement()) !== null && _b !== void 0 ? _b : null;
7053
6902
  }
7054
- //------------------------------------------------------------------------//
7055
6903
  // 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
6904
  setSize(width, height) {
7066
6905
  if (this._initialized) this._stageController.setSize(width, height);else {
7067
6906
  const parsedWidth = NumberUtilities.parseValue(width);
@@ -7072,13 +6911,6 @@
7072
6911
  this._configManager.getSettingsData().getVideoData().videoHeightInPixels = parsedHeight.isPixels;
7073
6912
  }
7074
6913
  }
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
6914
  setWidth(width) {
7083
6915
  if (this._initialized) this._stageController.setWidth(width);else {
7084
6916
  const parsedWidth = NumberUtilities.parseValue(width);
@@ -7086,13 +6918,6 @@
7086
6918
  this._configManager.getSettingsData().getVideoData().videoWidthInPixels = parsedWidth.isPixels;
7087
6919
  }
7088
6920
  }
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
6921
  setHeight(height) {
7097
6922
  if (this._initialized) this._stageController.setHeight(height);else {
7098
6923
  const parsedHeight = NumberUtilities.parseValue(height);
@@ -7100,66 +6925,26 @@
7100
6925
  this._configManager.getSettingsData().getVideoData().videoHeightInPixels = parsedHeight.isPixels;
7101
6926
  }
7102
6927
  }
7103
- /**
7104
- * Returns current streamer width in pixels.
7105
- *
7106
- * @returns Current width in pixels
7107
- */
7108
6928
  getWidth() {
7109
- if (this._initialized) return this._stageController.getContainerWidth();else {
7110
- if (this._configManager.getSettingsData().getVideoData().videoWidthInPixels) return this._configManager.getSettingsData().getVideoData().videoWidthValue;
7111
- }
6929
+ if (this._initialized) return this._stageController.getContainerWidth();
6930
+ if (this._configManager.getSettingsData().getVideoData().videoWidthInPixels) return this._configManager.getSettingsData().getVideoData().videoWidthValue;
7112
6931
  return 0;
7113
6932
  }
7114
- /**
7115
- * Returns current streamer height in pixels.
7116
- *
7117
- * @returns Current height in pixels
7118
- */
7119
6933
  getHeight() {
7120
- if (this._initialized) return this._stageController.getContainerHeight();else {
7121
- if (this._configManager.getSettingsData().getVideoData().videoHeightInPixels) return this._configManager.getSettingsData().getVideoData().videoHeightValue;
7122
- }
6934
+ if (this._initialized) return this._stageController.getContainerHeight();
6935
+ if (this._configManager.getSettingsData().getVideoData().videoHeightInPixels) return this._configManager.getSettingsData().getVideoData().videoHeightValue;
7123
6936
  return 0;
7124
6937
  }
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
6938
  setScalingMode(newMode) {
7131
- if (this._stageController) {
7132
- this._stageController.setScalingMode(newMode);
7133
- } else {
7134
- this._configManager.getSettingsData().getVideoData().scalingMode = newMode;
7135
- }
6939
+ if (this._stageController) this._stageController.setScalingMode(newMode);else this._configManager.getSettingsData().getVideoData().scalingMode = newMode;
7136
6940
  }
7137
- /**
7138
- * Returns the current streamer scaling mode.
7139
- *
7140
- * @returns Current scaling mode
7141
- */
7142
6941
  getScalingMode() {
7143
- if (this._stageController) {
7144
- return this._stageController.getScalingMode();
7145
- } else {
7146
- return this._configManager.getSettingsData().getVideoData().scalingMode;
7147
- }
6942
+ if (this._stageController) return this._stageController.getScalingMode();
6943
+ return this._configManager.getSettingsData().getVideoData().scalingMode;
7148
6944
  }
7149
- /**
7150
- * Forces the streamer to recalculate its size based on parent internal dimensions.
7151
- */
7152
6945
  updateToSize() {
7153
- if (this._initialized) {
7154
- this._stageController.handleResize();
7155
- }
6946
+ if (this._initialized) this._stageController.handleResize();
7156
6947
  }
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
6948
  makeScreenshot() {
7164
6949
  let canvas = document.createElement('canvas');
7165
6950
  let context = canvas.getContext('2d');
@@ -7170,64 +6955,24 @@
7170
6955
  let element = this._stageController.getScreenElement().getVideoElement();
7171
6956
  if (context) {
7172
6957
  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
- }
6958
+ canvas.toBlob(blob => resolve(blob), 'image/png');
6959
+ } else resolve(null);
6960
+ } else resolve(null);
7182
6961
  });
7183
6962
  }
7184
- //------------------------------------------------------------------------//
7185
6963
  // 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
6964
  createFPSGraph(container) {
7196
6965
  return new FPSGraph(this, container);
7197
6966
  }
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
6967
  createBitrateGraph(container) {
7207
6968
  return new BitrateGraph(this, container);
7208
6969
  }
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
6970
  createMicrophoneGraph(container) {
7218
6971
  return new MicrophoneGraph(this, container);
7219
6972
  }
7220
- /**
7221
- * Adds new graph to the internal collection of active graphs.
7222
- *
7223
- * @param newGraph - Graph instance to add
7224
- */
7225
6973
  addGraph(newGraph) {
7226
6974
  if (this._graphs != null) this._graphs.push(newGraph);
7227
6975
  }
7228
- /**
7229
- * Stops all active performance graphs.
7230
- */
7231
6976
  stopAllGraphs() {
7232
6977
  if (this._graphs != null && this._graphs.length > 0) {
7233
6978
  for (let i = 0; i < this._graphs.length; i++) {
@@ -7235,198 +6980,136 @@
7235
6980
  }
7236
6981
  }
7237
6982
  }
7238
- //------------------------------------------------------------------------//
7239
6983
  // FULLSCREEN
7240
- //------------------------------------------------------------------------//
7241
- /**
7242
- * Enters fullscreen mode for the streamer container.
7243
- */
7244
6984
  enterFullScreen() {
7245
6985
  if (this._initialized && this._stageController) this._stageController.enterFullScreen();
7246
6986
  }
7247
- /**
7248
- * Exits fullscreen mode.
7249
- */
7250
6987
  exitFullScreen() {
7251
6988
  if (this._initialized && this._stageController) this._stageController.exitFullScreen();
7252
6989
  }
7253
- /**
7254
- * Returns true if the streamer is currently in fullscreen mode.
7255
- *
7256
- * @returns Boolean indicating fullscreen status
7257
- */
7258
6990
  isFullScreenMode() {
7259
6991
  if (this._initialized && this._stageController) return this._stageController.isFullScreenMode();
7260
6992
  return false;
7261
6993
  }
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
- */
6994
+ // GETTERS
7270
6995
  getStreamKey() {
7271
6996
  var _a, _b, _c;
7272
6997
  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
6998
  }
7274
- /**
7275
- * Returns the Stats Controller instance which contains statistical data about streaming performance.
7276
- *
7277
- * @returns Stats controller instance or null
7278
- */
7279
6999
  getStatsController() {
7280
7000
  return this._statsController;
7281
7001
  }
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
7002
  getStreamerID() {
7288
7003
  return this._streamerID;
7289
7004
  }
7290
- /**
7291
- * Returns the logger instance used by this streamer.
7292
- *
7293
- * @returns Logger instance
7294
- */
7295
7005
  getLogger() {
7296
7006
  return this._logger;
7297
7007
  }
7298
- /**
7299
- * Returns the configuration manager for this streamer.
7300
- *
7301
- * @returns Config manager instance or null
7302
- */
7303
7008
  getConfigManager() {
7304
7009
  return this._configManager;
7305
7010
  }
7306
- /**
7307
- * Returns the network controller which manages all server communication.
7308
- *
7309
- * @returns Network controller instance or null
7310
- */
7311
7011
  getNetworkController() {
7312
7012
  return this._networkController;
7313
7013
  }
7314
- /**
7315
- * Returns the streamer controller which manages media stream operations.
7316
- *
7317
- * @returns Streamer controller instance or null
7318
- */
7319
7014
  getStreamerController() {
7320
7015
  return this._streamerController;
7321
7016
  }
7322
- /**
7323
- * Returns the stage controller which manages visual presentation.
7324
- *
7325
- * @returns Stage controller instance or null
7326
- */
7327
7017
  getStageController() {
7328
7018
  return this._stageController;
7329
7019
  }
7330
- /**
7331
- * Returns the storage manager which handles persistent data storage.
7332
- *
7333
- * @returns Storage manager instance or null
7334
- */
7335
7020
  getStorageManager() {
7336
7021
  return this._storageManager;
7337
7022
  }
7338
- /**
7339
- * Returns the HTML video element used by this streamer instance.
7340
- *
7341
- * @returns Video element or null
7342
- */
7343
7023
  getVideoElement() {
7344
7024
  var _a, _b, _c;
7345
7025
  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
7026
  }
7347
- /**
7348
- * Returns true if this streamer instance has already been initialized.
7349
- *
7350
- * @returns Boolean indicating initialization status
7351
- */
7352
7027
  isInitialized() {
7353
7028
  return this._initialized;
7354
7029
  }
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
- */
7030
+ isDestroyed() {
7031
+ return this._isDestroyed;
7032
+ }
7360
7033
  getVersion() {
7361
7034
  return this.STREAMER_VERSION;
7362
7035
  }
7363
- /**
7364
- * Returns the development branch of this streamer (e.g., main, experimental).
7365
- *
7366
- * @returns Branch name string
7367
- */
7368
7036
  getBranch() {
7369
7037
  return this.STREAMER_BRANCH;
7370
7038
  }
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
- */
7039
+ // EVENTS
7380
7040
  dispatchEvent(eventName, event) {
7381
7041
  super.dispatchEvent(eventName, event);
7382
7042
  }
7383
- //------------------------------------------------------------------------//
7384
- // CLEAN UP
7385
- //------------------------------------------------------------------------//
7386
- /**
7387
- * Starts the streaming process.
7388
- *
7389
- * @returns Promise that resolves when streaming has started
7390
- */
7043
+ // START / STOP
7391
7044
  start() {
7392
7045
  var _a;
7393
7046
  return __awaiter(this, void 0, void 0, function* () {
7394
7047
  return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.start();
7395
7048
  });
7396
7049
  }
7397
- /**
7398
- * Stops the streaming process.
7399
- */
7400
7050
  stop() {
7401
7051
  var _a;
7402
- return (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.stop();
7052
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.stop();
7053
+ }
7054
+ // DEBUG
7055
+ debugMediaState() {
7056
+ var _a;
7057
+ (_a = this._streamerController) === null || _a === void 0 ? void 0 : _a.debugMediaState();
7403
7058
  }
7404
- //------------------------------------------------------------------------//
7405
- // CLEAN UP
7406
- //------------------------------------------------------------------------//
7407
7059
  /**
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.
7060
+ * Destroys this instance of StormStreamer, releasing all resources.
7061
+ * This method is SYNCHRONOUS - it sets flags immediately to prevent race conditions.
7062
+ * All pending async operations will detect the destroyed state and abort.
7410
7063
  */
7411
7064
  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
7065
+ var _a, _b, _c, _d, _e, _f, _g;
7066
+ // Prevent double destroy
7067
+ if (this._isDestroyed) {
7068
+ (_a = this._logger) === null || _a === void 0 ? void 0 : _a.warning(this, "🔴 [DESTROY] Already destroyed, skipping");
7069
+ return;
7070
+ }
7071
+ (_b = this._logger) === null || _b === void 0 ? void 0 : _b.warning(this, "🔴 [DESTROY] Starting streamer instance destruction (sync)...");
7072
+ // CRITICAL: Set flag IMMEDIATELY
7073
+ this._isDestroyed = true;
7416
7074
  this._initialized = false;
7417
7075
  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
7076
+ // Remove from global array
7077
+ if (this.DEV_MODE && 'StormStreamerArray' in window) {
7078
+ window.StormStreamerArray[this._streamerID] = null;
7079
+ }
7080
+ // Stop all graphs
7081
+ this.stopAllGraphs();
7082
+ this._graphs = [];
7083
+ // Destroy network controller
7084
+ if (this._networkController) {
7085
+ (_c = this._logger) === null || _c === void 0 ? void 0 : _c.info(this, "🔴 [DESTROY] Destroying NetworkController...");
7086
+ try {
7087
+ (_d = this._networkController.getConnection()) === null || _d === void 0 ? void 0 : _d.destroy();
7088
+ } catch (e) {/* ignore */}
7089
+ this._networkController = null;
7090
+ }
7091
+ // Destroy streamer controller (this is also sync now)
7092
+ if (this._streamerController) {
7093
+ (_e = this._logger) === null || _e === void 0 ? void 0 : _e.info(this, "🔴 [DESTROY] Destroying StreamerController...");
7094
+ this._streamerController.destroy();
7095
+ this._streamerController = null;
7096
+ }
7097
+ // Destroy stage controller
7098
+ if (this._stageController) {
7099
+ (_f = this._logger) === null || _f === void 0 ? void 0 : _f.info(this, "🔴 [DESTROY] Destroying StageController...");
7100
+ try {
7101
+ this._stageController.destroy();
7102
+ } catch (e) {/* ignore */}
7103
+ this._stageController = null;
7104
+ }
7105
+ // Clear other references
7106
+ this._storageManager = null;
7107
+ this._statsController = null;
7108
+ // Remove all event listeners
7423
7109
  this.removeAllEventListeners();
7110
+ (_g = this._logger) === null || _g === void 0 ? void 0 : _g.success(this, "🔴 [DESTROY] Streamer instance destroyed successfully (sync)");
7424
7111
  }
7425
7112
  }
7426
- /**
7427
- * Next ID for the streamer instance. Each subsequent instance has a higher number.
7428
- * @private
7429
- */
7430
7113
  StormStreamer.NEXT_STREAMER_ID = 0;
7431
7114
 
7432
7115
  function create(config) {