Support Pulseaudio sink-inputs

This commit is contained in:
nemunaire 2023-11-13 20:00:28 +01:00
parent a9c6cdcd0f
commit bbde2299fe
8 changed files with 145 additions and 10 deletions

View File

@ -57,14 +57,18 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
c.JSON(http.StatusOK, src.CurrentlyPlaying())
})
inputsRoutes.POST("/pause", func(c *gin.Context) {
streamRoutes := inputsRoutes.Group("/stream/:stream")
streamRoutes.Use(StreamHandler)
streamRoutes.POST("/pause", func(c *gin.Context) {
input, ok := c.MustGet("input").(inputs.ControlableInput)
if !ok {
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
return
}
err := input.TogglePause()
err := input.TogglePause(c.MustGet("streamid").(string))
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to pause the input: %s", err.Error())})
return
@ -85,3 +89,20 @@ func InputHandler(c *gin.Context) {
c.Next()
}
func StreamHandler(c *gin.Context) {
input := c.MustGet("input").(inputs.SoundInput)
streams := input.CurrentlyPlaying()
stream, ok := streams[c.Param("stream")]
if !ok {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Stream not found: %s", c.Param("stream"))})
return
}
c.Set("streamid", c.Param("stream"))
c.Set("stream", stream)
c.Next()
}

View File

@ -122,7 +122,7 @@ func declareSourcesRoutes(cfg *config.Config, router *gin.RouterGroup) {
return
}
c.JSON(http.StatusOK, s.TogglePause())
c.JSON(http.StatusOK, s.TogglePause("default"))
})
}

1
app.go
View File

@ -38,7 +38,6 @@ func NewApp(cfg *config.Config) *App {
// Register routes
ui.DeclareRoutes(router, cfg)
ui.DeclareNoJSRoutes(router, cfg)
api.DeclareRoutes(router, cfg)
router.GET("/api/version", func(c *gin.Context) {

View File

@ -7,9 +7,9 @@ var SoundInputs = map[string]SoundInput{}
type SoundInput interface {
GetName() string
IsActive() bool
CurrentlyPlaying() *string
CurrentlyPlaying() map[string]string
}
type ControlableInput interface {
TogglePause() error
TogglePause(string) error
}

View File

@ -103,7 +103,7 @@ func (i *MPRISInput) IsActive() bool {
return err == nil
}
func (i *MPRISInput) CurrentlyPlaying() *string {
func (i *MPRISInput) CurrentlyPlaying() map[string]string {
p, err := i.getPlayer()
if err != nil || p == nil {
log.Println(err)
@ -129,10 +129,12 @@ func (i *MPRISInput) CurrentlyPlaying() *string {
}
ret := strings.Join(infos, " - ")
return &ret
return map[string]string{
"default": ret,
}
}
func (i *MPRISInput) TogglePause() error {
func (i *MPRISInput) TogglePause(id string) error {
p, err := i.getPlayer()
if err != nil {
return err

112
inputs/pulseaudio/input.go Normal file
View File

@ -0,0 +1,112 @@
package pa
import (
"encoding/json"
"fmt"
"log"
"os/exec"
"strconv"
"git.nemunai.re/nemunaire/hathoris/inputs"
)
type PulseaudioInput struct {
}
type PASink struct {
Index int `json:"index"`
Name string `json:"name"`
Driver string `json:"driver"`
SampleSpecification string `json:"sample_specifications"`
State string `json:"state"`
}
type PAVolume struct {
Value uint `json:"value"`
ValuePercent string `json:"value_percent"`
DB string `json:"db"`
}
type PASinkInput struct {
Index int64 `json:"index"`
Driver string `json:"driver"`
OwnerModule string `json:"owner_module"`
Client string `json:"client"`
Sink int `json:"sink"`
SampleSpecification string `json:"sample_specifications"`
ChannelMap string `json:"channel_map"`
Format string `json:"format"`
Corked bool `json:"corked"`
Mute bool `json:"mute"`
Volume map[string]PAVolume `json:"volume"`
Balance float64 `json:"balance"`
BufferLatencyUsec float64 `json:"buffer_latency_usec"`
SinkLatencyUsec float64 `json:"sink_latency_usec"`
ResampleMethod string `json:"resample_method"`
Properties map[string]string `json:"properties"`
}
func init() {
cmd := exec.Command("pactl", "-f", "json", "list", "sinks", "short")
err := cmd.Run()
if err == nil {
inputs.SoundInputs["pulseaudio"] = &PulseaudioInput{}
} else {
log.Println("Unable to access pulseaudio:", err.Error())
}
}
func (s *PulseaudioInput) GetName() string {
return "pulseaudio"
}
func (s *PulseaudioInput) IsActive() bool {
cmd := exec.Command("pactl", "-f", "json", "list", "sinks", "short")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
return false
}
var sinks []PASink
err = json.Unmarshal(stdoutStderr, &sinks)
if err != nil {
return false
}
for _, sink := range sinks {
if sink.State != "SUSPENDED" {
return true
}
}
return false
}
func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
cmd := exec.Command("pactl", "-f", "json", "list", "sink-inputs")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
log.Println("Unable to list sink-inputs:", err.Error())
return nil
}
var sinkinputs []PASinkInput
err = json.Unmarshal(stdoutStderr, &sinkinputs)
if err != nil {
log.Println("Unable to list sink-inputs:", err.Error())
return nil
}
ret := map[string]string{}
for _, input := range sinkinputs {
if v, ok := input.Properties["media.name"]; ok {
ret[strconv.FormatInt(input.Index, 10)] = v
} else if v, ok := input.Properties["device.description"]; ok {
ret[strconv.FormatInt(input.Index, 10)] = v
} else {
ret[strconv.FormatInt(input.Index, 10)] = fmt.Sprintf("#%d", input.Index)
}
}
return ret
}

View File

@ -8,6 +8,7 @@ import (
"git.nemunai.re/nemunaire/hathoris/config"
_ "git.nemunai.re/nemunaire/hathoris/inputs/mpris"
_ "git.nemunai.re/nemunaire/hathoris/inputs/pulseaudio"
_ "git.nemunai.re/nemunaire/hathoris/sources/amp1_gpio"
_ "git.nemunai.re/nemunaire/hathoris/sources/mpv"
_ "git.nemunai.re/nemunaire/hathoris/sources/spdif"

View File

@ -180,7 +180,7 @@ func (s *MPVSource) CurrentlyPlaying() string {
return "-"
}
func (s *MPVSource) TogglePause() error {
func (s *MPVSource) TogglePause(id string) error {
if s.ipcSocket == "" {
return fmt.Errorf("Not supported")
}