added Airtime API and progress bar
parent
1488d51388
commit
c03a002448
|
|
@ -29,8 +29,8 @@ android {
|
||||||
applicationId "fr.forum_thalie.tsumugi"
|
applicationId "fr.forum_thalie.tsumugi"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 100
|
versionCode 110
|
||||||
versionName "1.0.0"
|
versionName "1.1.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name=".streamerNotificationService.StreamerMonitorService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true"/>
|
|
||||||
|
|
||||||
<receiver android:name=".BootBroadcastReceiver"
|
<receiver android:name=".BootBroadcastReceiver"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,13 @@ class BootBroadcastReceiver : BroadcastReceiver(){
|
||||||
|
|
||||||
if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
|
if (arg1.getStringExtra("action") == "$tag.${Actions.PLAY_OR_FALLBACK.name}" )
|
||||||
{
|
{
|
||||||
|
|
||||||
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
RadioAlarm.instance.setNextAlarm(context) // schedule next alarm
|
||||||
|
if (!PlayerStore.instance.isInitialized)
|
||||||
|
PlayerStore.instance.initApi()
|
||||||
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
|
if (PlayerStore.instance.streamerName.value.isNullOrBlank())
|
||||||
PlayerStore.instance.initPicture(context)
|
PlayerStore.instance.initPicture(context)
|
||||||
|
// TODO: add initialization for programme.
|
||||||
|
|
||||||
val i = Intent(context, RadioService::class.java)
|
val i = Intent(context, RadioService::class.java)
|
||||||
i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)
|
i.putExtra("action", Actions.PLAY_OR_FALLBACK.name)
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,9 @@ class MainActivity : BaseActivity() {
|
||||||
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
|
colorGreenListCompat = (ResourcesCompat.getColorStateList(resources, R.color.button_green_compat, null))
|
||||||
colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
|
colorAccent = (ResourcesCompat.getColor(resources, R.color.colorAccent, null))
|
||||||
|
|
||||||
|
PlayerStore.instance.initUrl(this)
|
||||||
|
PlayerStore.instance.initApi()
|
||||||
|
|
||||||
// Post-UI Launch
|
// Post-UI Launch
|
||||||
if (PlayerStore.instance.isInitialized)
|
if (PlayerStore.instance.isInitialized)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,27 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
private val titleObserver = Observer<String> {
|
private val titleObserver = Observer<String> {
|
||||||
// We're checking if a new song arrives. If so, we put the currentSong in Lp and update the backup.
|
// 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
|
if (PlayerStore.instance.currentSong != PlayerStore.instance.currentSongBackup
|
||||||
&& it != noConnectionValue)
|
&& it != noConnectionValue)
|
||||||
{
|
{
|
||||||
|
|
@ -163,6 +184,12 @@ class RadioService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
preferenceStore = PreferenceManager.getDefaultSharedPreferences(this)
|
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
|
// Define managers
|
||||||
telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
|
||||||
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
package fr.forum_thalie.tsumugi
|
package fr.forum_thalie.tsumugi
|
||||||
|
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
import fr.forum_thalie.tsumugi.playerstore.PlayerStore
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
class ApiFetchTick : TimerTask() {
|
||||||
|
override fun run() {
|
||||||
|
if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED)
|
||||||
|
{
|
||||||
|
PlayerStore.instance.fetchApi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Tick : TimerTask() {
|
class Tick : TimerTask() {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)
|
PlayerStore.instance.currentTime.postValue(PlayerStore.instance.currentTime.value!! + 500)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,16 @@ import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import fr.forum_thalie.tsumugi.*
|
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 {
|
class PlayerStore {
|
||||||
|
|
||||||
|
private lateinit var urlToScrape: String
|
||||||
val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
|
val isPlaying: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
|
val isServiceStarted: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
val volume: MutableLiveData<Int> = MutableLiveData()
|
val volume: MutableLiveData<Int> = MutableLiveData()
|
||||||
|
|
@ -27,6 +34,7 @@ class PlayerStore {
|
||||||
val listenersCount: MutableLiveData<Int> = MutableLiveData()
|
val listenersCount: MutableLiveData<Int> = MutableLiveData()
|
||||||
var latencyCompensator : Long = 0
|
var latencyCompensator : Long = 0
|
||||||
var isInitialized: Boolean = false
|
var isInitialized: Boolean = false
|
||||||
|
var isStreamDown: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackState.value = PlaybackStateCompat.STATE_STOPPED
|
playbackState.value = PlaybackStateCompat.STATE_STOPPED
|
||||||
|
|
@ -43,6 +51,112 @@ class PlayerStore {
|
||||||
listenersCount.value = 0
|
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 ##############
|
// ############## QUEUE / LP FUNCTIONS ##############
|
||||||
// ##################################################
|
// ##################################################
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@
|
||||||
android:progressDrawable="@drawable/progress_bar_progress"
|
android:progressDrawable="@drawable/progress_bar_progress"
|
||||||
app:layout_constraintBottom_toTopOf="@id/play_pause"
|
app:layout_constraintBottom_toTopOf="@id/play_pause"
|
||||||
tools:layout_editor_absoluteX="0dp"
|
tools:layout_editor_absoluteX="0dp"
|
||||||
android:visibility="gone"/>
|
android:visibility="visible"/>
|
||||||
|
|
||||||
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -318,7 +318,7 @@
|
||||||
android:textAlignment="textEnd"
|
android:textAlignment="textEnd"
|
||||||
app:layout_constraintEnd_toEndOf="@id/progressBar"
|
app:layout_constraintEnd_toEndOf="@id/progressBar"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
android:visibility="gone"/>
|
android:visibility="visible"/>
|
||||||
|
|
||||||
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
<!-- REMOVE VISIBILITY GONE IF YOU HAVE TIME VALUES TO DISPLAY THE PROGRESS BAR -->
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -331,7 +331,7 @@
|
||||||
android:textAlignment="textStart"
|
android:textAlignment="textStart"
|
||||||
app:layout_constraintStart_toStartOf="@id/progressBar"
|
app:layout_constraintStart_toStartOf="@id/progressBar"
|
||||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
android:visibility="gone"/>
|
android:visibility="visible"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
|
<string name="github_url_new_issue">https://github.com/yattoz/Tsumugi-app/issues/</string>
|
||||||
<string name="website_url">https://tsumugi.forum-thalie.fr/</string>
|
<string name="website_url">https://tsumugi.forum-thalie.fr/</string>
|
||||||
<string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
|
<string name="rss_url">https://tsumugi.forum-thalie.fr/?feed=rss2</string>
|
||||||
|
<string name="API_URL">https://radio.mahoro-net.org/airtime/api/live-info-v2</string>
|
||||||
<string name="planning_url">ADD SOME URL HERE</string>
|
<string name="planning_url">ADD SOME URL HERE</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue