Управление плеером с помощью голосовых команд
Умные устройства Sber поддерживают следующие команды управления воспроизведением аудио/видео:
«Продолжить»;
«Повторить»;
«Следующий/Предыдущий трек»;
«Случайный трек»;
«Остановить/Пауза»;
«Играть сначала»;
Команды перемотки:
«Перемотай назад/вперед на n секунд/минут/часов»;
«Перемотай»;
Перематывает вперед на 15 секунд.
«Перемотай на n».
Перематывает вперед на n минут.
Когда пользователь произносит одну из команд, операционная система устройства автоматически определяет приложение, к которому относится команда, и запрашивает у него информацию о состоянии плеера. После обработки состояния, операционная система устройства передает команду в приложение. Приложение изменяет состояние плеера соответствующим образом, например, останавливает плеер или перематывает трек.
Этот процесс можно представить так:
Таким образом, для поддержки голосового управления плеером, приложение должно:
- передавать состояние плеера при запросе ОС;
- обрабатывать команды, которые передает ОС.
Чтобы поддержать эту функциональность реализуйте инструменты Native App SDK.
Пример работы с голосовым управлением плеером
Рассмотрим как добавить поддержку голосового управления плеером в Android-приложение, созданное на основе стандартного шаблона Android Studio. Используя средства Native App SDK приложение передает состояние плеера в ОС и пишет логи при получении от ОС голосовых команд.
Пример MainActivity
package com.example.playercommanddemo
import android.os.Bundle
import android.util.Log
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import android.view.Menu
import android.view.MenuItem
import com.example.playercommanddemo.PlayerCommandEnum.*
import com.example.playercommanddemo.databinding.ActivityMainBinding
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.sberdevices.messaging.MessageId
import ru.sberdevices.messaging.Messaging
import ru.sberdevices.messaging.MessagingFactory
import ru.sberdevices.messaging.Payload
import ru.sberdevices.services.appstate.AppStateManagerFactory
import ru.sberdevices.services.appstate.AppStateProvider
import java.util.Date
class MainActivity : AppCompatActivity(), AppStateProvider {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
// Player commands
private val commandParser = Json {
ignoreUnknownKeys = true
isLenient = true
coerceInputValues = true
}
private var isPlaying = true
private var playingPosition = 10.0f
private val playingDuration = 100
private val listener = object : Messaging.Listener {
override fun onError(messageId: MessageId, throwable: Throwable) {
Log.d("DEMOAPP", "Got error ${throwable.message}")
}
override fun onMessage(messageId: MessageId, payload: Payload) {
Log.d("DEMOAPP", "Got ${payload.data}")
val playerCommand = commandParser.decodeFromString<PlayerCommand>(payload.data)
Log.d("DEMOAPP", "Got $playerCommand")
when (playerCommand.command) {
PLAYER_STOP -> {
isPlaying = false
Log.d("DEMOAPP", "Stopping play")
}
PLAYER_CONTINUE -> {
isPlaying = true
Log.d("DEMOAPP", "Resuming play")
}
PLAYER_NEXT -> {
Log.d("DEMOAPP", "Next track")
}
PLAYER_PREV -> {
Log.d("DEMOAPP", "Previous track")
}
PLAYER_TO_START -> {
Log.d("DEMOAPP", "Go to start")
playingPosition = 0f
}
PLAYER_REWIND -> {
Log.d("DEMOAPP", "Rewinding to position ${playerCommand.position}")
playerCommand.position?.let {
playingPosition = it
}
}
null -> {
Log.d("DEMOAPP", "Got unexpected command")
}
}
}
}
override fun getState(): String {
val state = Json.encodeToString(
AppState(
PlayerAppState(
playing = isPlaying,
duration = playingDuration,
position = playingPosition,
stateChangedTimestamp = Date().time,
type = MediaType.video,
live = false
)
)
)
Log.d("DEMOAPP", "State requested from app. Will give state: $state")
return state
}
private fun initSberSdk() {
val messaging = MessagingFactory.create(this)
messaging.addListener(listener)
// AppStateManager можно создавать только в единственном экземпляре на одно приложение
val requestManager = AppStateManagerFactory.createRequestManager(this)
// помимо activity можно использовать любой объект
requestManager.setProvider(this)
}
//////////////////// Default code ///////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initSberSdk()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
binding.fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
}
Подключение и инициализация Native App SDK
Так же как и другие зависимости, SDK подключается в разделе dependencies
файла app/build.gradle
:
dependencies {
implementation 'ru.sberdevices.smartapp:sdk:1.0.1'
...
}
Инициализируйте SDK в подходящем месте, например, в MainActivity
:
AppStateManager можно создавать только в единственном экземпляре на одно приложение.
private fun initSberSdk() {
val messaging = MessagingFactory.create(this)
messaging.addListener(listener)
val requestManager = AppStateManagerFactory.createRequestManager(this)
// помимо activity можно использовать любой объект
requestManager.setProvider(this)
}
Передача состояния плеера
Состояние плеера описывается согласно заданному формату.
Добавьте класс с описанием состояния плеера:
package com.example.playercommanddemo
import kotlinx.serialization.Serializable
@Serializable
data class AppState(
val player: PlayerAppState
)
@Serializable
data class PlayerAppState(
val playing: Boolean,
val type: MediaType,
val live: Boolean,
val duration: Int,
val position: Float,
val stateChangedTimestamp: Long
)
@Serializable
enum class MediaType {
audio, video
}
Для передачи состояния плеера при запросе используйте метод getState()
, интерфейса AppStateProvider
:
override fun getState(): String {
val state = Json.encodeToString(
AppState(
PlayerAppState(
playing = isPlaying,
duration = playingDuration,
position = playingPosition,
stateChangedTimestamp = Date().time,
type = MediaType.video,
live = false
)
)
)
Log.d("DEMOAPP", "State requested from app. Will give state: $state")
return state
}
Обработка команд управления плеером
Операционная система устройства возвращает команды в заданном формате.
Добавьте класс с перечислением команд, которые может передать ОС:
package com.example.playercommanddemo
import kotlinx.serialization.Serializable
/**
* @property position in seconds
* @property command [Entities.DirectiveId]
*/
@Serializable
data class PlayerCommand(
val command: PlayerCommandEnum? = null,
val position: Float? = null
)
enum class PlayerCommandEnum {
PLAYER_STOP,
PLAYER_CONTINUE,
PLAYER_NEXT,
PLAYER_PREV,
PLAYER_TO_START,
PLAYER_REWIND
}
Чтобы обработать команду управления плеером используйте метод onMessage()
слушателя библиотеки Messaging:
private val listener = object : Messaging.Listener {
override fun onError(messageId: MessageId, throwable: Throwable) {
Log.d("DEMOAPP", "Got error ${throwable.message}")
}
override fun onMessage(messageId: MessageId, payload: Payload) {
Log.d("DEMOAPP", "Got ${payload.data}")
val playerCommand = commandParser.decodeFromString<PlayerCommand>(payload.data)
Log.d("DEMOAPP", "Got $playerCommand")
when (playerCommand.command) {
PLAYER_STOP -> {
isPlaying = false
Log.d("DEMOAPP", "Stopping play")
}
PLAYER_CONTINUE -> {
isPlaying = true
Log.d("DEMOAPP", "Resuming play")
}
PLAYER_NEXT -> {
Log.d("DEMOAPP", "Next track")
}
PLAYER_PREV -> {
Log.d("DEMOAPP", "Previous track")
}
PLAYER_TO_START -> {
Log.d("DEMOAPP", "Go to start")
playingPosition = 0f
}
PLAYER_REWIND -> {
Log.d("DEMOAPP", "Rewinding to position ${playerCommand.position}")
playerCommand.position?.let {
playingPosition = it
}
}
null -> {
Log.d("DEMOAPP", "Got unexpected command")
}
}
}
}
Описание формата состояния плеера
- Пример
- Описание
Описание формата команд управления плеером
- Пример
- Описание
Поле position
, содержит время, на которое нужно перемотать трек, и может передаваться только в команде PLAYER_REWIND
.