Enable streaming TTS to Sonos
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.bartek">
|
||||
|
||||
<permission
|
||||
@@ -8,6 +9,7 @@
|
||||
android:label="@string/permission_http_server_label"
|
||||
android:protectionLevel="normal" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -20,7 +22,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="m">
|
||||
<activity
|
||||
android:name="io.bartek.ttsserver.help.HelpActivity"
|
||||
android:parentActivityName=".ttsserver.MainActivity" />
|
||||
|
||||
22
app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt
Normal file
22
app/src/main/java/io/bartek/ttsserver/network/NetworkUtil.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package io.bartek.ttsserver.network
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.WIFI_SERVICE
|
||||
import android.net.wifi.WifiManager
|
||||
import java.net.InetAddress
|
||||
|
||||
|
||||
object NetworkUtil {
|
||||
fun getIpAddress(context: Context): String {
|
||||
return (context.getApplicationContext().getSystemService(WIFI_SERVICE) as WifiManager).let {
|
||||
inetAddress(it.dhcpInfo.ipAddress).toString().substring(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inetAddress(hostAddress: Int) = byteArrayOf(
|
||||
(0xff and hostAddress).toByte(),
|
||||
(0xff and (hostAddress shr 8)).toByte(),
|
||||
(0xff and (hostAddress shr 16)).toByte(),
|
||||
(0xff and (hostAddress shr 24)).toByte()
|
||||
).let { InetAddress.getByAddress(it) }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.bartek.ttsserver.sonos
|
||||
|
||||
import com.vmichalak.sonoscontroller.SonosDiscovery
|
||||
import java.io.File
|
||||
|
||||
class SonosController(private val host: String, private val port: Int) {
|
||||
fun clip(wave: File, zone: String, volume: Int) {
|
||||
SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == zone }?.let {
|
||||
val filename = wave.name
|
||||
val url = "http://$host:$port/sonos/$filename"
|
||||
val currentVolume = it.volume
|
||||
it.volume = volume
|
||||
it.clip(url, "")
|
||||
it.volume = currentVolume
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.bartek.ttsserver.web
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoHTTPD.ResponseException
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
data class SonosTTSRequestData(val text: String, val language: Locale, val zone: String, val volume: Int) {
|
||||
companion object {
|
||||
fun fromJSON(json: String): SonosTTSRequestData {
|
||||
val root = JSONObject(json)
|
||||
|
||||
val language = root.optString("language")
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { Locale(it) }
|
||||
?: Locale.US
|
||||
val text = root.optString("text") ?: throw ResponseException(
|
||||
NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
""
|
||||
)
|
||||
val zone = root.optString("zone") ?: throw ResponseException(
|
||||
NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
""
|
||||
)
|
||||
val volume = root.optInt("volume", 50)
|
||||
|
||||
return SonosTTSRequestData(text, language, zone, volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
25
app/src/main/java/io/bartek/ttsserver/web/TTSRequestData.kt
Normal file
25
app/src/main/java/io/bartek/ttsserver/web/TTSRequestData.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package io.bartek.ttsserver.web
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoHTTPD.ResponseException
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
data class TTSRequestData(val text: String, val language: Locale) {
|
||||
companion object {
|
||||
fun fromJSON(json: String): TTSRequestData {
|
||||
val root = JSONObject(json)
|
||||
|
||||
val language = root.optString("language")
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { Locale(it) }
|
||||
?: Locale.US
|
||||
val text = root.optString("text") ?: throw ResponseException(
|
||||
NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
""
|
||||
)
|
||||
|
||||
return TTSRequestData(text, language)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,15 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.preference.PreferenceManager
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status.*
|
||||
import io.bartek.ttsserver.network.NetworkUtil
|
||||
import io.bartek.ttsserver.preference.PreferenceKey
|
||||
import io.bartek.ttsserver.service.ForegroundService
|
||||
import io.bartek.ttsserver.service.ServiceState
|
||||
import io.bartek.ttsserver.sonos.SonosController
|
||||
import io.bartek.ttsserver.tts.TTS
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.*
|
||||
|
||||
private data class TTSRequestData(val text: String, val language: Locale)
|
||||
|
||||
|
||||
class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
|
||||
@@ -26,6 +24,7 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val tts = TTS(context, this)
|
||||
private val endpoints = Endpoints()
|
||||
private val sonos = SonosController(NetworkUtil.getIpAddress(context), port)
|
||||
|
||||
override fun serve(session: IHTTPSession?): Response {
|
||||
try {
|
||||
@@ -47,9 +46,10 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
|
||||
}
|
||||
}
|
||||
|
||||
private fun sonos(session: IHTTPSession): Response {
|
||||
// SonosDiscovery.discover().firstOrNull { it.zoneGroupState.name == "Salon" }
|
||||
// ?.play()
|
||||
private fun say(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
@@ -59,9 +59,52 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
}
|
||||
|
||||
val (text, language) = getRequestData(session)
|
||||
val (text, language) = extractBody(session) { TTSRequestData.fromJSON(it) }
|
||||
|
||||
tts.performTTS(text, language)
|
||||
return newFixedLengthResponse(OK, MIME_PLAINTEXT, "")
|
||||
}
|
||||
|
||||
private fun <T> extractBody(session: IHTTPSession, provider: (String) -> T): T {
|
||||
return mutableMapOf<String, String>().let {
|
||||
session.parseBody(it)
|
||||
provider(it["postData"] ?: "{}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun wave(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
}
|
||||
|
||||
val (text, language) = extractBody(session) { TTSRequestData.fromJSON(it) }
|
||||
|
||||
val (stream, size) = tts.fetchTTSStream(text, language)
|
||||
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
|
||||
}
|
||||
|
||||
private fun sonos(session: IHTTPSession): Response {
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
}
|
||||
|
||||
val (text, language, zone, volume) = extractBody(session) { SonosTTSRequestData.fromJSON(it) }
|
||||
|
||||
val file = tts.createTTSFile(text, language)
|
||||
return newFixedLengthResponse(file.toString())
|
||||
sonos.clip(file, zone, volume)
|
||||
return newFixedLengthResponse("")
|
||||
}
|
||||
|
||||
private fun sonosCache(session: IHTTPSession): Response {
|
||||
@@ -81,54 +124,6 @@ class WebServer(port: Int, private val context: Context) : NanoHTTPD(port),
|
||||
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
|
||||
}
|
||||
|
||||
private fun wave(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
}
|
||||
|
||||
val (text, language) = getRequestData(session)
|
||||
val (stream, size) = tts.fetchTTSStream(text, language)
|
||||
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
|
||||
}
|
||||
|
||||
private fun say(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
}
|
||||
|
||||
val (text, language) = getRequestData(session)
|
||||
tts.performTTS(text, language)
|
||||
return newFixedLengthResponse(OK, MIME_PLAINTEXT, "")
|
||||
}
|
||||
|
||||
private fun getRequestData(session: IHTTPSession): TTSRequestData {
|
||||
val map = mutableMapOf<String, String>()
|
||||
session.parseBody(map)
|
||||
val json = JSONObject(map["postData"] ?: "{}")
|
||||
val language = json.optString("language")
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { Locale(it) }
|
||||
?: Locale.US
|
||||
val text = json.optString("text") ?: throw ResponseException(BAD_REQUEST, "")
|
||||
return TTSRequestData(text, language)
|
||||
}
|
||||
|
||||
override fun onInit(status: Int) = start()
|
||||
|
||||
override fun start() {
|
||||
|
||||
Reference in New Issue
Block a user