Commit ee0fbdab authored by Daniel Sonck's avatar Daniel Sonck
Browse files

[THFMA-28][THFMA-27] Implement custom streaming host

- Add ability to set a custom streaming host address
- Changed over to elapsedRealtime for progress calculation
- Implemented widget progress
- Cleaned up useless code and comments
- Simplified play_pause_toggle drawable
- Removed settings_fragment layout in favor of settings.xml
- Completed dutch translation
parent 080b9c43
......@@ -51,6 +51,7 @@ dependencies {
implementation "androidx.media:media:1.1.0"
implementation "androidx.legacy:legacy-support-v4:$rootProject.legacySupportVersion"
implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerVersion"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "androidx.gridlayout:gridlayout:$rootProject.gridLayoutVersion"
......
......@@ -20,13 +20,12 @@
package fm.touhou.touhoufm.radio
import android.util.Log
import fm.touhou.touhoufm.service.players.RadioPlayer
import fm.touhou.touhoufm.utils.BasePacket
import fm.touhou.touhoufm.utils.OpusDecoder
import java.io.IOException
import java.net.*
class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: MetaListener, private val progressListener: ProgressListener) : Thread("RtpThread") {
class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: MetaListener, private val progressListener: ProgressListener, private var streamHost: String, private var streamPort: Int) : Thread("RtpThread") {
private var mLastSeqNum = 0
......@@ -34,8 +33,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
private var mOpusDecoder: OpusDecoder? = null
private var mLastHello: Long = 0
private var address: InetAddress? = null
private var mUdpImpl: UdpImpl? = null
init {
......@@ -57,11 +55,11 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
}
override fun run() {
try {
val udpImpl = UdpImpl(DatagramSocket())
val udpImpl = UdpImpl(DatagramSocket(), streamPort)
mUdpImpl = udpImpl
udpImpl.resolveHost(streamHost)
udpImpl.initUdpConn()
udpImpl.play()
udpImpl.clrUdpConn()
......@@ -74,18 +72,26 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
}
}
fun setStreamHost(streamHost: String) {
this.streamHost = streamHost
mUdpImpl?.resolveHost(this.streamHost)
}
fun setStreamPort(streamPort: Int) {
this.streamPort = streamPort
mUdpImpl?.setStreamPort(streamPort)
}
inner class UdpImpl(private val mRecvSocket: DatagramSocket) {
inner class UdpImpl(private val mRecvSocket: DatagramSocket, private var mStreamPort: Int) {
private var address: InetAddress? = null
private val addressLock = Object()
internal fun clrUdpConn() {
try {
// send mic off
val msg = "Bye"
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, RadioPlayer.STREAM_PORT)
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, synchronized(addressLock) { address }, synchronized(mStreamPort) {mStreamPort})
mRecvSocket.send(sendPacket)
} catch (e: IOException) {
Log.e(TAG, "Failed to clr udp conn", e)
......@@ -94,34 +100,40 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
}
@Throws(UnknownHostException::class)
private fun resolveHost() {
fun resolveHost(streamHost: String) {
var count = 8
while (address == null) {
try {
address = InetAddress.getByName(RadioPlayer.STREAM_HOST)
} catch (e: UnknownHostException) {
if (count < 0) {
address = null
throw e
} else {
try {
sleep(10000)
} catch (e1: InterruptedException) {
interrupt()
}
synchronized(addressLock) {
address = null
while (address == null) {
try {
address = InetAddress.getByName(streamHost)
} catch (e: UnknownHostException) {
if (count < 0) {
address = null
throw e
} else {
try {
sleep(10000)
} catch (e1: InterruptedException) {
interrupt()
}
}
}
}
count--
count--
}
}
}
@Throws(IOException::class)
private fun sendHello() {
val msg = "Hello"
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, RadioPlayer.STREAM_PORT)
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, synchronized(addressLock) { address }, synchronized(mStreamPort) {mStreamPort})
var retry = true
......@@ -152,12 +164,9 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
@Throws(UnknownHostException::class)
internal fun initUdpConn() {
// connect
resolveHost()
val msg = "Hello"
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, synchronized(addressLock) { address }, synchronized(mStreamPort) {mStreamPort})
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, RadioPlayer.STREAM_PORT)
try {
mRecvSocket.send(sendPacket)
} catch (e: IOException) {
......@@ -231,7 +240,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
if (rtpPacket.type == BasePacket.TYPE_META) {
val metaPacket = rtpPacket.toMeta()
metaListener.newMeta(metaPacket.field, metaPacket.contents?:"")
metaListener.newMeta(metaPacket.field, metaPacket.contents ?: "")
}
} else {
Log.w(TAG, "Invalid RTP packet received")
......@@ -269,6 +278,12 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
return recvPacket
}
fun setStreamPort(streamPort: Int) {
synchronized(mStreamPort) {
mStreamPort = streamPort
}
}
}
private fun shutDown() {
......
......@@ -35,21 +35,22 @@
package fm.touhou.touhoufm.service
import android.R
import android.app.Service
import android.appwidget.AppWidgetManager
import android.content.*
import android.os.Binder
import android.os.Handler
import android.os.IBinder
import android.os.Parcel
import androidx.core.content.ContextCompat
import android.os.Looper
import android.os.SystemClock.elapsedRealtime
import android.support.v4.media.MediaMetadataCompat
import androidx.media.session.MediaButtonReceiver
import android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.support.v4.media.session.PlaybackStateCompat.*
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.media.session.MediaButtonReceiver
import fm.touhou.touhoufm.service.notifications.MediaNotificationManager
import fm.touhou.touhoufm.service.players.MediaPlayerAdapter
import fm.touhou.touhoufm.utils.buildPkgString
......@@ -59,9 +60,12 @@ import fm.touhou.touhoufm.widget.ControlsWidget.Companion.ACTION_WIDGET_ENABLED
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.ACTION_WIDGET_INIT
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.EXTRA_MEDIA_METADATA
import fm.touhou.touhoufm.widget.ControlsWidget.Companion.EXTRA_PLAYBACK_STATE
import java.lang.System.currentTimeMillis
import kotlin.math.max
class MusicService : Service() {
private var mWidgetPositionTimestamp = System.currentTimeMillis()
private var widget = 0
private lateinit var mSession: MediaSessionCompat
private lateinit var mPlayback: PlayerAdapter
......@@ -86,6 +90,28 @@ class MusicService : Service() {
}
private val mProgressHandler = Handler(Looper.getMainLooper())
private val mProgressUpdater = object : Runnable {
fun onStart() {
mProgressHandler.postDelayed(this, max(50000,mPlayback.currentMedia?.getLong(METADATA_KEY_DURATION) ?: 50000)/50)
}
override fun run() {
if(mPlayback.playbackState.state == STATE_PLAYING) {
val pos = mPlayback.playbackState.run {
playbackSpeed * (elapsedRealtime() - lastPositionUpdateTime) + position
}
updateWidgetPosition(pos / (mPlayback.currentMedia?.getLong(METADATA_KEY_DURATION)
?: 1).toFloat())
}
onStart()
}
}
private fun updateWidget() {
if (widget != 0) {
updateWidgetState()
......@@ -103,6 +129,16 @@ class MusicService : Service() {
sendWidgetBroadcast(mediaIntent)
}
private fun updateWidgetPosition(pos: Float) {
val media = mPlayback.currentMedia ?: return
val timestamp = System.currentTimeMillis()
if(timestamp - mWidgetPositionTimestamp < media.getLong(METADATA_KEY_DURATION) / 50)
return
mWidgetPositionTimestamp = currentTimeMillis()
sendWidgetBroadcast(Intent(ControlsWidget.ACTION_WIDGET_UPDATE_POSITION).putExtra("position".buildPkgString(), pos))
}
private fun sendWidgetBroadcast(intent: Intent) {
intent.component = ComponentName(this, ControlsWidget::class.java)
sendBroadcast(intent)
......@@ -146,6 +182,9 @@ class MusicService : Service() {
}
registerReceiver(mReceiver, filter)
mProgressUpdater.onStart()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
......
......@@ -36,19 +36,22 @@
package fm.touhou.touhoufm.service.players
import android.content.Context
import android.content.SharedPreferences
import android.media.MediaPlayer
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.os.SystemClock.elapsedRealtime
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.preference.PreferenceManager.getDefaultSharedPreferences
import fm.touhou.touhoufm.R
import fm.touhou.touhoufm.radio.RtpReceiver
import fm.touhou.touhoufm.service.PlaybackInfoListener
import fm.touhou.touhoufm.service.PlayerAdapter
import fm.touhou.touhoufm.service.players.RadioPlayer.Companion.STREAM_HOST
import fm.touhou.touhoufm.service.players.RadioPlayer.Companion.STREAM_PORT
import fm.touhou.touhoufm.ui.MainActivity
import fm.touhou.touhoufm.utils.MetaPacket
import java.util.*
/**
* Exposes the functionality of the [MediaPlayer] and implements the [PlayerAdapter]
......@@ -60,7 +63,7 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
private var mMediaPlayer: RadioPlayer? = null
private var mCurrentMedia: MediaMetadataCompat? = null
private var mState: Int = 0
private var mSongStart = Date().time
private var mSongStart = elapsedRealtime()
private var mSongDuration: Long = 0
private var songTitle: String
......@@ -73,9 +76,9 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
override val playbackState: PlaybackStateCompat
get() = PlaybackStateCompat.Builder().setActions(availableActions).setState(mState,
Date().time - mSongStart,
elapsedRealtime() - mSongStart,
1.0f,
SystemClock.elapsedRealtime()).build()
elapsedRealtime()).build()
override val currentMedia: MediaMetadataCompat?
......@@ -103,6 +106,36 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
}
}
private val mSharedPreferenceChangeListener = object : SharedPreferences.OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
when (key) {
"stream_custom" -> {
val custom = prefs.getBoolean("stream_custom", false)
val host = if (custom) prefs.getString("stream_host", "strm.touhou.fm")
?: STREAM_HOST else STREAM_HOST
mMediaPlayer?.setStreamHost(host)
val port = if (custom) prefs.getString("stream_port", "1234")?.toInt()
?: STREAM_PORT else STREAM_PORT
mMediaPlayer?.setStreamPort(port)
}
"stream_host" -> {
val custom = prefs.getBoolean("stream_custom", false)
val host = if (custom) prefs.getString("stream_host", "strm.touhou.fm")
?: STREAM_HOST else STREAM_HOST
mMediaPlayer?.setStreamHost(host)
}
"stream_port" -> {
val custom = prefs.getBoolean("stream_custom", false)
val port = if (custom) prefs.getString("stream_port", "1234")?.toInt()
?: STREAM_PORT else STREAM_PORT
mMediaPlayer?.setStreamPort(port)
}
}
}
}
init {
songTitle = mContext.getString(R.string.unknown_song)
songAlbum = mContext.getString(R.string.unknown_album)
......@@ -121,6 +154,8 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, String.format(mContext.getString(R.string.artist_circle_format), songArtist, songCircle))
build()
}.also { media -> mPlaybackInfoListener.onMetadataChange(media) }
getDefaultSharedPreferences(mContext).registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener)
}
public override fun onStop() {
......@@ -138,14 +173,22 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
}
override fun onPlay() {
val prefs = getDefaultSharedPreferences(mContext)
val custom = prefs.getBoolean("stream_custom", false)
val host = if (custom) prefs.getString("stream_host", "strm.touhou.fm")
?: STREAM_HOST else STREAM_HOST
val port = if (custom) prefs.getString("stream_port", "1234")?.toInt()
?: STREAM_PORT else STREAM_PORT
mMediaPlayer.let {
if (it == null) {
mMediaPlayer = RadioPlayer(this, this).apply {
startMusic()
startMusic(host, port)
setNewState(PlaybackStateCompat.STATE_PLAYING)
}
} else {
it.startMusic()
it.startMusic(host, port)
setNewState(PlaybackStateCompat.STATE_PLAYING)
}
}
......@@ -193,7 +236,7 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
private fun queueNewState(@PlaybackStateCompat.State newPlayerState: Int) {
val updater = mMetadataUpdater
if(updater == null) {
if (updater == null) {
queueUpdateMetadata()
} else {
updater.setState(newPlayerState)
......@@ -218,8 +261,8 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
}
override fun newProgress(progress: Int) {
if (kotlin.math.abs(mSongStart - (Date().time - progress)) > 1000) {
mSongStart = Date().time - progress
if (kotlin.math.abs(mSongStart - (elapsedRealtime() - progress)) > 1000) {
mSongStart = elapsedRealtime() - progress
queueNewState(mState)
}
}
......
......@@ -40,25 +40,36 @@ class RadioPlayer(private var metaListener: RtpReceiver.MetaListener, private va
internal val isPlaying: Boolean
get() = mRingBuffer.let { b -> b?.isRunning ?: false }
internal fun startMusic() {
internal fun startMusic(streamHost: String, streamPort: Int) {
if (mRingBuffer == null) {
mRingBuffer = RingBuffer(mMinBufSize * 16, mMinBufSize * 2, FRAME_SIZE * 4)
.also {
mAudioPlayer = AndroidPlayer(it, mMinBufSize).also { player -> player.start() }
mRtpReceiver = RtpReceiver(it, metaListener, progressListener).also { receiver -> receiver.start() }
mRtpReceiver = RtpReceiver(it, metaListener, progressListener, streamHost, streamPort).also { receiver -> receiver.start() }
}
}
}
internal fun setStreamHost(host: String) = mRtpReceiver?.setStreamHost(host)
internal fun setStreamPort(port: Int) = mRtpReceiver?.setStreamPort(port)
internal fun stopMusic() {
mRingBuffer?.run {
abort()
mRingBuffer = null
}
mAudioPlayer = null
mRtpReceiver = null
mAudioPlayer?.run{
interrupt()
mAudioPlayer = null
}
mRtpReceiver?.run{
interrupt()
mRtpReceiver = null
}
}
internal fun setVolume(gain: Float) {
......
......@@ -35,16 +35,9 @@
package fm.touhou.touhoufm.ui
import android.content.res.Resources
import android.os.Bundle
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.drawerlayout.widget.DrawerLayout
......@@ -52,18 +45,12 @@ import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.*
import com.facebook.drawee.view.SimpleDraweeView
import com.google.android.material.navigation.NavigationView
import fm.touhou.touhoufm.R
import fm.touhou.touhoufm.client.MediaBrowserAdapter
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration : AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.navigation_activity)
......@@ -83,41 +70,18 @@ class MainActivity : AppCompatActivity() {
setupActionBar(navController, appBarConfiguration)
setupNavigationMenu(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
val dest: String = try {
resources.getResourceName(destination.id)
} catch (e: Resources.NotFoundException ) {
Integer.toString(destination.id)
}
Toast.makeText(this@MainActivity, "Navigated to $dest",
Toast.LENGTH_SHORT).show()
Log.d("NavigationActivity", "Navigated to $dest")
}
}
private fun setupNavigationMenu(navController: NavController) {
// TODO STEP 9.4 - Use NavigationUI to set up a Navigation View
// In split screen mode, you can drag this view out from the left
// This does NOT modify the actionbar
val sideNavView = findViewById<NavigationView>(R.id.nav_view)
sideNavView.setupWithNavController(navController)
sideNavView.inflateHeaderView(R.layout.nav_drawer_header)
// TODO END STEP 9.4
}
private fun setupActionBar(navController: NavController,
appBarConfig : AppBarConfiguration) {
// TODO STEP 9.6 - Have NavigationUI handle what your ActionBar displays
// This allows NavigationUI to decide what label to show in the action bar
// By using appBarConfig, it will also determine whether to
// show the up arrow or drawer menu icon
setupActionBarWithNavController(navController, appBarConfig)
// TODO END STEP 9.6
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
......@@ -133,23 +97,14 @@ class MainActivity : AppCompatActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// return super.onOptionsItemSelected(item)
// TODO STEP 9.2 - Have Navigation UI Handle the item selection - make sure to delete
// the old return statement above
// Have the NavigationUI look for an action or destination matching the menu
// item id and navigate there if found.
// Otherwise, bubble up to the parent.
return item.onNavDestinationSelected(findNavController(R.id.my_nav_host_fragment))
|| super.onOptionsItemSelected(item)
// TODO END STEP 9.2
}
// TODO STEP 9.7 - Have NavigationUI handle up behavior in the ActionBar
override fun onSupportNavigateUp(): Boolean {
// Allows NavigationUI to support proper up navigation or the drawer layout
// drawer menu, depending on the situation
return findNavController(R.id.my_nav_host_fragment).navigateUp(appBarConfiguration)
}
// TODO END STEP 9.7
}
......@@ -43,10 +43,8 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.AttributeSet
import android.view.animation.LinearInterpolator
import android.widget.ProgressBar
import java.util.Locale
import fm.touhou.touhoufm.R
import java.util.*
/**
* SeekBar that can be used with a [MediaSessionCompat] to track and seek in playing
......@@ -69,8 +67,8 @@ class MediaSeekBar : ProgressBar, ValueAnimator.AnimatorUpdateListener {
super.setProgress(progress)
val max = max
mTimeCallback?.invoke(
String.format(Locale.getDefault(), context.getString(R.string.time_format), progress / 10 % 60, progress / 10 / 60),
String.format(Locale.getDefault(), context.getString(R.string.time_format), max / 10 % 60, max / 10 / 60)
String.format(Locale.getDefault(), context.getString(R.string.time_format), progress % 60, progress / 60),
String.format(Locale.getDefault(), context.getString(R.string.time_format), max % 60, max / 60)
)
}
......@@ -78,8 +76,8 @@ class MediaSeekBar : ProgressBar, ValueAnimator.AnimatorUpdateListener {
super.setMax(max)
val progress = progress
mTimeCallback?.invoke(
String.format(Locale.getDefault(), context.getString(R.string.time_format), progress / 10 % 60, progress / 10 / 60),
String.format(Locale.getDefault(), context.getString(R.string.time_format), max / 10 % 60, max / 10 / 60)
String.format(Locale.getDefault(), context.getString(R.string.time_format), progress % 60, progress / 60),
String.format(Locale.getDefault(), context.getString(R.string.time_format), max % 60, max / 60)
)
}
......@@ -97,7 +95,7 @@ class MediaSeekBar : ProgressBar, ValueAnimator.AnimatorUpdateListener {
}
if (state != null && state.state == PlaybackStateCompat.STATE_PLAYING) {
val progress = state.position.toInt() / 100
val progress = state.position.toInt() / 1000
setProgress(progress)
// If the media is playing then the seekbar should follow it, and the easiest