Able to control stream volume
This commit is contained in:
parent
5d32f30a54
commit
d2090bee67
@ -11,9 +11,12 @@ import (
|
||||
)
|
||||
|
||||
type InputState struct {
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Controlable bool `json:"controlable"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
Controlable bool `json:"controlable"`
|
||||
Streams map[string]string `json:"streams,omitempty"`
|
||||
Mixable bool `json:"mixable"`
|
||||
Mixer map[string]*inputs.InputMixer `json:"mixer,omitempty"`
|
||||
}
|
||||
|
||||
func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
@ -21,12 +24,21 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
ret := map[string]*InputState{}
|
||||
|
||||
for k, inp := range inputs.SoundInputs {
|
||||
var mixer map[string]*inputs.InputMixer
|
||||
|
||||
_, controlable := inp.(inputs.ControlableInput)
|
||||
im, mixable := inp.(inputs.MixableInput)
|
||||
if mixable {
|
||||
mixer, _ = im.GetMixers()
|
||||
}
|
||||
|
||||
ret[k] = &InputState{
|
||||
Name: inp.GetName(),
|
||||
Active: inp.IsActive(),
|
||||
Controlable: controlable,
|
||||
Streams: inp.CurrentlyPlaying(),
|
||||
Mixable: mixable,
|
||||
Mixer: mixer,
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,25 +49,36 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
inputsRoutes.Use(InputHandler)
|
||||
|
||||
inputsRoutes.GET("", func(c *gin.Context) {
|
||||
src := c.MustGet("input").(inputs.SoundInput)
|
||||
inp := c.MustGet("input").(inputs.SoundInput)
|
||||
|
||||
var mixer map[string]*inputs.InputMixer
|
||||
_, controlable := inp.(inputs.ControlableInput)
|
||||
im, mixable := inp.(inputs.MixableInput)
|
||||
if mixable {
|
||||
mixer, _ = im.GetMixers()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &InputState{
|
||||
Name: src.GetName(),
|
||||
Active: src.IsActive(),
|
||||
Name: inp.GetName(),
|
||||
Active: inp.IsActive(),
|
||||
Controlable: controlable,
|
||||
Streams: inp.CurrentlyPlaying(),
|
||||
Mixable: mixable,
|
||||
Mixer: mixer,
|
||||
})
|
||||
})
|
||||
inputsRoutes.GET("/settings", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, c.MustGet("input"))
|
||||
})
|
||||
inputsRoutes.GET("/streams", func(c *gin.Context) {
|
||||
src := c.MustGet("input").(inputs.SoundInput)
|
||||
inp := c.MustGet("input").(inputs.SoundInput)
|
||||
|
||||
if !src.IsActive() {
|
||||
if !inp.IsActive() {
|
||||
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{"errmsg": "Input not active"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, src.CurrentlyPlaying())
|
||||
c.JSON(http.StatusOK, inp.CurrentlyPlaying())
|
||||
})
|
||||
|
||||
streamRoutes := inputsRoutes.Group("/streams/:stream")
|
||||
@ -74,18 +97,40 @@ func declareInputsRoutes(cfg *config.Config, router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
streamRoutes.POST("/volume", func(c *gin.Context) {
|
||||
input, ok := c.MustGet("input").(inputs.MixableInput)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, gin.H{"errmsg": "The source doesn't support that"})
|
||||
return
|
||||
}
|
||||
|
||||
var mixer inputs.InputMixer
|
||||
err := c.BindJSON(&mixer)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = input.SetMixer(c.MustGet("streamid").(string), &mixer)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to pause the input: %s", err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func InputHandler(c *gin.Context) {
|
||||
src, ok := inputs.SoundInputs[c.Param("input")]
|
||||
inp, ok := inputs.SoundInputs[c.Param("input")]
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": fmt.Sprintf("Input not found: %s", c.Param("input"))})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("input", src)
|
||||
c.Set("input", inp)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
@ -4,6 +4,14 @@ import ()
|
||||
|
||||
var SoundInputs = map[string]SoundInput{}
|
||||
|
||||
type InputMixer struct {
|
||||
Volume uint `json:"volume"`
|
||||
VolumePercent string `json:"volume_percent"`
|
||||
VolumeDB string `json:"volume_db"`
|
||||
Mute bool `json:"mute"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
|
||||
type SoundInput interface {
|
||||
GetName() string
|
||||
IsActive() bool
|
||||
@ -13,3 +21,8 @@ type SoundInput interface {
|
||||
type ControlableInput interface {
|
||||
TogglePause(string) error
|
||||
}
|
||||
|
||||
type MixableInput interface {
|
||||
GetMixers() (map[string]*InputMixer, error)
|
||||
SetMixer(string, *InputMixer) error
|
||||
}
|
||||
|
@ -82,16 +82,24 @@ func (s *PulseaudioInput) IsActive() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
|
||||
func (s *PulseaudioInput) getPASinkInputs() ([]PASinkInput, error) {
|
||||
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
|
||||
return nil, fmt.Errorf("unable to list sink-inputs: %w", err)
|
||||
}
|
||||
|
||||
var sinkinputs []PASinkInput
|
||||
err = json.Unmarshal(stdoutStderr, &sinkinputs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse sink-inputs list: %w", err)
|
||||
}
|
||||
|
||||
return sinkinputs, nil
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
|
||||
sinkinputs, err := s.getPASinkInputs()
|
||||
if err != nil {
|
||||
log.Println("Unable to list sink-inputs:", err.Error())
|
||||
return nil
|
||||
@ -110,3 +118,59 @@ func (s *PulseaudioInput) CurrentlyPlaying() map[string]string {
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) GetMixers() (map[string]*inputs.InputMixer, error) {
|
||||
sinkinputs, err := s.getPASinkInputs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[string]*inputs.InputMixer{}
|
||||
for _, input := range sinkinputs {
|
||||
var maxvolume string
|
||||
for k, vol := range input.Volume {
|
||||
if maxvolume == "" || vol.Value > input.Volume[maxvolume].Value {
|
||||
maxvolume = k
|
||||
}
|
||||
}
|
||||
|
||||
ret[strconv.FormatInt(input.Index, 10)] = &inputs.InputMixer{
|
||||
Volume: input.Volume[maxvolume].Value,
|
||||
VolumePercent: input.Volume[maxvolume].ValuePercent,
|
||||
VolumeDB: input.Volume[maxvolume].DB,
|
||||
Balance: input.Balance,
|
||||
Mute: input.Mute,
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *PulseaudioInput) SetMixer(stream string, volume *inputs.InputMixer) error {
|
||||
sinkinputs, err := s.getPASinkInputs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range sinkinputs {
|
||||
if strconv.FormatInt(input.Index, 10) == stream {
|
||||
cmd := exec.Command("pactl", "set-sink-input-volume", stream, strconv.FormatUint(uint64(volume.Volume), 10))
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to set volume: %w", err)
|
||||
}
|
||||
|
||||
if input.Mute != volume.Mute {
|
||||
cmd := exec.Command("pactl", "set-sink-input-mute", stream, "toggle")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to change mute state: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to find stream %q", stream)
|
||||
}
|
||||
|
@ -1,7 +1,16 @@
|
||||
<script>
|
||||
import { activeInputs, inputsList } from '$lib/stores/inputs';
|
||||
import { activeInputs, inputs, inputsList } from '$lib/stores/inputs';
|
||||
|
||||
export let showInactives = false;
|
||||
|
||||
let altering_mixer = null;
|
||||
async function alterMixer(input, streamid, volume) {
|
||||
if (altering_mixer) altering_mixer.abort();
|
||||
altering_mixer = setTimeout(() => {
|
||||
fetch(`api/inputs/${input.name}/streams/${streamid}/volume`, {headers: {'Accept': 'application/json'}, method: 'POST', body: JSON.stringify({'volume': volume ? volume : input.mixer[streamid].volume})}).then(() => inputs.refresh());
|
||||
altering_mixer = null;
|
||||
}, 450);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="list-group list-group-flush">
|
||||
@ -12,35 +21,49 @@
|
||||
</span>
|
||||
</li>
|
||||
{/if}
|
||||
{#each $inputsList as input}
|
||||
{#each $inputsList as input, iid}
|
||||
{#if showInactives || input.active}
|
||||
<li class="list-group-item py-3 d-flex flex-column">
|
||||
<strong>{input.name}</strong>
|
||||
{#await input.streams()}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{:then streams}
|
||||
{#each Object.keys(streams) as idstream}
|
||||
{@const title = streams[idstream]}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<span class="text-muted">{title}</span>
|
||||
</div>
|
||||
{#if input.controlable}
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={() => input.playpause(idstream)}
|
||||
>
|
||||
<i class="bi bi-pause"></i>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.keys(input.streams) as idstream}
|
||||
{@const title = input.streams[idstream]}
|
||||
<li class="list-group-item py-3 d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<label for={'input' + iid + 'stream' + idstream} class="form-label d-inline">{title}</label>
|
||||
<span class="text-muted">({input.name})</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</li>
|
||||
{#if input.controlable}
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={() => input.playpause(idstream)}
|
||||
>
|
||||
<i class="bi bi-pause"></i>
|
||||
</button>
|
||||
</div>
|
||||
{:else if input.mixable && input.mixer[idstream]}
|
||||
<div
|
||||
class="badge bg-primary"
|
||||
title={input.mixer[idstream].volume_percent}
|
||||
>
|
||||
{input.mixer[idstream].volume_db}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if input.mixable && input.mixer[idstream]}
|
||||
<div>
|
||||
<input
|
||||
type="range"
|
||||
class="form-range"
|
||||
id={'input' + iid + 'stream' + idstream}
|
||||
min={0}
|
||||
max={65536}
|
||||
bind:value={input.mixer[idstream].volume}
|
||||
on:change={() => alterMixer(input, idstream)}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
@ -6,13 +6,16 @@ export class Input {
|
||||
}
|
||||
}
|
||||
|
||||
update({ name, active, controlable }) {
|
||||
update({ name, active, controlable, streams, mixable, mixer }) {
|
||||
this.name = name;
|
||||
this.active = active;
|
||||
this.controlable = controlable;
|
||||
this.streams = streams;
|
||||
this.mixable = mixable;
|
||||
this.mixer = mixer;
|
||||
}
|
||||
|
||||
async streams() {
|
||||
async getStreams() {
|
||||
const data = await fetch(`api/inputs/${this.id}/streams`, {headers: {'Accept': 'application/json'}});
|
||||
if (data.status == 200) {
|
||||
return await data.json();
|
||||
|
Loading…
Reference in New Issue
Block a user