Browse Source

Show the connection dialog on play when the stream is down

The "Off the Air" dialog used to be fired by the 20s status poll, so it threw
itself over the page unprompted whenever nobody was broadcasting -- which is
most of the time. It now opens only in response to someone actually pressing
play, or a retry that failed. The poll still swaps the off-air artwork quietly,
so the player reads as off air without a dialog in the way.

This retires REVIEW_MODE: it existed to suppress that unprompted dialog, and
there's nothing left to suppress. No temporary flags remain in the source.

The play interceptor is bound on document in the CAPTURE phase, and that
matters: binding on the button itself would not work, because at the target
listeners run in registration order regardless of the capture flag and
Amplitude registers first. Capturing on an ancestor lets stopPropagation keep
the event from reaching the button at all, so Amplitude never starts a doomed
playback attempt against a dead mount. When the stream is up the handler returns
early and Amplitude gets the click untouched.

Three bugs fixed on the way through:

  - Duplicate id="radio": the player's tower icon and the dialog's header icon
    both had it, so $('#radio') only ever matched the player and the dialog's
    icon was never being updated by the code that thought it was. The dialog's
    copy is static off-air artwork, so it just loses the id.
  - Stacked retry handlers: the click handler was bound inside showOffAir(),
    which the poll called every 20 seconds. Five minutes off air meant ~15
    handlers firing on one click. Bound once at ready now.
  - Retry never retried: it animated the bar to 100% and announced ERROR from
    the `done` callback without asking the server anything. It now re-checks
    /api/status for real -- back on air closes the dialog, still down shows
    ERROR.

Verified in a browser both ways: off air, play opens the dialog and Amplitude
stays paused, retry re-checks and reports ERROR, and one click triggers exactly
one progressbar call. With a live status, play goes straight through to
Amplitude with no dialog, and a retry that succeeds closes the dialog itself.
windhamdavid 1 day ago
parent
commit
8149c4ce05
2 changed files with 70 additions and 26 deletions
  1. 6 1
      src/index.html
  2. 64 25
      src/js/radio.js

+ 6 - 1
src/index.html

@@ -119,7 +119,12 @@
                    <div class="modal-content">
                    <div class="modal-content">
                      <div class="modal-header">
                      <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-                       <h4 class="modal-title"><img id="radio" src="img/none.svg" title="radio" width="25px" style="float:left;margin-right:15px;"> Off the Air</h4>
+                       <!-- Deliberately id-less: the player's tower icon already
+                            claims that id, and the duplicate meant jQuery only
+                            ever matched the first. This one is always the
+                            off-air artwork anyway, since the dialog only opens
+                            when we're off air. -->
+                       <h4 class="modal-title"><img src="img/none.svg" title="radio" width="25px" style="float:left;margin-right:15px;"> Off the Air</h4>
                      </div>
                      </div>
                      <div class="modal-body">
                      <div class="modal-body">
                         <div class="alert alert-danger" role="alert">
                         <div class="alert alert-danger" role="alert">

+ 64 - 25
src/js/radio.js

@@ -1,13 +1,8 @@
 /*global amplitude_config:true */
 /*global amplitude_config:true */
 
 
-// TEMPORARY (2026-07-16) — suppresses the "Off the Air" modal, which pops up
-// whenever the stream has no source (i.e. right now) and covers the page.
-//
-// This is all that's left of review mode: the terms and nickname/password
-// modals are gone for good now, not just hidden. The off-air artwork still
-// swaps in either way, so the player reads as off air regardless — this only
-// stops the dialog. Set to false to get the dialog back.
-var REVIEW_MODE = true;
+// Whether the stream currently has a source, kept fresh by radioTitle()'s poll.
+// Read when the play button is pressed — see the interceptor further down.
+var streamOnline = false;
 
 
 
 
 /* Radio Funtions */
 /* Radio Funtions */
@@ -163,24 +158,27 @@ function get_radio_eq_none() {return 'img/1.png';}
 
 
 var interval = null;
 var interval = null;
 
 
-function showOffAir() {
-  if (!REVIEW_MODE) {
-    $('#connection-error').modal('show');
-  }
-  $('#error-reconnecting').hide();
-  $('#error-reconnecting-again').hide();
-  $('#connection-error-reconnecting').attr('data-transitiongoal', 0).progressbar();
-  $('#connection-error-retry').on('click', function () {
-    $('#error-reconnecting').show();
-    $('#connection-error-reconnecting').attr('data-transitiongoal', 100).progressbar({
-        done: function() { $('#error-reconnecting-again').show(); }
-    });
-  });
+// Off-air *artwork* only — no dialog. The poll calls this, so the player reads
+// as off air quietly in the background.
+function showOffAirArt() {
   $('#track').text('* Off Air *');
   $('#track').text('* Off Air *');
   $('#radio').attr('src', get_radio_none()).fadeIn(300);
   $('#radio').attr('src', get_radio_none()).fadeIn(300);
   $('#eq').attr('src', get_radio_eq_none()).fadeIn(300);
   $('#eq').attr('src', get_radio_eq_none()).fadeIn(300);
 }
 }
 
 
