PlayerStore.kt 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package fr.riff_app.riff.playerstore
  2. import android.content.Context
  3. import android.graphics.Bitmap
  4. import android.graphics.BitmapFactory
  5. import android.support.v4.media.session.PlaybackStateCompat
  6. import android.util.Log
  7. import androidx.lifecycle.MutableLiveData
  8. import fr.riff_app.riff.*
  9. import fr.riff_app.riff.planning.Planning
  10. import org.json.JSONObject
  11. import java.net.URL
  12. import java.text.ParseException
  13. import java.text.SimpleDateFormat
  14. import java.time.format.TextStyle
  15. import java.util.*
  16. import kotlin.collections.ArrayList
  17. class PlayerStore {
  18. private lateinit var urlToScrape: String
  19. val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
  20. val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
  21. val volume: MutableLiveData<Int> = MutableLiveData()
  22. val playbackState: MutableLiveData<Int> = MutableLiveData()
  23. val currentTime: MutableLiveData<Long> = MutableLiveData()
  24. val streamerPicture: MutableLiveData<Bitmap> = MutableLiveData()
  25. val streamerName: MutableLiveData<String> = MutableLiveData()
  26. val currentSong : Song = Song()
  27. val currentSongBackup: Song = Song()
  28. val lp : ArrayList<Song> = ArrayList()
  29. val queue : ArrayList<Song> = ArrayList()
  30. val isQueueUpdated: MutableLiveData<Boolean> = MutableLiveData()
  31. val isLpUpdated: MutableLiveData<Boolean> = MutableLiveData()
  32. val isMuted : MutableLiveData<Boolean> = MutableLiveData()
  33. val listenersCount: MutableLiveData<Int> = MutableLiveData()
  34. var latencyCompensator : Long = 0
  35. var isInitialized: Boolean = false
  36. var isStreamDown: Boolean = false
  37. init {
  38. playbackState.value = PlaybackStateCompat.STATE_STOPPED
  39. isPlaying.value = false
  40. isServiceStarted.value = false
  41. streamerName.value = ""
  42. volume.value = preferenceStore.getInt("volume", 100)
  43. currentTime.value = System.currentTimeMillis()
  44. isQueueUpdated.value = false
  45. isLpUpdated.value = false
  46. isMuted.value = false
  47. currentSong.setTitleArtist(noConnectionValue)
  48. currentSongBackup.setTitleArtist(noConnectionValue)
  49. listenersCount.value = 0
  50. }
  51. fun initUrl(c: Context)
  52. {
  53. urlToScrape = c.getString(R.string.API_URL)
  54. }
  55. private fun getTimestamp(s: String) : Long
  56. {
  57. val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss z", Locale.getDefault())
  58. try {
  59. val t: Date? = dateFormat.parse("$s ${Planning.instance.timeZone.getDisplayName(Planning.instance.timeZone.useDaylightTime(), TimeZone.SHORT)}")
  60. //[REMOVE LOG CALLS]Log.d(tag, "date: $s -> $t")
  61. return t!!.time
  62. } catch (e: ParseException) {
  63. e.printStackTrace()
  64. }
  65. return 0
  66. }
  67. // ##################################################
  68. // ################# API FUNCTIONS ##################
  69. // ##################################################
  70. private fun updateApi(res: JSONObject, isCompensatingLatency : Boolean = false) {
  71. // If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
  72. val resMain = res.getJSONObject("tracks").getJSONObject("current")
  73. val s = extractSong(resMain)
  74. if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
  75. || currentSong.title.value == noConnectionValue)
  76. currentSong.setTitleArtist("${s.artist.value} - ${s.title.value}")
  77. val starts = s.startTime.value
  78. val ends = s.stopTime.value
  79. if (currentSong.startTime.value != starts)
  80. currentSong.startTime.value = starts
  81. currentSong.stopTime.value = ends
  82. val apiTime = getTimestamp(res.getJSONObject("station").getString("schedulerTime"))
  83. // I noticed that the server has a big (3 to 9 seconds !!) offset for current time.
  84. // we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed
  85. // latencyCompensator is set to null when beginPlaying() (we can't measure it at the moment we start playing, since we're in the middle of a song),
  86. // at this moment, we set it to 0. Then, next time the updateApi is called when we're playing, we measure the latency and we set out latencyComparator.
  87. if(isCompensatingLatency)
  88. {
  89. latencyCompensator = apiTime - (currentSong.startTime.value!!)
  90. //[REMOVE LOG CALLS]Log.d(tag, "latency compensator set to ${(latencyCompensator).toFloat() / 1000} s")
  91. }
  92. currentTime.value = apiTime - (latencyCompensator)
  93. /*
  94. val listeners = resMain.getInt("listeners")
  95. listenersCount.value = listeners
  96. //[REMOVE LOG CALLS]Log.d((tag, playerStoreTag + "store updated")
  97. */
  98. }
  99. private val scrape : (Any?) -> String =
  100. {
  101. URL(urlToScrape).readText()
  102. }
  103. /* initApi is called :
  104. - at startup
  105. - when a streamer changes.
  106. the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup.
  107. The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes.
  108. */
  109. fun initApi()
  110. {
  111. val post : (parameter: Any?) -> Unit = {
  112. val result = JSONObject(it as String)
  113. if (result.has("tracks"))
  114. {
  115. updateApi(result)
  116. currentSongBackup.copy(currentSong)
  117. fetchLastRequest()
  118. isQueueUpdated.value = true
  119. isLpUpdated.value = true
  120. }
  121. isInitialized = true
  122. }
  123. Async(scrape, post)
  124. }
  125. fun fetchApi(isCompensatingLatency: Boolean = false) {
  126. val post: (parameter: Any?) -> Unit = {
  127. val result = JSONObject(it as String)
  128. if (!result.isNull("tracks"))
  129. {
  130. updateApi(result, isCompensatingLatency)
  131. }
  132. }
  133. Async(scrape, post)
  134. }
  135. private fun extractSong(songJSON: JSONObject) : Song {
  136. val song = Song()
  137. song.setTitleArtist(songJSON.getString("name"))
  138. song.startTime.value = getTimestamp(songJSON.getString("starts"))
  139. song.stopTime.value = getTimestamp(songJSON.getString("ends"))
  140. song.type.value = 0 // only used for R/a/dio
  141. return song
  142. }
  143. // ##################################################
  144. // ############## QUEUE / LP FUNCTIONS ##############
  145. // ##################################################
  146. fun updateQueue() {
  147. //[REMOVE LOG CALLS]Log.d(tag, queue.toString())
  148. fetchLastRequest()
  149. }
  150. fun updateLp() {
  151. // note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
  152. //if (lp.isNotEmpty()){
  153. val n = Song()
  154. n.copy(currentSongBackup)
  155. if (n != Song(noConnectionValue) && n != Song(streamDownValue))
  156. lp.add(0, n)
  157. currentSongBackup.copy(currentSong)
  158. isLpUpdated.value = true
  159. //[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + lp.toString())
  160. //}
  161. }
  162. private fun fetchLastRequest()
  163. {
  164. isQueueUpdated.value = false
  165. val sleepScrape: (Any?) -> String = {
  166. /* we can maximize our chances to retrieve the last queued song by specifically waiting for the number of seconds we measure between ICY metadata and API change.
  167. we add 2 seconds just to get a higher probability that the API has correctly updated. (the latency compensator can have a jitter of 1 second usually)
  168. If, against all odds, the API hasn't updated yet, we will retry in the same amount of seconds. So we'll have the data anyway.
  169. This way to fetch at the most probable time is a good compromise between fetch speed and fetch frequency
  170. We don't fetch too often, and we start to fetch at the most *probable* time.
  171. If there's no latencyCompensator measured yet, we only wait for 3 seconds.
  172. If the song is the same, it will be called again. 3 seconds is a good compromise between speed and frequency:
  173. it might be called twice, rarely 3 times, and it's only the 2 first songs ; after these, the latencyCompensator is set to fetch at the most probable time.
  174. */
  175. val sleepTime: Long = if (latencyCompensator > 0) latencyCompensator + 2000 else 3000
  176. Thread.sleep(sleepTime) // we wait a bit (10s) for the API to get updated on R/a/dio side!
  177. URL(urlToScrape).readText()
  178. }
  179. lateinit var post: (parameter: Any?) -> Unit
  180. fun postFun(result: JSONObject)
  181. {
  182. if (result.has("tracks")) {
  183. val resMain = result.getJSONObject("tracks")
  184. /*
  185. if ((resMain.has("isafkstream") && !resMain.getBoolean("isafkstream")) &&
  186. queue.isNotEmpty())
  187. {
  188. queue.clear() //we're not requesting anything anymore.
  189. isQueueUpdated.value = true
  190. } else if (resMain.has("isafkstream") && resMain.getBoolean("isafkstream") &&
  191. queue.isEmpty())
  192. {
  193. initApi()
  194. } else
  195. */
  196. if (resMain.has("next")) {
  197. val queueJSON =
  198. resMain.getJSONObject("next")
  199. val t = extractSong(queueJSON)
  200. if (queue.isNotEmpty() && (t == queue.last() || t == currentSong) && isQueueUpdated.value == false)
  201. {
  202. //[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + "Song already in there: $t\nQueue:$queue")
  203. Async(sleepScrape, post)
  204. } else {
  205. if (queue.isNotEmpty())
  206. queue.remove(queue.first())
  207. queue.add(queue.size, t)
  208. //[REMOVE LOG CALLS]Log.d(tag, playerStoreTag + "added last queue song: $t")
  209. isQueueUpdated.value = true
  210. return // FUUUCK IT WAS CALLING THE ASYNC ONE MORE TIME AFTERWARDS !?
  211. }
  212. }
  213. }
  214. }
  215. post = {
  216. val result = JSONObject(it as String)
  217. /* The goal is to pass the result to a function that will process it (postFun).
  218. The magic trick is, under circumstances, the last queue song might not have been updated yet when we fetch it.
  219. So if this is detected ==> if (t == queue.last() )
  220. Then the function re-schedule an Async(sleepScrape, post).
  221. To do that, the "post" must be defined BEFORE the function, but the function must be defined BEFORE the "post" value.
  222. So I declare "post" as lateinit var, define the function, then define the "post" that calls the function. IT SHOULD WORK.
  223. */
  224. postFun(result)
  225. }
  226. Async(sleepScrape, post)
  227. }
  228. // ##################################################
  229. // ############## PICTURE FUNCTIONS #################
  230. // ##################################################
  231. fun initPicture(c: Context) {
  232. streamerPicture.value = BitmapFactory.decodeResource(c.resources,
  233. R.drawable.logo_roundsquare
  234. )
  235. }
  236. private val playerStoreTag = "====PlayerStore===="
  237. companion object {
  238. val instance by lazy {
  239. PlayerStore()
  240. }
  241. }
  242. }