For builders

Embedding Guru Vani Radio

The stream is a plain, public MP3 feed over HTTPS. There is no API key, no account and no geoblocking, so a website, a native app or an Alexa skill can play it directly. These notes collect the endpoints, the format and the platform rules in one place.

← Back to the listener page

The stream at a glance

Live streamhttps://radio.jaikhyapaparampara.com/listen/guru_vani_radio/radio.mp3
FormatMP3, 64 kbps CBR, 44.1 kHz, stereo, continuous
TransportIcecast 2 served by AzuraCast; audio/mpeg; HTTP 206 responses; Accept-Ranges: bytes
TLSLet's Encrypt certificate, auto-renewed; HTTPS only; HSTS enabled
AccessPublic and unauthenticated; no ads; no listener cap
CORSStream reflects the request Origin; Access-Control-Allow-Credentials: true; ICY headers exposed
ICY metadataYes. Send Icy-MetaData: 1 and the response adds icy-metaint: 16000; the current talk title follows every 16,000 bytes
Playlists/public/guru_vani_radio/playlist.m3u and /public/guru_vani_radio/playlist.pls
Public player/public/guru_vani_radio — a full AzuraCast player page; it sets no X-Frame-Options, so it can be iframed
Now playing JSON/api/nowplaying/guru_vani_radio — public, Access-Control-Allow-Origin: *, cached 15 s
Not availableHLS and AAC mounts (both 404); on-demand downloads; requests list
Content150 recorded talks, ordered oldest to newest and looped; talk dates are in the artist field as YYYYMMDD

The station shortcode used across the AzuraCast URLs is guru_vani_radio. Everything above is served from radio.jaikhyapaparampara.com, a separate host from the website.

Embed it in a web page

The smallest possible embed is a plain audio element. The site's own player does more (current talk, progress, history) by polling the now-playing endpoint every 15 seconds.

<audio controls preload="none"
  src="https://radio.jaikhyapaparampara.com/listen/guru_vani_radio/radio.mp3">
</audio>

If you want the ready-made AzuraCast interface instead, the public player can be framed — it sends no X-Frame-Options and no frame-ancestors policy:

<iframe
  src="https://radio.jaikhyapaparampara.com/public/guru_vani_radio"
  title="Guru Vani Radio"
  width="100%" height="520" style="border:0"
  loading="lazy"></iframe>

Two rules apply in every browser. Playback must start from a user gesture (a tap or click), and if you call play() from JavaScript the returned promise can reject with NotAllowedError — handle it and ask for a tap. The site's own player does exactly that.

Android apps

The recommended path is Jetpack Media3 (ExoPlayer) with a MediaSessionService for background playback. Progressive MP3 plays directly from the URL, and Media3 handles Icecast metadata itself: it asks for Icy-MetaData: 1, strips the interleaved metadata, and merges the current talk title into player.mediaMetadata.

// Gradle: media3-exoplayer and media3-session
// Manifest: INTERNET, FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK
private const val STREAM = "https://radio.jaikhyapaparampara.com/listen/guru_vani_radio/radio.mp3"

class PlaybackService : MediaSessionService() {
    private var session: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = ExoPlayer.Builder(this)
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(C.USAGE_MEDIA)
                    .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
                    .build(),
                true,
            )
            .setHandleAudioBecomingNoisy(true)   // pause when headphones unplug
            .setWakeMode(C.WAKE_MODE_NETWORK)     // keep streaming with screen off
            .build()
            .apply {
                setMediaItem(MediaItem.fromUri(STREAM))
                prepare()
                playWhenReady = true
            }
        session = MediaSession.Builder(this, player).build()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = session

    override fun onDestroy() {
        session?.run { player.release(); release() }
        session = null
        super.onDestroy()
    }
}

Declare the service with android:foregroundServiceType="mediaPlayback" and an intent filter for androidx.media3.session.MediaSessionService, then connect from your activity with a MediaController. Media notifications from a media session are exempt from the Android 13+ notification permission.

Options at a glance

ApproachEffortNotes
Media3 ExoPlayer + MediaSessionService1–3 daysRecommended: notification, lock screen, audio focus, ICY metadata
WebView with the public playerHoursPlayback dies with the app; no lock-screen controls
Intent.ACTION_VIEW to VLC or the browserMinutesSimplest of all; leaves your app and loses your branding
Platform MediaPlayer~1 dayPlays the stream, but no ICY metadata or MediaSession integration

A live stream never ends, so do not rely on the player finishing an item. Recover from changes and outages (Wi-Fi to cellular, long background pauses) by re-preparing the media item with a capped backoff — 1, 2, 5, 10 seconds — and resetting it on success. Add com.google.android.gms.car.application metadata and a MediaLibraryService browse tree if you want Android Auto.

iPhone and iPad apps

