From c03a00244861e3d76869a312ce0de5f22a205c66 Mon Sep 17 00:00:00 2001 From: yattoz <12408848+yattoz@users.noreply.github.com> Date: Sat, 11 Jan 2020 22:51:50 +0100 Subject: [PATCH] added Airtime API and progress bar --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 3 - .../tsumugi/BootBroadcastReceiver.kt | 4 + .../fr/forum_thalie/tsumugi/MainActivity.kt | 3 + .../fr/forum_thalie/tsumugi/RadioService.kt | 27 +++++ .../java/fr/forum_thalie/tsumugi/Tickers.kt | 10 ++ .../tsumugi/playerstore/PlayerStore.kt | 114 ++++++++++++++++++ .../main/res/layout/fragment_nowplaying.xml | 6 +- app/src/main/res/values/strings.xml | 1 + 9 files changed, 164 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9a1cae0..3ac518f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "fr.forum_thalie.tsumugi" minSdkVersion 16 targetSdkVersion 29 - versionCode 100 - versionName "1.0.0" + versionCode 110 + versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12e9c38..7e20574 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,9 +31,6 @@ - { // We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup. + + if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING) + { + Log.d(tag, radioTag + "SONG CHANGED AND PLAYING") + // we activate latency compensation only if it's been at least 2 songs... + when { + PlayerStore.instance.isStreamDown -> { + // if we reach here, it means that the observer has been called by a new song and that the stream was down previously. + // so the stream is now back to normal. + PlayerStore.instance.isStreamDown = false + PlayerStore.instance.initApi() + } + PlayerStore.instance.currentSong.title.value == noConnectionValue -> { + PlayerStore.instance.isStreamDown = true + } + else -> { + PlayerStore.instance.fetchApi(numberOfSongs >= 2) + } + } + } + if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup && it != noConnectionValue) { @@ -163,6 +184,12 @@ class RadioService : MediaBrowserServiceCompat() { preferenceStore = PreferenceManager.getDefaultSharedPreferences(this) + // start ticker for when the player is stopped + val periodString = PreferenceManager.getDefaultSharedPreferences(this).getString("fetchPeriod", "10") ?: "10" + val period: Long = Integer.parseInt(periodString).toLong() + if (period > 0) + apiTicker.schedule(ApiFetchTick(), 0, period * 1000) + // Define managers telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE) diff --git a/app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt b/app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt index 512c132..ec114b6 100644 --- a/app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt +++ b/app/src/main/java/fr/forum_thalie/tsumugi/Tickers.kt @@ -1,8 +1,18 @@ package fr.forum_thalie.tsumugi +import android.support.v4.media.session.PlaybackStateCompat import fr.forum_thalie.tsumugi.playerstore.PlayerStore import java.util.* +class ApiFetchTick : TimerTask() { + override fun run() { + if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED) + { + PlayerStore.instance.fetchApi() + } + } +} + class Tick : TimerTask() { override fun run() { PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500) diff --git a/app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt b/app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt index 41037c6..a2e4b5d 100644 --- a/app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt +++ b/app/src/main/java/fr/forum_thalie/tsumugi/playerstore/PlayerStore.kt @@ -7,9 +7,16 @@ import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.lifecycle.MutableLiveData import fr.forum_thalie.tsumugi.* +import org.json.JSONObject +import java.net.URL +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* +import kotlin.collections.ArrayList class PlayerStore { + private lateinit var urlToScrape: String val isPlaying: MutableLiveData = MutableLiveData() val isServiceStarted: MutableLiveData = MutableLiveData() val volume: MutableLiveData = MutableLiveData() @@ -27,6 +34,7 @@ class PlayerStore { val listenersCount: MutableLiveData = MutableLiveData() var latencyCompensator : Long = 0 var isInitialized: Boolean = false + var isStreamDown: Boolean = false init { playbackState.value = PlaybackStateCompat.STATE_STOPPED @@ -43,6 +51,112 @@ class PlayerStore { listenersCount.value = 0 } + fun initUrl(c: Context) + { + urlToScrape = c.getString(R.string.API_URL) + } + + private fun getTimestamp(s: String) : Long + { + val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault()) + try { + val t: Date? = dateFormat.parse(s) + return t!!.time + } catch (e: ParseException) { + e.printStackTrace() + } + return 0 + } + + + // ################################################## + // ################# API FUNCTIONS ################## + // ################################################## + + private fun updateApi(res: JSONObject, isCompensatingLatency : Boolean = false) { + // If we're not in PLAYING state, update title / artist metadata. If we're playing, the ICY will take care of that. + + val resMain = res.getJSONObject("tracks").getJSONObject("current") + if (playbackState.value != PlaybackStateCompat.STATE_PLAYING || currentSong.title.value.isNullOrEmpty() + || currentSong.title.value == noConnectionValue) + currentSong.setTitleArtist(resMain.getString("name")) + + val starts = getTimestamp(resMain.getString("starts")) + val ends = getTimestamp(resMain.getString("ends")) + + if (currentSong.startTime.value != starts) + currentSong.startTime.value = starts + + currentSong.stopTime.value = ends + + // I noticed that the server has a big (3 to 9 seconds !!) offset for current time. + // we can measure it when the player is playing, to compensate it and have our progress bar perfectly timed + // 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), + // 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. + if(isCompensatingLatency) + { + latencyCompensator = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (currentSong.startTime.value ?: getTimestamp(res.getJSONObject("station").getString("schedulerTime"))) + Log.d(tag, playerStoreTag + "latency compensator set to ${(latencyCompensator).toFloat()/1000} s") + } + currentTime.value = getTimestamp(res.getJSONObject("station").getString("schedulerTime")) - (latencyCompensator) + + /* + val listeners = resMain.getInt("listeners") + listenersCount.value = listeners + Log.d(tag, playerStoreTag + "store updated") + */ + } + + private val scrape : (Any?) -> String = + { + URL(urlToScrape).readText() + } + + /* initApi is called : + - at startup + - when a streamer changes. + the idea is to fetch the queue when a streamer changes (potentially Hanyuu), and at startup. + The Last Played is only fetched if it's empty (so, only at startup), not when a streamer changes. + */ + fun initApi() + { + val post : (parameter: Any?) -> Unit = { + val result = JSONObject(it as String) + if (result.has("tracks")) + { + updateApi(result) + currentSongBackup.copy(currentSong) + + isQueueUpdated.value = true + + isLpUpdated.value = true + } + isInitialized = true + } + Async(scrape, post) + } + + fun fetchApi(isCompensatingLatency: Boolean = false) { + val post: (parameter: Any?) -> Unit = { + val result = JSONObject(it as String) + if (!result.isNull("tracks")) + { + updateApi(result, isCompensatingLatency) + } + } + Async(scrape, post) + } + + private fun extractSong(songJSON: JSONObject) : Song { + val song = Song() + song.setTitleArtist(songJSON.getString("meta")) + song.startTime.value = songJSON.getLong("timestamp") + song.stopTime.value = song.startTime.value + song.type.value = songJSON.getInt("type") + return song + } + + // ################################################## // ############## QUEUE / LP FUNCTIONS ############## // ################################################## diff --git a/app/src/main/res/layout/fragment_nowplaying.xml b/app/src/main/res/layout/fragment_nowplaying.xml index 5fa21cd..f876dd6 100644 --- a/app/src/main/res/layout/fragment_nowplaying.xml +++ b/app/src/main/res/layout/fragment_nowplaying.xml @@ -305,7 +305,7 @@ android:progressDrawable="@drawable/progress_bar_progress" app:layout_constraintBottom_toTopOf="@id/play_pause" tools:layout_editor_absoluteX="0dp" - android:visibility="gone"/> + android:visibility="visible"/> + android:visibility="visible"/> + android:visibility="visible"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f9137f..a132784 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ https://github.com/yattoz/Tsumugi-app/issues/ https://tsumugi.forum-thalie.fr/ https://tsumugi.forum-thalie.fr/?feed=rss2 + https://radio.mahoro-net.org/airtime/api/live-info-v2 ADD SOME URL HERE