Browse Source

Audio autoplay (probe + first-gesture) and recent-tracks polling

Autoplay, done honestly. Browsers block audio-with-sound autoplay on a cold
visit (granted per-origin via Media Engagement Index), and Amplitude v2 doesn't
check the play() promise, so a blind attempt would show "playing" with no sound.
Instead:
  - On load, PROBE: play a tiny silent clip. Autoplay permission is per-page, so
    if the probe plays the real stream will too -- then click play. If blocked,
    leave it paused. Never fakes the state.
  - Fallback: start on the first user gesture anywhere (a real activation the
    browser always accepts), so it's never more than one click away -- guarded so
    clicking the play button itself doesn't double-toggle.
  - Gated on Amplitude being bound (window.load) to avoid clicking before its
    handler exists, which would silently no-op and never retry.

Recent tracks now refresh every 2 minutes, REPLACING the list rather than
appending (the old code would have stacked duplicates). Only the first load
fades in; refreshes swap items silently. 120s poll vs the 60s /api/lastfm cache
means every poll gets fresh data. Values go in as text nodes now, so a track or
artist name can't inject markup (also closes a latent unclosed-span bug).
windhamdavid 3 weeks ago
parent
commit
424cdb6a88
1 changed files with 95 additions and 31 deletions
  1. 95 31
      src/js/radio.js

+ 95 - 31
src/js/radio.js

@@ -4,6 +4,72 @@
 // Read when the play button is pressed — see the interceptor further down.
 // Read when the play button is pressed — see the interceptor further down.
 var streamOnline = false;
 var streamOnline = false;
 
 
+// One-shot autostart. It needs TWO things true: the stream is live (known from
+// the status poll) and Amplitude has finished initializing (it binds its play
+// handler on window.onload). Clicking before Amplitude is bound is a silent
+// no-op, and radioAutoplayTried would then block a retry — so gate on both and
+// let whichever finishes last fire it.
+var radioAutoplayTried = false;
+var amplitudeReady = false;
+// After the load event, defer one turn so Amplitude's own onload handler (which
+// binds the button) has definitely run.
+window.addEventListener('load', function () {
+  setTimeout(function () { amplitudeReady = true; maybeRadioAutoplay(); }, 0);
+});
+
+function maybeRadioAutoplay() {
+  if (radioAutoplayTried || !amplitudeReady || !streamOnline) return;
+  radioAutoplayTried = true;
+  tryRadioAutoplay();
+}
+
+// A tiny silent WAV. Playing it unmuted is subject to the same per-page autoplay
+// policy as the real stream, so a successful play() means autoplay is allowed.
+var SILENT_CLIP =
+  'data:audio/wav;base64,UklGRjQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YRAAAACAgICAgICAgICAgICAgICA';
+
+// Fallback for when the browser blocks load-autoplay (a cold visit, Safari,
+// etc.): start the radio on the first user interaction anywhere on the page.
+// The first real gesture is an activation the browser accepts, so audio starts
+// without the user having to find the play button. Fires at most once.
+function armFirstGestureAutoplay() {
+  function start(e) {
+    // If the gesture IS the play button, let Amplitude handle it — clicking it
+    // ourselves too would toggle it right back to paused.
+    if (e.target && e.target.closest && e.target.closest('#amplitude-play-pause')) {
+      remove();
+      return;
+    }
+    var btn = document.getElementById('amplitude-play-pause');
+    if (streamOnline && btn && btn.classList.contains('amplitude-paused')) {
+      btn.click();
+      remove(); // started; stop listening
+    }
+    // If offline, stay armed so a later gesture (once live) still starts it.
+  }
+  function remove() {
+    document.removeEventListener('pointerdown', start, true);
+    document.removeEventListener('keydown', start, true);
+  }
+  document.addEventListener('pointerdown', start, true);
+  document.addEventListener('keydown', start, true);
+}
+
+function tryRadioAutoplay() {
+  var probe;
+  try { probe = new Audio(SILENT_CLIP); } catch (e) { return; }
+  var p = probe.play();
+  if (!p || !p.then) return; // no promise: don't risk a fake-playing state
+  p.then(function () {
+    // Allowed. Stop the probe and start the real player via its button.
+    try { probe.pause(); } catch (e) { /* noop */ }
+    var btn = document.getElementById('amplitude-play-pause');
+    if (btn && streamOnline && btn.classList.contains('amplitude-paused')) btn.click();
+  }).catch(function () {
+    // Blocked: leave the radio paused; the play button is right there.
+  });
+}
+
 
 
 /* Radio Funtions */
 /* Radio Funtions */
 
 
