0

I am not a fan of polling for information and suspect there is a better way of achieveing what I want.

I am playing an internet radio stream with Android's MediaPlayer. I can find out which tune is playing and by which artist by requesting the 7.html file at the server's address.

My questions are:

  1. Is there a way to receive a notification when a new song begins to play?
  2. Must I poll the 7.html to find out what is now playing?
  3. If I do have to poll, is there any way in which I can determine the duration of the current song so I can poll only when a new song starts?

I guess if I had a low-level stream processing function of my own, I could tell when the song changes because I would receive the meta-data, but I'm not sure how to do that with the Android MediaPlayer class.

SparkyNZ
  • 6,266
  • 7
  • 39
  • 80
  • 1
    Yes, the `7.html` method is not reliable, and and is increasingly unavailable. As you have suggested, you will have to write a stream demuxer to split the audio and metadata components apart. I'm not an Android dev so I can't suggest a way to do this, but I have seen classes around here on Stack Overflow that handle the metadata. I did write up instructions on how to do this in PHP. Perhaps it could be translated: http://stackoverflow.com/questions/4911062/pulling-track-info-from-an-audio-stream-using-php/4914538#4914538 – Brad Nov 09 '14 at 17:32
  • @Brad: Thanks Brad. I have some C# code that does just that.. I'll just poll every 10 seconds for now until I feel the need to write my own stream player in Android. – SparkyNZ Nov 09 '14 at 21:03
  • Did you every try to get the `Metatags` as described here ( http://uniqueculture.net/2010/11/stream-metadata-plain-java/ )? I know that SHOUTcast just updated their API, but I was only working with the iOS version. We use the tag `StreamTitle` which usually to 98% is `ARTIST - TITLE`. You just have use regex if artist contains an "-"... – longi Jan 26 '15 at 08:48
  • @longilong: Thanks for the reply.. Seems like ages ago since I did this. I got it working OK using polling which was annoying (on Android) but whatever I did on iOS worked fine. I think the player abstraction in iOS must filter out the meta data contained within the stream whereas Android did not. Can't believe it was only Nov when I asked about this. Seems a lifetime ago :) – SparkyNZ Jan 31 '15 at 20:37

1 Answers1

0

Haha, seven years after commenting I finally had to implement this :-D I want a tumbleweed badge for this ;-)

  1. Not to my knowledge
  2. Yes
  3. Not to my knowledge, but polling timers between 30-60 seconds should be fine. At the beginning I wanted to reduce network traffic for users, but this is irrelevant if you are streaming radio at the same time :-D

And here my quick and dirty solution, just in case someone needs it. There are some custom classes in the example, but you ll get the point

import androidx.core.text.HtmlCompat
import de.jahpress.android.main.L
import de.jahpress.android.main.MAX_REQUEST_FOR_SHOUTCAST_TRACK_INFO
import de.jahpress.android.service.Data
import de.jahpress.android.service.radio.model.BaseStation
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread

class ShoutCastTrackInfoManager {

    private val timeOut = 5L
    private val pollingIntervalMs = 60_000L
    private var updateTimer: Timer? = null
    private var trackInfoThread: Thread? = null

    private var invalidTrackInfoCounter = 0

    //will ask track info only one time (select station in my use case)
    fun updateTrackInfoFor(station: BaseStation, resultCallback: (info: String?) -> Unit) {
        L.d("TrackInfo: Get title info for ${station.getStationName()}")
        invalidTrackInfoCounter = 0
        stopTrackInfoPolling()
        requestTrackInfoFromShoutcast(station, resultCallback)
    }

    //will start track info polling (if station is playing)
    fun startTrackInfoPolling(station: BaseStation) {
        L.d("TrackInfo: Get title info for ${station.getStationName()}")
        stopTrackInfoPolling()
        updateTimer = Timer()
        updateTimer?.schedule(object : TimerTask() {
            override fun run() {
                requestTrackInfoFromShoutcast(station, null)
            }
        }, 0, pollingIntervalMs)
    }

    fun stopTrackInfoPolling() {
        trackInfoThread?.let {
            L.d("TrackInfo: Stopping current title update for stream")
            it.interrupt()
        }
        updateTimer?.cancel()
    }

    private fun requestTrackInfoFromShoutcast(
        station: BaseStation,
        resultCallback: ((info: String?) -> Unit)?
    ) {
        if (invalidTrackInfoCounter >= MAX_REQUEST_FOR_SHOUTCAST_TRACK_INFO) {
            L.d("TrackInfo: $MAX_REQUEST_FOR_SHOUTCAST_TRACK_INFO invalid stream titles. Sto...")
            invalidTrackInfoCounter = 0
            stopTrackInfoPolling()
            Data.currentTitleInfo = null  //reset track info
            return
        }

        trackInfoThread = thread {
            try {
                var trackInfo: String? = null
                get7HtmlFromStream(station)?.let {
                    L.d("TrackInfo: Request track info at $it")
                    val request = Request.Builder().url(it).build()
                    val okHttpClient = OkHttpClient.Builder()
                        .connectTimeout(timeOut, TimeUnit.SECONDS)
                        .writeTimeout(timeOut, TimeUnit.SECONDS)
                        .readTimeout(timeOut, TimeUnit.SECONDS)
                        .build()

                    val response = okHttpClient.newCall(request).execute()

                    if (response.isSuccessful) {
                        val result = response.body?.string()
                        trackInfo = extractTrackInfoFrom7Html(result)

                        if (trackInfo != null) {
                            Data.currentTitleInfo = trackInfo
                        }
                    }
                    response.close()
                }

                resultCallback?.invoke(trackInfo)

            } catch (e: Exception) {
                L.e(e)
                resultCallback?.invoke(null)
                stopTrackInfoPolling()
            }
        }
    }

    /**
     * Will create Shoutcast 7.html which is located at stream url.
     *
     * For example: http://66.55.145.43:7473/stream
     * 7.html at http://66.55.145.43:7473/7.html
     */
    private fun get7HtmlFromStream(station: BaseStation): String? {
        val baseStreamUrl = station.getStreamUrl()
        L.w("Base url -> $baseStreamUrl")
        if (baseStreamUrl == null) return null

        val numberSlash = baseStreamUrl.count { c -> c == '/' }
        if (numberSlash <= 2) {
            return "$baseStreamUrl/7.html"
        }

        val startOfPath = station.getStreamUrl().indexOf("/", 8)
        val streamUrl = station.getStreamUrl().subSequence(0, startOfPath)
        return "$streamUrl/7.html"
    }

    /**
     * Will convert webpage to trackinfo. Therefore
     * 1. Remove all html-tags
     * 2. Get <body> content of webpage
     * 3. Extract and return trackinfo
     *
     * Trackinfo format is always like
     * "632,1,1943,2000,439,128,Various Artists - Dance to Dancehall"
     * so method will return everything after sixth "," comma character.
     *
     * Important:
     * - Shoutcast might return invalid html
     * - Site will return 404 error strings
     * - might be empty
     */
    private fun extractTrackInfoFrom7Html(html: String?): String? {
        L.i("Extract track info from ->  $html")
        if (html == null) return null
        val content = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
        val array = content.split(",")
        return if (array.size < 7) {
            null
        } else {
            var combinedTrackInfo = ""
            for (index in 6 until array.size) {
                combinedTrackInfo += "${array[index]} "
            }

            if (combinedTrackInfo.trim().isEmpty()) {
                return null
            }

            return combinedTrackInfo
        }
    }
} 
longi
  • 11,104
  • 10
  • 55
  • 89