AVPlayer with an audio session is the standard approach. Continuous MP3 over HTTPS works in practice, but Apple does not document a live Icecast stream as a supported input, so treat it as empirical: test on the current iOS release and always ship reconnect logic. AVPlayer also does not expose ICY metadata, so poll the now-playing JSON for the current talk.

// Info.plist: UIBackgroundModes = ["audio"]
import AVFoundation
import MediaPlayer

final class RadioPlayer {
    private let player = AVPlayer()
    private let stream = URL(
        string: "https://radio.jaikhyapaparampara.com/listen/guru_vani_radio/radio.mp3"
    )!

    func start() throws {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playback, mode: .default)
        try session.setActive(true)                 // keeps playing when locked

        let item = AVPlayerItem(url: stream)
        item.automaticallyWaitsToMinimizeStalling = false   // start live audio now
        player.replaceCurrentItem(with: item)
        player.play()

        MPNowPlayingInfoCenter.default().nowPlayingInfo = [
            MPMediaItemPropertyTitle: "Guru Vani Radio",
            MPMediaItemPropertyArtist: "Jai Khyapa Parampara",
            MPNowPlayingInfoPropertyIsLiveStream: true
        ]
    }
}

Add MPRemoteCommandCenter handlers for play and pause so the lock screen and headphones work, and handle AVAudioSession.interruptionNotification plus route changes (unplugged headphones should pause, not blast the speaker). Observe AVPlayerItem.status == .failed, playbackStalledNotification and AVPlayerItemFailedToPlayToEndTime for reconnection.

Options at a glance

ApproachEffortNotes
AVPlayer + AVAudioSession + Now Playing1–3 daysRecommended: background audio, lock screen, small binary
MobileVLCKit2–4 daysTolerates ICY and flaky networks; large binary and LGPL dynamic-linking duties
WKWebView with the public playerHoursBackground audio and lock screen unreliable
UIApplication.shared.open to SafariMinutesSimplest; exits your app

CarPlay needs Apple's com.apple.developer.carplay-audio entitlement, which is requested from Apple and can take weeks. A pure stream wrapper also risks App Store rejection under guideline 4.2 (minimum functionality) unless it adds real value: now playing, history, favourites, sharing, offline notes.

Autoplay, background audio and the silent switch

Audible autoplay is blocked by policy. On iOS it cannot be enabled at all; on Android Chrome it is allowed only after the visitor has interacted with the site or installed it. No header, flag or script changes that, so the correct design is a prominent tap-to-play button with honest failure handling — exactly what the listener page does.

iPhone and iPad

  • There is no autoplay setting. iOS has no Auto-Play preference — not in Settings > Apps > Safari, and not in the per-site aA menu. The familiar “Allow All Auto-Play” / “Stop Media with Sound” / “Never Auto-Play” choice exists only in Safari on macOS. Do not send iPhone users looking for it.
  • Call play() synchronously inside a click, touchend or keydown handler. Fetching anything first and calling play() afterwards can lose the gesture and reject with NotAllowedError.
  • Keep one persistent <audio> element. The first tap unlocks that element for the session, so reconnects and src changes work later; a new element or a fresh page load needs a new tap. Adding the page to the Home Screen does not remove the first tap.
  • The muted-autoplay trick does not work. WebKit's exception covers muted <video> only; <audio muted autoplay> still needs a gesture, and unmuting without one pauses playback — which is pointless for a live radio stream anyway.
  • Media Session. After the first tap, iOS Safari 15+ supports the Media Session API: lock-screen artwork, title and play/pause. Use it; skip setPositionState for a live stream.
  • Background audio. A plain <audio> in a Safari tab keeps playing when the screen locks or the user switches apps; closing the tab stops it. A Home Screen web app behaves similarly, but test it on each iOS release, and remember that timers may be throttled while the screen is locked — do not rely on JavaScript to reconnect in the background.
  • Silent switch. An HTML5 <audio> element plays on the media channel and is audible even when the Ring/Silent switch is on. Only Web Audio is treated as ambient and muted by that switch — if you add Web Audio, set navigator.audioSession.type = 'playback' (Safari 16.4+).
  • Low Power Mode. WebKit tightens media behaviour while it is on; a tap still works, but nothing should be expected to start on its own.
  • In-app browsers. Instagram, Facebook, WhatsApp, TikTok and Gmail open links in their own WebView. They also require a tap, and some interfere with streaming or audio routing. Detect the common user agents and show an “Open in Safari” hint.
  • Failure handling. play() returns a promise. Treat NotAllowedError as “ask for another tap”, AbortError as harmless, and anything else as a network or format problem worth retrying.

Android

  • Chrome always allows muted autoplay, and may allow audible autoplay once the visitor has interacted with the site or installed it to the home screen. Chrome's Media Engagement Index is a desktop feature, not Android.
  • Silent or vibrate mode does not mute media — media volume is separate — but Chrome's per-site sound setting can still block audio.
  • Use the same tap-first design; it is the only behaviour you can rely on across browsers and WebViews.

