Управление плеером с помощью голосовых команд
Умные устройства 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")
}
}
}
}
Описание формата состояния плеера
- Пример
- Описание
{
"playing": true,
"type": "video",
"live": false,
"duration": 200,
"position": 100,
"stateChangedTimestamp": 1432233446145000
}
Состояние плеера, которое надо сохранить в объекте appStateHolder
.
- если контент приостановлен — передается время постановки на паузу (в формате
timestamp
); - если контент воспроизводится — передается время включения плеера (в формате
timestamp
).
Указывает на то воспроизводится контент в данный момент или нет
Возможные значения: [video
, audio
]
какого типа контент играет
Указывает на потоковую трансляцию. Определяет возможность перемотки
Продолжительность контента в секундах. Не передается при потоковой трансляции (live == true
)
Текущая позиция в секундах. Не передается при потоковой трансляции (live == true
)
Время последнего изменения состояния плеера (поле playing
) в миллисекундах:
Описание формата команд управления плеером
- Пример
- Описание
{
"type": "player_command",
"player_command": {
"command": "PLAYER_REWIND",
"position": 115,
"app_info": {}
}
}
- PLAYER_REWIND
- PLAYER_CONTINUE
- PLAYER_LOOP
- PLAYER_NEXT
- PLAYER_PREV
- PLAYER_SHUFFLE
- PLAYER_STOP
- PLAYER_TO_START
Тип команды. Команда управления плеером на фронтенде смартапа
player_command
object
oneOf
Описание действия
Время на которое нужно перемотать трек в секундах
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Описание действия
Объект со служебными данными о смартапе
Поле position
, содержит время, на которое нужно перемотать трек, и может передаваться только в команде PLAYER_REWIND
.