PlayerStore.kt 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package fr.forum_thalie.tsumugi.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.forum_thalie.tsumugi.*
  9. import org.json.JSONObject
  10. import java.net.URL
  11. import java.text.ParseException
  12. import java.text.SimpleDateFormat
  13. import java.util.*
  14. import kotlin.collections.ArrayList
  15. class PlayerStore {
  16. private lateinit var urlToScrape: String
  17. val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
  18. val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
  19. val volume: MutableLiveData<Int> = MutableLiveData()
  20. val playbackState: MutableLiveData<Int> = MutableLiveData()
  21. val currentTime: MutableLiveData<Long> = MutableLiveData()
  22. val streamerPicture: MutableLiveData<Bitmap> = MutableLiveData()
  23. val streamerName: MutableLiveData<String> = MutableLiveData()
  24. val currentSong : Song = Song()
  25. val currentSongBackup: Song = Song()
  26. val lp : ArrayList<Song> = ArrayList()
  27. val queue : ArrayList<Song> = ArrayList()
  28. val isQueueUpdated: MutableLiveData<Boolean> = MutableLiveData()
  29. val isLpUpdated: MutableLiveData<Boolean> = MutableLiveData()
  30. val isMuted : MutableLiveData<Boolean> = MutableLiveData()
  31. val listenersCount: MutableLiveData<Int> = MutableLiveData()
  32. var latencyCompensator : Long = 0
  33. var isInitialized: Boolean = false
  34. var isStreamDown: Boolean = false
  35. init {
  36. playbackState.value = PlaybackStateCompat.STATE_STOPPED
  37. isPlaying.value = false
  38. isServiceStarted.value = false
  39. streamerName.value = ""
  40. volume.value = preferenceStore.getInt("volume", 100)
  41. currentTime.value = System.currentTimeMillis()
  42. isQueueUpdated.value = false
  43. isLpUpdated.value = false
  44. isMuted.value = false
  45. currentSong.title.value = noConnectionValue
  46. currentSongBackup.title.value = noConnectionValue
  47. listenersCount.value = 0
  48. }
  49. fun initUrl(c: Context)
  50. {
  51. urlToScrape = c.getString(R.string.API_URL)
  52. }
  53. private fun getTimestamp(s: String) : Long
  54. {
  55. val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
  56. try {
  57. val t: Date? = dateFormat.parse(s)
  58. return t!!.time
  59. } catch (e: ParseException) {
  60. e.printStackTrace()
  61. }
  62. return 0
  63. }
  64. // ##################################################
  65. // ################# API FUNCTIONS ##################
  66. // ##################################################
  67. private fun updateApi(res: JSONObject, isCompensatingLatency : Boolean = false) {
  68. // If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that.
  69. val resMain = res.getJSONObject("tracks").getJSONObject("current")
  70. if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty()
  71. || currentSong.title.value == noConnectionValue)
  72. currentSong.setTitleArtist(resMain.getString("name"))
  73. val starts = getTimestamp(resMain.getString("starts"))
  74. val ends = getTimestamp(resMain.getString("ends"))
  75. if (currentSong.startTime.value != starts)
  76. currentSong.startTime.value = starts
  77. currentSong.stopTime.value = ends
  78. // I noticed that the server has a big (3 to 9 seconds !!) offset for current time.
  79. // we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed
  80. // 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),
  81. // 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.
  82. if(isCompensatingLatency)
  83. {
  84. latencyCompensator = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (currentSong.startTime.value ?: getTimestamp(res.getJSONObject("station").getString("schedulerTime")))
  85. Log.d(tag, playerStoreTag + "latency compensator set to ${(latencyCompensator).toFloat()/1000} s")
  86. }
  87. currentTime.value = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (latencyCompensator)
  88. /*
  89. val listeners = resMain.getInt("listeners")
  90. listenersCount.value = listeners
  91. Log.d(tag, playerStoreTag + "store updated")
  92. */
  93. }
  94. private val scrape : (Any?) -> String =
  95. {
  96. URL(urlToScrape).readText()
  97. }
  98. /* initApi is called :
  99. - at startup
  100. - when a streamer changes.
  101. the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup.
  102. The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes.
  103. */
  104. fun initApi()
  105. {
  106. val post : (parameter: Any?) -> Unit = {
  107. val result = JSONObject(it as String)
  108. if (result.has("tracks"))
  109. {
  110. updateApi(result)
  111. currentSongBackup.copy(currentSong)
  112. isQueueUpdated.value = true
  113. isLpUpdated.value = true
  114. }
  115. isInitialized = true
  116. }
  117. Async(scrape, post)
  118. }
  119. fun fetchApi(isCompensatingLatency: Boolean = false) {
  120. val post: (parameter: Any?) -> Unit = {
  121. val result = JSONObject(it as String)
  122. if (!result.isNull("tracks"))
  123. {
  124. updateApi(result, isCompensatingLatency)
  125. }
  126. }
  127. Async(scrape, post)
  128. }
  129. private fun extractSong(songJSON: JSONObject) : Song {
  130. val song = Song()
  131. song.setTitleArtist(songJSON.getString("meta"))
  132. song.startTime.value = songJSON.getLong("timestamp")
  133. song.stopTime.value = song.startTime.value
  134. song.type.value = songJSON.getInt("type")
  135. return song
  136. }
  137. // ##################################################
  138. // ############## QUEUE / LP FUNCTIONS ##############
  139. // ##################################################
  140. fun updateLp() {
  141. // note : lp is empty at initialization. This check was needed when we used the R/a/dio API.
  142. //if (lp.isNotEmpty()){
  143. val n = Song()
  144. n.copy(currentSongBackup)
  145. if (n.title.value != noConnectionValue && n.title.value != streamDownValue)
  146. lp.add(0, n)
  147. currentSongBackup.copy(currentSong)
  148. isLpUpdated.value = true
  149. Log.d(tag, playerStoreTag + lp.toString())
  150. //}
  151. }
  152. // ##################################################
  153. // ############## PICTURE FUNCTIONS #################
  154. // ##################################################
  155. fun initPicture(c: Context) {
  156. streamerPicture.value = BitmapFactory.decodeResource(c.resources,
  157. R.drawable.logo_roundsquare
  158. )
  159. }
  160. private val playerStoreTag = "====PlayerStore===="
  161. companion object {
  162. val instance by lazy {
  163. PlayerStore()
  164. }
  165. }
  166. }