Commit 5c11dd2d authored by Daniel Sonck's avatar Daniel Sonck
Browse files

Pull request #48: Add V2 protocol

Merge in THFM/fm.touhou.touhoufm from devel/dsonck to development

* commit '6c93b569':
  Fix tests and update gradle
  Add V2 protocol
parents 48532b7c 6c93b569
...@@ -8,7 +8,7 @@ buildscript { ...@@ -8,7 +8,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.android.tools.build:gradle:4.0.1'
} }
} }
...@@ -45,7 +45,7 @@ dependencies { ...@@ -45,7 +45,7 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'org.java-websocket:Java-WebSocket:1.3.7' implementation 'org.java-websocket:Java-WebSocket:1.3.7'
implementation 'com.facebook.fresco:fresco:1.8.0' implementation 'com.facebook.fresco:fresco:2.1.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.google.android.material:material:$rootProject.materialVersion" implementation "com.google.android.material:material:$rootProject.materialVersion"
...@@ -79,6 +79,7 @@ List<String> dirs = [ ...@@ -79,6 +79,7 @@ List<String> dirs = [
android { android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 14
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application <application
android:name=".TouHouFM" android:name=".TouHouFM"
...@@ -16,7 +17,8 @@ ...@@ -16,7 +17,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:extractNativeLibs="false"> android:extractNativeLibs="false"
tools:targetApi="m">
<activity <activity
android:name=".ui.MainActivity" android:name=".ui.MainActivity"
android:launchMode="singleTop"> android:launchMode="singleTop">
......
...@@ -81,7 +81,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -81,7 +81,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
} }
inner class UdpImpl(private val mRecvSocket: DatagramSocket, private var mStreamHost: String, private var mStreamPort: Int) { inner class UdpImpl(private val mReceiveSocket: DatagramSocket, private var mStreamHost: String, private var mStreamPort: Int) {
private var mAddressCache: InetAddress? = null private var mAddressCache: InetAddress? = null
private val mAddressLock = Object() private val mAddressLock = Object()
...@@ -126,10 +126,14 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -126,10 +126,14 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
mLastSeqNum = sequenceNumber mLastSeqNum = sequenceNumber
// Decode the opus data into pcm frames // Decode the opus data into pcm frames
val samps = mOpusDecoder?.decode(opus, mPcmFrames) ?: 0 val samples = mOpusDecoder?.decode(opus, mPcmFrames) ?: 0
if(samples < 0) {
throw IOException("Opus Decoding error $samples")
}
// Write the audio data to the buffer // Write the audio data to the buffer
if (mRingBuffer.write(mPcmFrames, samps * 2) < 0) { if (mRingBuffer.write(mPcmFrames, samples * 2) < 0) {
// If the write returns -1, this means the buffer is aborted and we should stop // If the write returns -1, this means the buffer is aborted and we should stop
interrupt() interrupt()
} }
...@@ -141,6 +145,10 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -141,6 +145,10 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
metaListener.newMeta(packet.field, packet.contents ?: "") metaListener.newMeta(packet.field, packet.contents ?: "")
} }
override fun onMetaListPacket(base: BasePacket, packet: MetaListPacket) {
metaListener.newMeta(packet.meta)
}
override fun onUnknownPacket(base: BasePacket) { override fun onUnknownPacket(base: BasePacket) {
Log.w(TAG, "Unknown packet") Log.w(TAG, "Unknown packet")
} }
...@@ -151,7 +159,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -151,7 +159,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
// send mic off // send mic off
val msg = "Bye" val msg = "Bye"
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, port) val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, port)
mRecvSocket.send(sendPacket) mReceiveSocket.send(sendPacket)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Failed to clr udp conn", e) Log.e(TAG, "Failed to clr udp conn", e)
} }
...@@ -193,7 +201,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -193,7 +201,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
do { do {
try { try {
mRecvSocket.send(sendPacket) mReceiveSocket.send(sendPacket)
mLastHello = System.currentTimeMillis() mLastHello = System.currentTimeMillis()
retry = false retry = false
Log.d(TAG, "sending hello packet") Log.d(TAG, "sending hello packet")
...@@ -222,7 +230,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -222,7 +230,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, port) val sendPacket = DatagramPacket(msg.toByteArray(), msg.length, address, port)
try { try {
mRecvSocket.send(sendPacket) mReceiveSocket.send(sendPacket)
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Failed to send hello", e) Log.e(TAG, "Failed to send hello", e)
} }
...@@ -263,14 +271,14 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -263,14 +271,14 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
@Throws(IOException::class) @Throws(IOException::class)
private fun receiveDatagramPacket(): DatagramPacket { private fun receiveDatagramPacket(): DatagramPacket {
val message = ByteArray(OPUS_SIZE + RTP_SIZE) val message = ByteArray(3200)
val recvPacket = DatagramPacket(message, message.size) val recvPacket = DatagramPacket(message, message.size)
try { try {
// Receive the packet // Receive the packet
mRecvSocket.soTimeout = SAMPLE_LEN * 8 mReceiveSocket.soTimeout = SAMPLE_LEN * 8
mRecvSocket.receive(recvPacket) mReceiveSocket.receive(recvPacket)
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
sendHello() sendHello()
...@@ -307,6 +315,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -307,6 +315,7 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
interface MetaListener { interface MetaListener {
fun newMeta(field: Int, value: String) fun newMeta(field: Int, value: String)
fun newMeta(meta: List<MetaItem>)
} }
interface ProgressListener { interface ProgressListener {
...@@ -321,12 +330,8 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener: ...@@ -321,12 +330,8 @@ class RtpReceiver(private val mRingBuffer: RingBuffer, private val metaListener:
const val SAMPLE_RATE = 48000 const val SAMPLE_RATE = 48000
const val FRAME_SIZE = SAMPLE_RATE * SAMPLE_LEN / 1000 const val FRAME_SIZE = SAMPLE_RATE * SAMPLE_LEN / 1000
private const val BITRATE = 320000
private const val OPUS_SIZE = SAMPLE_LEN * BITRATE / 8 / 1000
private const val BUF_SIZE = FRAME_SIZE * 2 private const val BUF_SIZE = FRAME_SIZE * 2
private const val RTP_SIZE = 16
} }
} }
...@@ -51,6 +51,7 @@ import fm.touhou.touhoufm.service.PlayerAdapter ...@@ -51,6 +51,7 @@ 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_HOST
import fm.touhou.touhoufm.service.players.RadioPlayer.Companion.STREAM_PORT import fm.touhou.touhoufm.service.players.RadioPlayer.Companion.STREAM_PORT
import fm.touhou.touhoufm.ui.MainActivity import fm.touhou.touhoufm.ui.MainActivity
import fm.touhou.touhoufm.utils.MetaItem
import fm.touhou.touhoufm.utils.MetaPacket import fm.touhou.touhoufm.utils.MetaPacket
/** /**
...@@ -223,6 +224,18 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf ...@@ -223,6 +224,18 @@ class MediaPlayerAdapter(private val mContext: Context, private val mPlaybackInf
} }
override fun newMeta(meta: List<MetaItem>) {
for (metaItem in meta)
metaItem.contents?.let { value ->
when (metaItem.field) {
MetaItem.FIELD_SONG_TITLE -> songTitle = value
MetaItem.FIELD_ALBUM_TITLE -> songAlbum = value
MetaItem.FIELD_SONG_ARTIST -> songArtist = value
MetaItem.FIELD_ALBUM_ARTIST -> songCircle = value
}
}
}
private fun queueUpdateMetadata() { private fun queueUpdateMetadata() {
if (mMetadataUpdater == null) { if (mMetadataUpdater == null) {
mMetadataUpdater = MetadataUpdater(mState).also { updater -> mMetadataUpdater = MetadataUpdater(mState).also { updater ->
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
package fm.touhou.touhoufm.ui package fm.touhou.touhoufm.ui
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
...@@ -72,7 +71,7 @@ class MainActivity : AppCompatActivity() { ...@@ -72,7 +71,7 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
queue = Volley.newRequestQueue(this) queue = Volley.newRequestQueue(this)
val prefs = getSharedPreferences("AppDetails", Context.MODE_PRIVATE); val prefs = getSharedPreferences("AppDetails", Context.MODE_PRIVATE)
setContentView(R.layout.navigation_activity) setContentView(R.layout.navigation_activity)
...@@ -99,14 +98,14 @@ class MainActivity : AppCompatActivity() { ...@@ -99,14 +98,14 @@ class MainActivity : AppCompatActivity() {
val jsonRequest = JsonObjectRequest("https://www.touhou.fm/app-release.json", null, val jsonRequest = JsonObjectRequest("https://www.touhou.fm/app-release.json", null,
Response.Listener { response: JSONObject -> Response.Listener { response: JSONObject ->
val supported: Boolean; val supported: Boolean
when { when {
appVersion < response.getInt("supported") -> { appVersion < response.getInt("supported") -> {
supported = false; supported = false
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Unsupported app version") .setTitle("Unsupported app version")
.setMessage("This version of the app is currently unsupported, please download the new version from the play store") .setMessage("This version of the app is currently unsupported, please download the new version from the play store")
.setPositiveButton("Download", DialogInterface.OnClickListener { dialogInterface, i -> .setPositiveButton("Download") { _, _ ->
val appPackageName = packageName val appPackageName = packageName
try { try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName"))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")))
...@@ -114,20 +113,20 @@ class MainActivity : AppCompatActivity() { ...@@ -114,20 +113,20 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName"))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName")))
} }
}) }
.setNegativeButton("Quit", DialogInterface.OnClickListener { dialogInterface, i -> .setNegativeButton("Quit") { _, _ ->
this@MainActivity.finish() this@MainActivity.finish()
}) }
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(android.R.drawable.ic_dialog_alert)
.show(); .show()
} }
appVersion < response.getInt("beta") - 1 -> { appVersion < response.getInt("beta") - 1 -> {
supported = true; supported = true
if (lastVersion != appVersion || lastKnownVersion != response.getInt("beta") - 1) { if (lastVersion != appVersion || lastKnownVersion != response.getInt("beta") - 1) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("New version") .setTitle("New version")
.setMessage("A new version is available in the play store") .setMessage("A new version is available in the play store")
.setPositiveButton("Download", DialogInterface.OnClickListener { dialogInterface, i -> .setPositiveButton("Download") { _, _ ->
val appPackageName = packageName val appPackageName = packageName
try { try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName"))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")))
...@@ -135,18 +134,18 @@ class MainActivity : AppCompatActivity() { ...@@ -135,18 +134,18 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName"))) startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName")))
} }
}) }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setIcon(android.R.drawable.ic_dialog_info) .setIcon(android.R.drawable.ic_dialog_info)
.show(); .show()
} }
} }
appVersion < response.getInt("alpha") -> { appVersion < response.getInt("alpha") -> {
supported = true; supported = true
Toast.makeText(this, "Thanks for being a beta tester", LENGTH_LONG).show() Toast.makeText(this, "Thanks for being a beta tester", LENGTH_LONG).show()
} }
else -> { else -> {
supported = true; supported = true
Toast.makeText(this, "Thanks for being an alpha tester", LENGTH_LONG).show() Toast.makeText(this, "Thanks for being an alpha tester", LENGTH_LONG).show()
} }
} }
...@@ -155,10 +154,10 @@ class MainActivity : AppCompatActivity() { ...@@ -155,10 +154,10 @@ class MainActivity : AppCompatActivity() {
Changelog.createDialog(this, versionCode = lastVersion).show() Changelog.createDialog(this, versionCode = lastVersion).show()
} }
}, Response.ErrorListener { }, Response.ErrorListener {
Toast.makeText(this, "Failed to check latest version", Toast.LENGTH_LONG).show(); Toast.makeText(this, "Failed to check latest version", LENGTH_LONG).show()
}) })
queue.add(jsonRequest); queue.add(jsonRequest)
} }
......
package fm.touhou.touhoufm.utils package fm.touhou.touhoufm.utils
import fm.touhou.touhoufm.utils.base_packet.BasePacketV1 import fm.touhou.touhoufm.utils.base_packet.BasePacketV1
import fm.touhou.touhoufm.utils.base_packet.BasePacketV2
import java.io.IOException import java.io.IOException
import java.net.DatagramPacket import java.net.DatagramPacket
import java.nio.ByteBuffer import java.nio.ByteBuffer
...@@ -16,7 +17,9 @@ interface BasePacket { ...@@ -16,7 +17,9 @@ interface BasePacket {
internal const val HEADER_SIZE = 4 internal const val HEADER_SIZE = 4
const val VERSION_1_0 = 0 const val VERSION_1_0 = 0
const val VERSION_1_1 = 1
@Suppress("unused")
const val SYSTEM_SESSION = 0 const val SYSTEM_SESSION = 0
const val SYSTEM_AUDIO = 1 const val SYSTEM_AUDIO = 1
...@@ -24,10 +27,6 @@ interface BasePacket { ...@@ -24,10 +27,6 @@ interface BasePacket {
const val TYPE_AUDIO_OPUS = 1 const val TYPE_AUDIO_OPUS = 1
const val TYPE_META = 2 const val TYPE_META = 2
private fun unsignedInt(i: Int): Int {
return if (i >= 0) i else 256 + i
}
fun decode(packet: ByteArray, length: Int, packetReceiver: PacketReceiver): BasePacket { fun decode(packet: ByteArray, length: Int, packetReceiver: PacketReceiver): BasePacket {
if (min(packet.size, length) < HEADER_SIZE) if (min(packet.size, length) < HEADER_SIZE)
throw InvalidBasePacket() throw InvalidBasePacket()
...@@ -37,24 +36,23 @@ interface BasePacket { ...@@ -37,24 +36,23 @@ interface BasePacket {
val system: Int val system: Int
buffer.get().toInt().let { buffer.get().toInt().let {
version = it shr 4 version = it ushr 4
system = it and 0x0F system = it and 0x0F
} }
when(version) { return when(version) {
VERSION_1_0 -> return BasePacketV1(system, buffer, packetReceiver) VERSION_1_0 -> BasePacketV1(system, buffer, packetReceiver)
VERSION_1_1 -> BasePacketV2(system, buffer, packetReceiver)
else -> throw InvalidBasePacket() else -> throw InvalidBasePacket()
} }
} }
fun encode(bb: ByteBuffer) { fun encode() {
} }
} }
class InvalidBasePacket : IOException() { class InvalidBasePacket : IOException()
}
} }
......
package fm.touhou.touhoufm.utils
interface MetaItem {
val field: String
val contents: String?
companion object {
const val FIELD_SONG_TITLE = "song"
const val FIELD_ALBUM_TITLE = "album"
const val FIELD_SONG_ARTIST = "artist"
const val FIELD_ALBUM_ARTIST = "circle"
}
}
\ No newline at end of file
package fm.touhou.touhoufm.utils
interface MetaListPacket {
val meta: List<MetaItem>
}
\ No newline at end of file
...@@ -5,5 +5,7 @@ interface PacketReceiver { ...@@ -5,5 +5,7 @@ interface PacketReceiver {
fun onMetaPacket(base: BasePacket, packet: MetaPacket) fun onMetaPacket(base: BasePacket, packet: MetaPacket)
fun onMetaListPacket(base: BasePacket, packet: MetaListPacket)
fun onUnknownPacket(base: BasePacket) fun onUnknownPacket(base: BasePacket)
} }
...@@ -12,16 +12,15 @@ class AudioPacketV1(bb: ByteBuffer) : AudioPacket { ...@@ -12,16 +12,15 @@ class AudioPacketV1(bb: ByteBuffer) : AudioPacket {
override val timestamp: Long = bb.long override val timestamp: Long = bb.long
override val position: Int = bb.int override val position: Int = bb.int
override val length: Int = bb.int override val length: Int = bb.int
override val audio = ByteArray(bb.limit() - bb.position()) override val audio = let {
val audio = ByteArray(bb.limit() - bb.position())
init {
bb.get(audio) bb.get(audio)
audio
} }
companion object { companion object {
private const val HEADER_SIZE = 16 private const val HEADER_SIZE = 16
} }
} }
\ No newline at end of file
package fm.touhou.touhoufm.utils.base_packet
import fm.touhou.touhoufm.utils.BasePacket
import fm.touhou.touhoufm.utils.BasePacket.Companion.HEADER_SIZE
import fm.touhou.touhoufm.utils.PacketReceiver
import fm.touhou.touhoufm.utils.audio_packet.AudioPacketV1
import fm.touhou.touhoufm.utils.meta_list_packet.MetaPacketV2
import java.nio.ByteBuffer
class BasePacketV2(override val system: Int, bb: ByteBuffer, packetReceiver: PacketReceiver) : BasePacket {
init {
if(bb.limit() - bb.position() < HEADER_SIZE - 1) {
throw BasePacket.InvalidBasePacket()
}
}
override val version: Int = BasePacket.VERSION_1_1
override val type: Int = bb.get().toInt()
override val sequenceNumber: Int = bb.short.toInt()
init {
when (type) {
BasePacket.TYPE_AUDIO_OPUS -> {
packetReceiver.onAudioPacket(this, AudioPacketV1(bb))
}
BasePacket.TYPE_META -> packetReceiver.onMetaListPacket(this, MetaPacketV2(bb))
else -> packetReceiver.onUnknownPacket(this)
}
}
}
\ No newline at end of file
package fm.touhou.touhoufm.utils.meta_item
import fm.touhou.touhoufm.utils.MetaItem