PluginProbe
WpStream – Live Streaming, Video on Demand, Pay Per View / 4.11.2
WpStream – Live Streaming, Video on Demand, Pay Per View v4.11.2
4.14.1 4.14.0 4.13.2 4.13.1 4.13 4.12.5 4.12.4 4.12.3 4.12.2 4.12.1 4.12 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5 4.5.1 4.5.11 4.5.11.1 4.5.11.2 4.5.11.4 4.5.11.5 4.5.11.6 All 181 releases
wpstream / public / js / broadcaster.js

broadcaster.js in WpStream – Live Streaming, Video on Demand, Pay Per View 4.11.2, at public/js/broadcaster.js

956 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WpStream Broadcaster
3 * @typedef {Object} WpStreamBroadcasterVars
4 * @property {string} whip_url - The WHIP URL for streaming
5 * @property {string} channel_id - The channel ID
6 * @property {string} ajax_url - The AJAX URL for requests
7 * @property {string} no_video_audio_access - Error message for no video/audio access
8 * @property {string} no_audio_access - Error message for no audio access
9 * @property {string} no_video_access - Error message for no video access
10 * @property {string} channel_off - Message when channel is off
11 */
12 /* global wpstream_broadcaster_vars */
13 document.addEventListener("DOMContentLoaded", function () {
14 // Global variables
15 let allDevices = null;
16 let input = null;
17 let streamingStarted = false;
18 let frameCalculatorTimer = null;
19 let totalVideoFrames = 0;
20 let whipUrl = null;
21 let videoEnabled = true;
22 let audioEnabled = true;
23 let localStream = null;
24 let considerReconnect = false; // set true after a successful start to allow auto-reconnects
25 let pendingReconnect = false; // true while waiting to reconnect
26 let pendingReconnectTimeout = null; // timeout handle for scheduled reconnect
27 const reconnectDelayMs = 10000; // 10s delay before attempting to reconnect
28 const BROADCASTER_SESSION_COOKIE = 'broadcasterSessionId';
29
30 function setSessionCookie(name, value) {
31 document.cookie = name + '=' + encodeURIComponent(value) + '; path=/';
32 }
33
34 function getCookie(name) {
35 const value = `; ${document.cookie}`;
36 const parts = value.split(`; ${name}=`);
37 if (parts.length === 2) {
38 return decodeURIComponent(parts.pop().split(';').shift());
39 }
40 return '';
41 }
42
43 function getPopupPayloadFromWindowName() {
44 let payload = {};
45 try {
46 if (window.name) {
47 const parsed = JSON.parse(window.name);
48 if (parsed && parsed.wpstream_onboarding_popup_payload) {
49 payload = parsed.wpstream_onboarding_popup_payload;
50 if (payload.session_id) {
51 setSessionCookie(BROADCASTER_SESSION_COOKIE, payload.session_id);
52 }
53 }
54 }
55 } catch (e) {
56 payload = {};
57 }
58
59 if (!payload.session_id) {
60 const cookieSessionId = getCookie(BROADCASTER_SESSION_COOKIE);
61 if (cookieSessionId) {
62 payload = {
63 session_id: cookieSessionId,
64 };
65 }
66 }
67
68 window.name = '';
69 return payload;
70 }
71
72 const popupPayload = getPopupPayloadFromWindowName();
73 const popupSessionId = popupPayload && popupPayload.session_id ? popupPayload.session_id : '';
74 console.log('Popup session ID:', popupSessionId);
75
76 function trackOnboardingStepSafe(action, step, elementType = '', elementName = '', trackingSessionId = '') {
77 if (typeof wpstream_track_onboarding_step === 'function') {
78 wpstream_track_onboarding_step(action, step, elementType, elementName, trackingSessionId);
79 }
80 }
81
82 // Get WHIP URL from config
83 if (wpstream_broadcaster_vars && wpstream_broadcaster_vars.whip_url) {
84 whipUrl = wpstream_broadcaster_vars.whip_url;
85 }
86
87 // DOM elements
88 const videoElement = document.getElementById("localVideo");
89 const streamingButton = document.getElementById("startBroadcast");
90 const stopButton = document.getElementById("stopBroadcast");
91 const videoSourceSelect = document.getElementById("videoDevice");
92 const videoToggle = document.getElementById("videoToggle");
93 const videoResolutionSelect = document.getElementById("videoQuality");
94 const audioSourceSelect = document.getElementById("audioDevice");
95 const audioToggle = document.getElementById("audioToggle");
96 const messageContainer = document.getElementById("messageContainer");
97 const statusIndicator = document.getElementById("statusIndicator");
98 const statusText = document.getElementById("statusText");
99 const liveIndicatorLive = document.getElementById("videoLiveIndicatorLive");
100 const liveIndicatorError = document.getElementById("videoLiveIndicatorError");
101 const loadSpinner = document.getElementById("wpstream-pre-load-spinner");
102
103 // Resolution mappings from demo
104 const userResolutions = {
105 vga: {
106 width: { ideal: 640 },
107 height: { ideal: 360 },
108 },
109 hd: {
110 width: { exact: 1280 },
111 height: { exact: 720 },
112 },
113 fhd: {
114 width: { exact: 1920 },
115 height: { exact: 1080 },
116 },
117 square: {
118 width: { exact: 800 },
119 height: { exact: 600 },
120 },
121 default: {
122 width: { min: 640, ideal: 1280, max: 1920 },
123 height: { min: 360, ideal: 720, max: 1080 },
124 },
125 };
126
127 const displayResolutions = {
128 vga: { width: 640, height: 360 },
129 hd: { width: 1280, height: 720 },
130 fhd: { width: 1920, height: 1080 },
131 square: { width: 800, height: 600 },
132 default: { width: 1280, height: 720 },
133 };
134
135 function getResolutionAndCalculateFrame(videoElement) {
136 if (frameCalculatorTimer) {
137 clearInterval(frameCalculatorTimer);
138 frameCalculatorTimer = null;
139 totalVideoFrames = 0;
140 }
141
142 frameCalculatorTimer = setInterval(function () {
143 console.log(
144 "Resolution: " +
145 videoElement.videoWidth +
146 "x" +
147 videoElement.videoHeight
148 );
149
150 if (totalVideoFrames === 0) {
151 totalVideoFrames =
152 videoElement.getVideoPlaybackQuality().totalVideoFrames;
153 } else {
154 let currentTotalFrame =
155 videoElement.getVideoPlaybackQuality().totalVideoFrames;
156 let frameRate = currentTotalFrame - totalVideoFrames;
157 // console.log('Frame rate: ' + frameRate + 'fps');
158 totalVideoFrames = currentTotalFrame;
159 }
160 }, 1000);
161 }
162
163 function getUserConstraints() {
164 let videoDeviceId = videoSourceSelect.value;
165 let videoResolution = videoResolutionSelect.value;
166 let audioDeviceId = audioSourceSelect.value;
167
168 let newConstraint = {};
169
170 if (videoDeviceId) {
171 newConstraint.video = {
172 deviceId: {
173 exact: videoDeviceId,
174 },
175 };
176 }
177
178 if (audioDeviceId) {
179 newConstraint.audio = {
180 deviceId: {
181 exact: audioDeviceId,
182 },
183 };
184 }
185
186 if (videoResolution && userResolutions[videoResolution]) {
187 const resolution = userResolutions[videoResolution];
188
189 if (!newConstraint.video) {
190 newConstraint.video = {};
191 }
192
193 newConstraint.video.width = resolution.width;
194 newConstraint.video.height = resolution.height;
195 }
196
197 return newConstraint;
198 }
199
200 function getDisplayConstraints() {
201 let videoResolution = videoResolutionSelect.value;
202
203 let newConstraint = {};
204 newConstraint.video = {};
205
206 if (videoResolution && displayResolutions[videoResolution]) {
207 const resolution = displayResolutions[videoResolution];
208 newConstraint.video.width = resolution.width;
209 newConstraint.video.height = resolution.height;
210 } else {
211 newConstraint.video = true;
212 }
213
214 newConstraint.audio = true;
215 return newConstraint;
216 }
217
218 function setDevice(type, select, devices) {
219 select.innerHTML = "";
220
221 if (type === "audio" && devices.length === 0) {
222 const option = document.createElement("option");
223 option.value = "";
224 option.textContent = "No Source Available";
225 select.appendChild(option);
226 } else {
227 devices.forEach(function (device) {
228 const option = document.createElement("option");
229 option.textContent =
230 device.label || `${type} ${select.options.length + 1}`;
231 option.value = device.deviceId;
232 select.appendChild(option);
233 });
234 }
235
236 if (select.options.length > 0) {
237 select.selectedIndex = 0;
238 }
239 }
240
241 function resetMessages() {
242 if (messageContainer) {
243 messageContainer.innerHTML = "";
244 }
245
246 clearInterval(frameCalculatorTimer);
247 frameCalculatorTimer = null;
248 }
249
250 function showMessage(message, type = "info") {
251 if (!messageContainer) return;
252
253 const messageElement = document.createElement("div");
254 messageElement.className = type + "-message";
255 messageElement.textContent = message;
256
257 const dismissButton = document.createElement("button");
258 dismissButton.className = 'dismiss-message';
259 dismissButton.innerHTML = '×';
260 dismissButton.addEventListener('click', function() {
261 messageElement.remove();
262 });
263
264 messageElement.appendChild(dismissButton);
265
266 messageContainer.innerHTML = "";
267 messageContainer.appendChild(messageElement);
268
269 if (type === "success" || type === "info") {
270 setTimeout(() => {
271 messageElement.remove();
272 }, 5000);
273 }
274 }
275
276 function updateStatus(status) {
277 if (!statusIndicator || !statusText) return;
278
279 statusIndicator.classList.remove(
280 "connected",
281 "disconnected",
282 "connecting",
283 "reconnecting"
284 );
285
286 switch (status) {
287 case "connected":
288 statusIndicator.classList.add("connected");
289 statusText.textContent = "Connected - Broadcasting Live";
290 liveIndicatorLive.style.display = 'inline';
291 liveIndicatorError.style.display = 'none';
292 break;
293 case "connecting":
294 statusIndicator.classList.add("connecting");
295 statusText.textContent = "Connecting...";
296 liveIndicatorError.style.display = 'inline';
297 liveIndicatorError.innerText = 'Connecting...';
298 break;
299 case "reconnecting":
300 statusIndicator.classList.add("connecting");
301 statusText.textContent = "Reconnecting";
302 liveIndicatorError.style.display = 'inline';
303 liveIndicatorLive.style.display = 'none';
304 liveIndicatorError.innerText = 'Connection lost. Reconnecting in 10 seconds...';
305 break;
306 case "disconnected":
307 default:
308 statusIndicator.classList.add("disconnected");
309 statusText.textContent = "Not Broadcasting";
310 liveIndicatorLive.style.display = 'none';
311 liveIndicatorError.style.display = 'none';
312 break;
313 }
314 }
315
316 function createInput( shouldAutoStart = false, keepMessages = false ) {
317 if (streamingButton) {
318 streamingButton.disabled = true;
319 }
320
321 // Show the loading spinner
322 if (loadSpinner) {
323 loadSpinner.style.display = "block";
324 }
325
326 if (input) {
327 input.remove();
328 input = null;
329 }
330
331 if ( !keepMessages ) {
332 resetMessages();
333 }
334
335 if (localStream) {
336 localStream.getTracks().forEach((track) => track.stop());
337 localStream = null;
338 }
339
340 input = OvenLiveKit.create({
341 callbacks: {
342 error: function (error) {
343 let errorMessage = '';
344
345 if (error.message) {
346 errorMessage = error.message;
347 } else if (error.name) {
348 errorMessage = error.name;
349 } else {
350 errorMessage = error.toString();
351 }
352
353 if (error.name === "OverconstrainedError") {
354 showMessage(
355 "Your browser or camera does not support this frame size: " + videoResolutionSelect.value,
356 'error'
357 );
358 videoResolutionSelect.value = 'default';
359 createInput(shouldAutoStart, true);
360 return;
361 }
362
363 resetMessages();
364 showMessage(errorMessage, "error");
365
366 if (shouldAutoStart) {
367 considerReconnect = false;
368 }
369 },
370 connectionClosed: function (type, event) {
371 console.log("Connection closed:", type, event);
372 streamingStarted = false;
373 // updateStatus("disconnected");
374
375 // if (streamingButton) {
376 // streamingButton.classList.remove("hidden");
377 // streamingButton.disabled = false;
378 // }
379 // if (stopButton) {
380 // stopButton.classList.add("hidden");
381 // }
382
383 if (considerReconnect && !pendingReconnect) {
384 console.log('connection closed, attempting to reconnect');
385 attemptReconnect();
386 liveIndicatorLive.style.display = 'none';
387 liveIndicatorError.style.display = 'inline';
388 liveIndicatorError.innerContent = 'Reconnecting';
389 } else {
390 console.log('connection closed, not reconnecting');
391 trackOnboardingStepSafe('broadcaster_streaming_stopped', 'wpstream_broadcaster', 'button', 'streaming_stopped', popupSessionId);
392 // updateInputState(false);
393 }
394 if (loadSpinner) {
395 loadSpinner.style.display = "none";
396 }
397 },
398 iceStateChange: function (state) {
399 console.log("ICE state changed:", state);
400 if ( state === 'connected' ) {
401 // showMessage("Broadcast started successfully");
402 updateStatus('connected');
403 trackOnboardingStepSafe('broadcaster_streaming_started', 'wpstream_broadcaster', 'button', 'streaming_started', popupSessionId);
404 }
405
406 if (state === "disconnected" && considerReconnect) {
407 streamingStarted = false;
408 if (considerReconnect && !pendingReconnect) {
409 console.log(
410 "connection closed, attempting to reconnect from ice state change"
411 );
412 updateStatus("reconnecting");
413 attemptReconnect();
414 } else {
415 showMessage(
416 "Connection failed. Please check your network settings.",
417 "error"
418 );
419 }
420 }
421 },
422 },
423 });
424
425 input.attachMedia(videoElement);
426
427 if (videoSourceSelect.options.length > 0) {
428 if (videoSourceSelect.value === "displayCapture") {
429 input
430 .getDisplayMedia(getDisplayConstraints())
431 .then(function (stream) {
432 localStream = stream;
433 if (streamingButton) {
434 streamingButton.disabled = false;
435 }
436 if (loadSpinner) {
437 loadSpinner.style.display = "none";
438 }
439
440 if ( shouldAutoStart && considerReconnect ) {
441 startStreaming(true);
442 }
443 })
444 .catch(function (error) {
445 console.error('Failed to get display media:', error);
446 if ( shouldAutoStart ) {
447 showMessage('Failed to access screen sharing.', 'error');
448 considerReconnect = false;
449 updateInputState(false);
450 }
451 if (loadSpinner) {
452 loadSpinner.style.display = "none";
453 }
454 });
455 } else {
456 input
457 .getUserMedia(getUserConstraints())
458 .then(function (stream) {
459 localStream = stream;
460 if (streamingButton) {
461 streamingButton.disabled = false;
462 }
463
464 if ( shouldAutoStart && considerReconnect ) {
465 startStreaming(true);
466 }
467 if (loadSpinner) {
468 loadSpinner.style.display = "none";
469 }
470 })
471 .catch(function (error) {
472 console.error('Failed to get user media:', error);
473 if ( shouldAutoStart ) {
474 showMessage('Failed to access camera/microphone.', 'error');
475 considerReconnect = false;
476 updateInputState(false);
477 }
478 if (loadSpinner) {
479 loadSpinner.style.display = "none";
480 }
481 });
482 }
483 }
484 }
485
486 function startStreaming( isReconnect = false) {
487 // make an ajax call to check if the channel is active and if the user has quota
488 const channelCheckPromise = checkChannelStatus(wpstream_broadcaster_vars.channel_id);
489 const channelUserQuotaPromise = checkUserQuota();
490
491 Promise.all([channelCheckPromise, channelUserQuotaPromise])
492 .then(function (results) {
493 const channelActive = results[0];
494 const userQuotaValid = results[1];
495
496 if (!channelActive) {
497 resetStreamingUI();
498 considerReconnect = false;
499 return; // Channel is not active, do not proceed with streaming
500 }
501
502 if (!userQuotaValid) {
503 resetStreamingUI();
504 considerReconnect = false;
505 return; // User quota is not valid, do not proceed with streaming
506 }
507
508 proceedWithStreaming(isReconnect);
509 });
510 }
511
512 function updateInputState(state) {
513 videoSourceSelect.disabled = state;
514 audioSourceSelect.disabled = state;
515 videoResolutionSelect.disabled = state;
516 streamingButton.disabled = state;
517 }
518
519 function stopStreaming() {
520 streamingStarted = false;
521 considerReconnect = false; // user-initiated stop should cancel auto-reconnect
522 if (pendingReconnect && pendingReconnectTimeout) {
523 clearTimeout(pendingReconnectTimeout);
524 pendingReconnect = false;
525 pendingReconnectTimeout = null;
526 }
527 updateStatus("disconnected");
528
529 if (streamingButton) {
530 streamingButton.classList.remove("hidden");
531 }
532 if (stopButton) {
533 stopButton.classList.add("hidden");
534 }
535
536 if (input) {
537 input.stopStreaming();
538 createInput();
539 }
540
541 // showMessage("Broadcasting stopped", "info");
542 updateInputState(false);
543 }
544
545 function attemptReconnect() {
546 console.log("attemptReconnect()");
547 updateStatus('reconnecting');
548
549 if ( pendingReconnect ) {
550 console.log('reconnect already in progress');
551 return;
552 }
553
554 // Clean up existing connection before reconnecting
555 if (input) {
556 if (input.peerConnection || input.webSocket) {
557 // Force cleanup without triggering callbacks
558 if (input.peerConnection) {
559 input.peerConnection.close();
560 input.peerConnection = null;
561 }
562 if (input.webSocket) {
563 input.webSocket.close();
564 input.webSocket = null;
565 }
566 // Reset streaming mode
567 input.streamingMode = null;
568 }
569 }
570
571 pendingReconnect = true;
572
573 // Show a reconnecting state and allow user to cancel via Stop button
574 // showMessage("Disconnected. Reconnecting in 5 seconds...", "info");
575 // updateInputState(false);
576
577 pendingReconnect = true;
578 pendingReconnectTimeout = setTimeout(function () {
579 pendingReconnect = false;
580 pendingReconnectTimeout = null;
581 if (considerReconnect) {
582 checkChannelStatus(wpstream_broadcaster_vars.channel_id)
583 .then(function(channelActive) {
584 if (channelActive && considerReconnect) {
585 console.log("Channel is active, proceeding with reconnect...");
586 updateStatus("connecting");
587 // input.stopStreaming();
588 setTimeout(function () {
589 createInput(true);
590 }, 5000);
591 } else {
592 console.log("Channel is not active, cannot reconnect.");
593 considerReconnect = false;
594 showMessage('Channel is no longer active. Broadcasting stopped');
595 resetStreamingUI();
596 }
597 })
598 .catch(function (error) {
599 console.error('Error checking channel status');
600 if (considerReconnect) {
601 console.error('Error during reconnect attempt:', error);
602 attemptReconnect();
603 }
604 });
605
606 // console.log('Reconnecting...');
607 // createInput( true );
608 // startStreaming();
609 }
610 }, reconnectDelayMs);
611 }
612
613 function toggleVideo(enabled) {
614 let stream = localStream;
615
616 if ( enabled ) {
617 document.getElementById('video-off').style.display = 'inline';
618 document.getElementById('video-on').style.display = 'none';
619 } else {
620 document.getElementById('video-on').style.display = 'inline';
621 document.getElementById('video-off').style.display = 'none';
622 }
623
624 if (!stream && videoElement && videoElement.srcObject) {
625 stream = videoElement.srcObject;
626 }
627
628 if (stream) {
629 const videoTracks = stream.getVideoTracks();
630
631 videoTracks.forEach((track) => {
632 track.enabled = enabled;
633 });
634
635 // showMessage(enabled ? "Video enabled" : "Video disabled", "info");
636 }
637 }
638
639 function toggleAudio(enabled) {
640 let stream = localStream;
641
642 if ( enabled ) {
643 document.getElementById('audio-off').style.display = 'inline';
644 document.getElementById('audio-on').style.display = 'none';
645 } else {
646 document.getElementById('audio-on').style.display = 'inline';
647 document.getElementById('audio-off').style.display = 'none';
648 }
649
650 if (!stream && videoElement && videoElement.srcObject) {
651 stream = videoElement.srcObject;
652 }
653
654 if (stream) {
655 const audioTracks = stream.getAudioTracks();
656
657 audioTracks.forEach((track) => {
658 track.enabled = enabled;
659 });
660
661 // showMessage(enabled ? "Audio enabled" : "Audio disabled", "info");
662 }
663 }
664
665 // Event listeners
666 if (streamingButton) {
667 streamingButton.addEventListener("click", function () {
668 if (!streamingStarted) {
669 updateStatus('connecting');
670 streamingButton.classList.add("hidden");
671 stopButton.classList.remove("hidden");
672 startStreaming();
673 }
674 });
675 }
676
677 if (stopButton) {
678 stopButton.addEventListener("click", function () {
679 // If a reconnect is pending, treat this as "Cancel Broadcast"
680 if (pendingReconnect) {
681 if (pendingReconnectTimeout) {
682 clearTimeout(pendingReconnectTimeout);
683 pendingReconnectTimeout = null;
684 }
685 pendingReconnect = false;
686 considerReconnect = false;
687 updateStatus("disconnected");
688 // showMessage("Broadcast stopped", "info");
689 updateInputState(false);
690 if (stopButton) {
691 stopButton.classList.add("hidden");
692 }
693 if (streamingButton) {
694 streamingButton.classList.remove("hidden");
695 streamingButton.disabled = false;
696 }
697 return;
698 }
699
700 if (streamingStarted) {
701 stopStreaming();
702 }
703 });
704 }
705
706 // Device change listeners
707 if (videoSourceSelect) {
708 videoSourceSelect.addEventListener("change", function () {
709 if (input) {
710 createInput();
711 }
712 });
713 }
714
715 if (videoResolutionSelect) {
716 videoResolutionSelect.addEventListener("change", function () {
717 if (input) {
718 createInput();
719 }
720 });
721 }
722
723 if (audioSourceSelect) {
724 audioSourceSelect.addEventListener("change", function () {
725 if (input) {
726 createInput();
727 }
728 });
729 }
730
731 if (videoToggle) {
732 videoToggle.addEventListener("click", function () {
733 videoEnabled = !videoEnabled;
734 toggleVideo(videoEnabled);
735 });
736 }
737
738 if (audioToggle) {
739 audioToggle.addEventListener("click", function () {
740 audioEnabled = !audioEnabled;
741 toggleAudio(audioEnabled);
742 });
743 }
744
745 // Mobile video expand toggle
746 const expandToggle = document.getElementById("videoExpandToggle");
747 if (expandToggle) {
748 expandToggle.addEventListener("click", function () {
749 const container = document.querySelector(".video-container");
750 if (container) {
751 container.classList.toggle("expanded");
752 // Sync accessibility attributes with visual state
753 const isExpanded = container.classList.contains("expanded");
754 expandToggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
755 // Ensure the toggle references the controlled element
756 if (!container.id) {
757 container.id = "videoContainer";
758 }
759 expandToggle.setAttribute("aria-controls", container.id);
760 }
761 });
762 }
763
764 function init() {
765 if (allDevices) {
766 setDevice("video", videoSourceSelect, allDevices.videoinput);
767 setDevice("audio", audioSourceSelect, allDevices.audioinput);
768 }
769
770 createInput();
771 }
772
773 // Initialize - get all devices first
774 OvenLiveKit.getDevices()
775 .then(function (devices) {
776 allDevices = devices;
777 init();
778 })
779 .catch(function (error) {
780 let errorMessage = "";
781
782 if (error.message) {
783 console.log(error.message);
784 changeErrorMessage(error.message);
785 } else if (error.name) {
786 errorMessage = error.name;
787 showMessage(errorMessage, "error");
788 } else {
789 errorMessage = error.toString();
790 showMessage(errorMessage, "error");
791 }
792 });
793
794 function changeErrorMessage(message) {
795 console.log(wpstream_broadcaster_vars);
796 switch (message) {
797 case "No input devices were found.":
798 showMessage(
799 wpstream_broadcaster_vars.no_video_audio_access,
800 "error"
801 );
802 break;
803 case "Can not find Audio devices":
804 showMessage(wpstream_broadcaster_vars.no_audio_access, "error");
805 break;
806 case "Can not find Video devices":
807 showMessage(wpstream_broadcaster_vars.no_video_access, "error");
808 }
809 }
810
811 function checkChannelStatus(channelId) {
812 return new Promise((resolve, reject) => {
813 if (!wpstream_broadcaster_vars.ajax_url) {
814 resolve(true);
815 return;
816 }
817
818 var nonce = jQuery('#wpstream_start_event_nonce').val();
819
820 jQuery.ajax({
821 url: wpstream_broadcaster_vars.ajax_url,
822 type: 'POST',
823 data: {
824 action: 'wpstream_check_event_status',
825 channel_id: channelId,
826 nonce: nonce
827 },
828 success: function(response) {
829 try {
830 const parsedResponse = JSON.parse(response);
831 if (parsedResponse.status === 'active') {
832 messageContainer.innerHTML = "";
833 resolve(true);
834 } else {
835 showMessage(wpstream_broadcaster_vars.channel_off, 'error');
836 resolve(false);
837 }
838 } catch (e) {
839 console.error('Error parsing response:', e);
840 showMessage('Error checking channel status', 'error');
841 reject(false);
842 }
843 },
844 error: function(xhr, status, error) {
845 console.error('Error checking channel status:', error);
846 resetStreamingUI();
847 showMessage('Error checking channel status: ' + error, 'error');
848 reject(false);
849 }
850 })
851 })
852 }
853
854 function checkUserQuota() {
855 return new Promise((resolve, reject) => {
856 if (!wpstream_broadcaster_vars.ajax_url) {
857 resolve(true);
858 return;
859 }
860
861 jQuery.ajax({
862 url: wpstream_broadcaster_vars.ajax_url,
863 type: 'POST',
864 data: {
865 action: 'wpstream_check_user_quota',
866 },
867 success: function(response) {
868 try {
869 const parsedResponse = JSON.parse(response);
870 if (parsedResponse.available_data_mb > 0) {
871 messageContainer.innerHTML = '';
872 resolve(true);
873 } else {
874 const messageElement = document.createElement("div");
875 messageElement.className = "error-message";
876 messageElement.innerHTML = wpstream_broadcaster_vars.not_enough_traffic;
877 messageContainer.innerHTML = "";
878 messageContainer.appendChild(messageElement);
879 resolve(false);
880 }
881 } catch (e) {
882 console.error('Error parsing response:', e);
883 showMessage('Error checking quota', 'error');
884 reject(false);
885 }
886 },
887 error: function(xhr, status, error) {
888 console.error('Error checking user quota:', error);
889 showMessage('Error checking user quota: ' + error, 'error');
890 reject(false);
891 }
892 })
893 })
894 }
895
896 function proceedWithStreaming(isReconnect) {
897 streamingStarted = true;
898 // If a reconnect was pending, cancel it as we're actively starting now
899 if (pendingReconnect && pendingReconnectTimeout) {
900 clearTimeout(pendingReconnectTimeout);
901 pendingReconnect = false;
902 pendingReconnectTimeout = null;
903 }
904
905 updateStatus("connecting");
906
907 if (streamingButton) {
908 streamingButton.classList.add("hidden");
909 }
910 if (stopButton) {
911 stopButton.classList.remove("hidden");
912
913 }
914
915 if (input && whipUrl) {
916 let connectionConfig = {};
917
918 // Begin streaming; mark that we should auto-reconnect on unexpected disconnects
919 considerReconnect = true;
920 input.startStreaming(whipUrl, connectionConfig);
921 if ( input ) {
922 // TODO: check why input is sometimes null here
923 console.log('something was wrong' );
924 }
925 // updateStatus("connected");
926 if ( isReconnect ) {
927 console.log('Reconnected successfully!');
928 // showMessage("Reconnected successfully!", "success");
929 }
930 updateInputState(true);
931 } else {
932 streamingButton.classList.remove("hidden");
933 stopButton.classList.add("hidden");
934 console.log(whipUrl);
935 showMessage("Error: No WHIP URL configured", "error");
936 updateStatus("disconnected");
937
938 // Stop reconnecting if there's an error
939 if ( isReconnect ) {
940 considerReconnect = false;
941 }
942 }
943 }
944
945 function resetStreamingUI() {
946 updateStatus("disconnected");
947 if (streamingButton) {
948 streamingButton.classList.remove("hidden");
949 streamingButton.disabled = false;
950 }
951 if (stopButton) {
952 stopButton.classList.add("hidden");
953 }
954 }
955 });
956