+// The dialog, shown only in response to someone actually trying to play, or a
+// retry that failed. It used to be fired by the 20s poll, which meant it threw
+// itself over the page unprompted whenever nobody was broadcasting.
+//
+// Resets the progress bar each time it opens, so a second attempt doesn't start
+// out showing the last one's finished bar and stale ERROR.
+function showConnectionError() {
+  $('#error-reconnecting').hide();
+  $('#error-reconnecting-again').hide();
+  $('#connection-error-reconnecting').attr('data-transitiongoal', 0).progressbar();
+  $('#connection-error').modal('show');
+}
+
 // Now-playing info comes from our own /api/status, which proxies Icecast's
 // Now-playing info comes from our own /api/status, which proxies Icecast's
 // status-json.xsl server-side.
 // status-json.xsl server-side.
 //
 //
@@ -188,13 +186,16 @@ function showOffAir() {
 // approach depended on a hand-edited XSL file that Icecast upgrades overwrite;
 // approach depended on a hand-edited XSL file that Icecast upgrades overwrite;
 // it 404s today and has since the 2021 upgrade noted in the README. Same-origin
 // it 404s today and has since the 2021 upgrade noted in the README. Same-origin
 // JSON also means no mixed content and no JSONP.
 // JSON also means no mixed content and no JSONP.
+// Returns the jqXHR so the retry button can wait on it.
 function radioTitle() {
 function radioTitle() {
-  $.getJSON(window.RADIO.url('api/status'))
+  return $.getJSON(window.RADIO.url('api/status'))
     .done(function(status) {
     .done(function(status) {
-      if (!status || !status.online) {
-        showOffAir();
+      streamOnline = !!(status && status.online);
+      if (!streamOnline) {
+        showOffAirArt();
         return;
         return;
       }
       }
+      // Back on air: close the dialog if someone's sitting in front of it.
       $('#connection-error').modal('hide');
       $('#connection-error').modal('hide');
       $('#track').text(status.title || 'Unknown track');
       $('#track').text(status.title || 'Unknown track');
       $('#listeners').text(status.listeners);
       $('#listeners').text(status.listeners);
@@ -206,7 +207,8 @@ function radioTitle() {
     .fail(function() {
     .fail(function() {
       // Keep polling rather than clearInterval-ing on the first blip, so the
       // Keep polling rather than clearInterval-ing on the first blip, so the
       // page recovers on its own once the stream or server is back.
       // page recovers on its own once the stream or server is back.
-      showOffAir();
+      streamOnline = false;
+      showOffAirArt();
     });
     });
 }
 }
 interval = setInterval(radioTitle,20000); // every 20 seconds
 interval = setInterval(radioTitle,20000); // every 20 seconds
@@ -218,6 +220,43 @@ $(document).ready(function() {
   // nickname modal opens from the person button beside the message box
   // nickname modal opens from the person button beside the message box
   // (data-toggle="modal" wires that up, no JS needed). Chatting as 'anonymous'
   // (data-toggle="modal" wires that up, no JS needed). Chatting as 'anonymous'
   // is fine; naming yourself is opt-in.
   // is fine; naming yourself is opt-in.
+
+  // Press play with nothing broadcasting -> show the connection dialog.
+  //
+  // Bound on document in the CAPTURE phase so it runs before the event ever
+  // reaches the button, which is what lets stopPropagation keep Amplitude's own
+  // click handler from firing. (Binding on the button itself wouldn't do it:
+  // at the target, listeners run in registration order regardless of capture,
+  // and Amplitude registers first.) Without this the click silently starts a
+  // doomed playback attempt against a dead mount and nothing tells the user.
+  document.addEventListener('click', function (e) {
+    if (!e.target || !e.target.closest) return;
+    if (!e.target.closest('#amplitude-play-pause')) return;
+    if (streamOnline) return;   // let Amplitude have it
+    e.stopPropagation();
+    e.preventDefault();
+    showConnectionError();
+  }, true);
+
+  // Retry actually re-checks now. It used to just animate the bar to 100% and
+  // announce ERROR on the `done` callback, having asked the server nothing.
+  //
+  // Bound once, here. It used to be bound inside showOffAir(), which the poll
+  // called every 20 seconds, so the handlers stacked up for as long as the
+  // stream was down and then all fired on a single click.
+  $('#connection-error-retry').on('click', function () {
+    $('#error-reconnecting-again').hide();
+    $('#error-reconnecting').show();
+    $('#connection-error-reconnecting').attr('data-transitiongoal', 100).progressbar();
+    radioTitle().always(function () {
+      if (streamOnline) {
+        $('#connection-error').modal('hide');
+      } else {
+        $('#error-reconnecting-again').show();
+      }
+    });
+  });
+
   $('#nick').validator();
   $('#nick').validator();
   var socket = window.RADIO.socket;
   var socket = window.RADIO.socket;
   var getNickname = function() {
   var getNickname = function() {