@@ -104,36 +170,27 @@ var streamOnline = false;
 
 
 
 
 
 
-function getRecentTracks() {
-  
-	$.fn.lfmr = function(){
-    //tracksinterval = setInterval(getRecentTracks,180000);  // check every 3 minutes
-		var urla = window.RADIO.url("api/lastfm?method=user.getrecenttracks&limit=100");
-		var tracks = [];
-		function isLoadedr (recentElement) {
-			for (var i = 0; i < tracks.length; i++){
-				var markup = $("<li class='list-group-item'>" + tracks[i].artist + " - <span class='artist'>" + tracks[i].title + " : " + tracks[i].album + "</li>");
-				recentElement.append(markup);
-			}
-		}
-		return this.each(function(){
-			var $this = $(this);
-			$.getJSON( urla, function(data){
-				$(data.recenttracks.track).each(function(){
-					tracks.push ({
-						artist:	this.artist["#text"],
-						title: this.name,
-						album: this.album["#text"]
-					});
-				});
-				isLoadedr($this);
-			});
-		});
-	};
-	
-	$('.recent').lfmr();
-  $('.recent').hide();
-  $('.recent').fadeIn(1000);
+// Recently-played tracks, refreshed on an interval (see $(document).ready).
+//
+// REPLACES the list on each call — the old version appended, so polling would
+// have stacked duplicates. Only the first load fades in; refreshes swap the
+// items silently so the tab doesn't blink every couple of minutes. Values go in
+// via text nodes / .text() so a track or artist name can't inject markup.
+function getRecentTracks(initial) {
+  $.getJSON(window.RADIO.url("api/lastfm?method=user.getrecenttracks&limit=100"), function (data) {
+    var tracks = data && data.recenttracks && data.recenttracks.track;
+    if (!tracks) return;
+    $('.recent').each(function () {
+      var $el = $(this).empty();
+      $(tracks).each(function () {
+        var $li = $("<li class='list-group-item'></li>");
+        $li.append(document.createTextNode(this.artist["#text"] + " - "));
+        $li.append($("<span class='artist'></span>").text(this.name + " : " + this.album["#text"]));
+        $el.append($li);
+      });
+    });
+    if (initial) $('.recent').hide().fadeIn(1000);
+  });
 }
 }
 
 
 
 
@@ -203,6 +260,9 @@ function radioTitle() {
       $('#bitrate').text(status.bitrate === null ? '--' : status.bitrate);
       $('#bitrate').text(status.bitrate === null ? '--' : status.bitrate);
       $('#radio').attr('src', get_radio_tower()).fadeIn(300);
       $('#radio').attr('src', get_radio_tower()).fadeIn(300);
       $('#eq').attr('src', get_radio_eq()).fadeIn(300);
       $('#eq').attr('src', get_radio_eq()).fadeIn(300);
+
+      // Autostart if the browser allows it (see maybeRadioAutoplay / the probe).
+      maybeRadioAutoplay();
     })
     })
     .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
@@ -273,7 +333,11 @@ $(document).ready(function() {
     }
     }
   }); 
   }); 
   radioTitle(); // call it once on load to avoid 20s delay
   radioTitle(); // call it once on load to avoid 20s delay
-  getRecentTracks();  //call it once to avoid 3m delay
+  armFirstGestureAutoplay(); // start audio on first interaction if load-autoplay was blocked
+  getRecentTracks(true); // initial load (fades in)
+  // Refresh recent tracks every 2 minutes. The /api/lastfm proxy caches for
+  // 60s, so a 120s poll always gets fresh data without hammering Last.fm.
+  setInterval(function () { getRecentTracks(false); }, 120000);
   var randomColor = Math.floor(Math.random()*16777215).toString(16);
   var randomColor = Math.floor(Math.random()*16777215).toString(16);
   $("span#user-label").css({ backgroundColor: '#' + randomColor });
   $("span#user-label").css({ backgroundColor: '#' + randomColor });
   $('ul.nav-tabs a').tooltip();
   $('ul.nav-tabs a').tooltip();