The pattern that works

const audio = document.querySelector('#radio-audio');
document.querySelector('#radio-toggle').addEventListener('click', () => {
  audio.play().catch((error) => {
    if (error.name === 'NotAllowedError') {
      // Gesture lost or blocked: keep the button visible and ask for a tap.
    } else if (error.name !== 'AbortError') {
      // Network or format problem: show a retry state.
    }
  });
});

Keep the same element and re-call play() after reconnection or when the page becomes visible again; only fall back to a button prompt if the promise rejects.

One codebase: React Native, Flutter, WebView

These all wrap the native players described above, so they inherit the same rules — a foreground service on Android, the audio background mode on iOS, and a user gesture on both.

  • React Native Track Player. Add the stream as a track with type: TrackType.Default. Playback uses ExoPlayer or AVPlayer underneath and you get a notification, lock-screen controls and background playback.
  • Flutter just_audio. Plays the URL directly. Pair it with just_audio_background (or audio_service) for the notification and background service.
  • Capacitor, Cordova or a plain WebView. The quickest route, but background audio is unreliable on iOS and playback stops when the app is killed. Fine for an internal or demo app, not for the store.

Practical notes

  • Metadata. The station's current talk is in /api/nowplaying/guru_vani_radio: now_playing.song.title holds the title, now_playing.song.artist the recording date as YYYYMMDD, and now_playing.duration / elapsed power a progress bar. Poll it every 15–30 seconds; it is cached for 15 seconds.
  • Raw ICY clients. Send Icy-MetaData: 1 and the response adds icy-metaint: 16000. Read the stream as 16,000-byte audio blocks, each followed by one length byte and the metadata block containing StreamTitle. That is how VLC, foobar2000 and ExoPlayer show the current talk.
  • Data and battery. 64 kbps works out to roughly 28.8 MB an hour, or about 230 MB over eight hours. Offer a Wi-Fi-only switch.
  • Reconnects. Treat it as live: never seek, and when the connection drops re-open the URL with a capped backoff rather than waiting for a natural end.
  • CORS. The stream reflects any request Origin and exposes the ICY response headers. A plain <audio src> needs no CORS attributes at all; do not add crossorigin unless you genuinely need Web Audio analysis.
  • HLS is not available today — the endpoints return 404. Enabling it in AzuraCast would give first-class HLS on both platforms, at the cost of 6–30 seconds of extra latency. Progressive MP3 is the low-latency option.
  • Attribution. Link back to the listener page, keep the talks free to hear, and write to us before publishing an app or skill.
  • Test checklist. Lock-screen controls, headphones unplugged, Wi-Fi to cellular, eight hours in the background, iPhone silent switch, and a cold start after the app was killed.

Amazon Echo / Alexa

Alexa plays audio only from sources it understands: a directory it has integrated, or an Alexa skill. A bare stream URL cannot be spoken to a device. The realistic options, in order of effort:

OptionVoice?Effort / costNotes
Bluetooth from a phoneNoNone, $0Works on current Echo models today; the phone stays the player. Steps are on the listener page.
myTuner Radio directoryPartialFree submission, ~30 min plus reviewmyTuner accepts station registrations and its Alexa skill plays catalogue stations by name (“Alexa, ask myTuner Radio to play …”). Listing is not guaranteed.
TuneIn directoryBestFree self-service outside the US; US listings go through TuneIn On Air (paid)If listed, “Alexa, play Guru Vani Radio” works with no listener setup. TuneIn has historically frozen new additions, so acceptance is uncertain.
Custom Alexa skillYes2–5 developer days, $0 with an Alexa-hosted skillThe only path fully under the station's control: “Alexa, open Guru Vani Radio”. Public release requires Amazon certification.
Radio Browser (third party)Clunky€2/month, ~15 minA consumer skill that accepts arbitrary URLs, including this one, after a one-time web setup. Not a station-side solution.

What a custom skill would need

  • An HTTPS endpoint on port 443 with a trusted certificate — the stream's Let's Encrypt certificate already satisfies this.
  • MP3 between 16 and 384 kbps — 64 kbps is comfortably inside the range.
  • The AudioPlayer interface, plus AMAZON.PauseIntent and AMAZON.ResumeIntent; a live stream never finishes, so PlaybackNearlyFinished is not used.
  • shouldEndSession: true on the playback response, or Alexa stops the stream to listen for more speech.
  • An Amazon developer account. An Alexa-hosted skill needs no AWS account and does not charge for hosting.
  • Beta testing works on devices registered to the developer account; everyone else needs the skill published and certified.
A common misconception. TuneIn's listener-side “Custom URL” feature does not make a station playable by voice on Alexa. It works inside the TuneIn apps (and for Sonos), but Alexa cannot reach those entries. The station must be in the public directory, or served by a skill.