Parcourir la source

added Airtime API and progress bar

yattoz il y a 5 ans
Parent
révision
c03a002448

+ 2 - 2
app/build.gradle Voir le fichier

@@ -29,8 +29,8 @@ android {
29 29
         applicationId "fr.forum_thalie.tsumugi"
30 30
         minSdkVersion 16
31 31
         targetSdkVersion 29
32
-        versionCode 100
33
-        versionName "1.0.0"
32
+        versionCode 110
33
+        versionName "1.1.0"
34 34
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
35 35
         vectorDrawables.useSupportLibrary = true
36 36
     }

+ 0 - 3
app/src/main/AndroidManifest.xml Voir le fichier

@@ -31,9 +31,6 @@
31 31
                 <action android:name="android.media.browse.MediaBrowserService" />
32 32
             </intent-filter>
33 33
         </service>
34
-        <service android:name=".streamerNotificationService.StreamerMonitorService"
35
-            android:enabled="true"
36
-            android:exported="true"/>
37 34
 
38 35
         <receiver android:name=".BootBroadcastReceiver"
39 36
             android:directBootAware="true"

+ 4 - 0
app/src/main/java/fr/forum_thalie/tsumugi/BootBroadcastReceiver.kt Voir le fichier

@@ -22,9 +22,13 @@ class BootBroadcastReceiver : BroadcastReceiver(){
22 22
 
23 23
         if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
24 24
         {
25
+
25 26
             RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
27
+            if (!PlayerStore.instance.isInitialized)
28
+                PlayerStore.instance.initApi()
26 29
             if (PlayerStore.instance.streamerName.value.isNullOrBlank())
27 30
                 PlayerStore.instance.initPicture(context)
31
+            // TODO: add initialization for programme.
28 32
 
29 33
             val i = Intent(context, RadioService::class.java)
30 34
             i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)

+ 3 - 0
app/src/main/java/fr/forum_thalie/tsumugi/MainActivity.kt Voir le fichier

@@ -129,6 +129,9 @@ class MainActivity : BaseActivity() {
129 129
         colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
130 130
         colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
131 131
 
132
+        PlayerStore.instance.initUrl(this)
133
+        PlayerStore.instance.initApi()
134
+
132 135
         // Post-UI Launch
133 136
         if (PlayerStore.instance.isInitialized)
134 137
         {

+ 27 - 0
app/src/main/java/fr/forum_thalie/tsumugi/RadioService.kt Voir le fichier

@@ -124,6 +124,27 @@ class RadioService : MediaBrowserServiceCompat() {
124 124
 
125 125
     private val titleObserver = Observer<String> {
126 126
         // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
127
+
128
+        if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING)
129
+        {
130
+            Log.d(tag, radioTag + "SONG CHANGED AND PLAYING")
131
+            // we activate latency compensation only if it's been at least 2 songs...
132
+            when {
133
+                PlayerStore.instance.isStreamDown -> {
134
+                    // if we reach here, it means that the observer has been called by a new song and that the stream was down previously.
135
+                    // so the stream is now back to normal.
136
+                    PlayerStore.instance.isStreamDown = false
137
+                    PlayerStore.instance.initApi()
138
+                }
139
+                PlayerStore.instance.currentSong.title.value == noConnectionValue -> {
140
+                    PlayerStore.instance.isStreamDown = true
141
+                }
142
+                else -> {
143
+                    PlayerStore.instance.fetchApi(numberOfSongs >= 2)
144
+                }
145
+            }
146
+        }
147
+
127 148
         if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
128 149
             && it != noConnectionValue)
129 150
         {
@@ -163,6 +184,12 @@ class RadioService : MediaBrowserServiceCompat() {
163 184
 
164 185
         preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
165 186
 
187
+        // start ticker for when the player is stopped
188
+        val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10"
189
+        val period: Long = Integer.parseInt(periodString).toLong()
190
+        if (period > 0)
191
+            apiTicker.schedule(ApiFetchTick(), 0, period * 1000)
192
+
166 193
         // Define managers
167 194
         telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
168 195
         telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)

+ 10 - 0
app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt Voir le fichier

@@ -1,8 +1,18 @@
1 1
 package fr.forum_thalie.tsumugi
2 2
 
3
+import android.support.v4.media.session.PlaybackStateCompat
3 4
 import fr.forum_thalie.tsumugi.playerstore.PlayerStore
4 5
 import java.util.*
5 6
 
7
+class ApiFetchTick  : TimerTask() {
8
+    override fun run() {
9
+        if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
10
+        {
11
+            PlayerStore.instance.fetchApi()
12
+        }
13
+    }
14
+}
15
+
6 16
 class Tick  : TimerTask() {
7 17
     override fun run() {
8 18
         PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)

+ 114 - 0
app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt Voir le fichier

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

+ 3 - 3
app/src/main/res/layout/fragment_nowplaying.xml Voir le fichier

@@ -305,7 +305,7 @@
305 305
             android:progressDrawable="@drawable/progress_bar_progress"
306 306
             app:layout_constraintBottom_toTopOf="@id/play_pause"
307 307
             tools:layout_editor_absoluteX="0dp"
308
-            android:visibility="gone"/>
308
+            android:visibility="visible"/>
309 309
 
310 310
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
311 311
         <TextView
@@ -318,7 +318,7 @@
318 318
             android:textAlignment="textEnd"
319 319
             app:layout_constraintEnd_toEndOf="@id/progressBar"
320 320
             app:layout_constraintTop_toBottomOf="@id/progressBar"
321
-            android:visibility="gone"/>
321
+            android:visibility="visible"/>
322 322
 
323 323
         <!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
324 324
         <TextView
@@ -331,7 +331,7 @@
331 331
             android:textAlignment="textStart"
332 332
             app:layout_constraintStart_toStartOf="@id/progressBar"
333 333
             app:layout_constraintTop_toBottomOf="@id/progressBar"
334
-            android:visibility="gone"/>
334
+            android:visibility="visible"/>
335 335
 
336 336
 
337 337
 

+ 1 - 0
app/src/main/res/values/strings.xml Voir le fichier

@@ -7,6 +7,7 @@
7 7
     <string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
8 8
     <string name="website_url">https://tsumugi.forum-thalie.fr/</string>
9 9
     <string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
10
+    <string name="API_URL">https://radio.mahoro-net.org/airtime/api/live-info-v2</string>
10 11
     <string name="planning_url">ADD SOME URL HERE</string>
11 12
 
12 13