mirror of
https://github.com/xaionaro-go/streamctl.git
synced 2025-09-26 19:41:17 +08:00
Add audio handlers
This commit is contained in:
7
go.mod
7
go.mod
@@ -19,6 +19,8 @@ replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20241006110309-
|
||||
|
||||
replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06
|
||||
|
||||
replace github.com/jfreymuth/pulse v0.1.1 => github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb
|
||||
|
||||
require (
|
||||
github.com/facebookincubator/go-belt v0.0.0-20240804203001-846c4409d41c
|
||||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
@@ -64,6 +66,7 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/datarhei/gosrt v0.7.0 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -110,6 +113,7 @@ require (
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect
|
||||
github.com/jezek/xgb v1.1.0 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
@@ -198,6 +202,7 @@ require (
|
||||
github.com/chai2010/webp v1.1.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/ebitengine/oto/v3 v3.3.1
|
||||
github.com/getsentry/sentry-go v0.28.1
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
github.com/go-ng/xmath v0.0.0-20230704233441-028f5ea62335
|
||||
@@ -206,6 +211,8 @@ require (
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/immune-gmbh/attestation-sdk v0.0.0-20230711173209-f44e4502aeca
|
||||
github.com/jfreymuth/oggvorbis v1.0.5
|
||||
github.com/jfreymuth/pulse v0.1.1
|
||||
github.com/kbinani/screenshot v0.0.0-20230812210009-b87d31814237
|
||||
github.com/lusingander/colorpicker v0.7.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
12
go.sum
12
go.sum
@@ -176,6 +176,10 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/oto/v3 v3.3.1 h1:d4McwGQuXOT0GL7bA5g9ZnaUEIEjQvG3hafzMy+T3qE=
|
||||
github.com/ebitengine/oto/v3 v3.3.1/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
|
||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
@@ -424,6 +428,10 @@ github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN
|
||||
github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg=
|
||||
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
|
||||
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
|
||||
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
@@ -659,8 +667,6 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/xaionaro-go/datacounter v1.0.4 h1:+QMZLmu73R5WGkQfUPwlXF/JFN+Weo4iuDZkiL2wVm8=
|
||||
github.com/xaionaro-go/datacounter v1.0.4/go.mod h1:Sf9vBevuV6w5iE6K3qJ9pWVKcyS60clWBUSQLjt5++c=
|
||||
github.com/xaionaro-go/deepcopy v0.0.0-20241022170035-e7567ec09853 h1:i8myxLpqtNsz0pXpaZhBSkLjeV5+qlgXiEEzOl8eCsk=
|
||||
github.com/xaionaro-go/deepcopy v0.0.0-20241022170035-e7567ec09853/go.mod h1:FdcXb6OQaB+zrL4xtd+AW85iDYnAuZ7iP2qDGFTX6ok=
|
||||
github.com/xaionaro-go/deepcopy v0.0.0-20241022201332-e9025712e972 h1:0RuOVRrOFvEvCQe95VGarA9yDgHoz2pyU22F3XYmdC8=
|
||||
github.com/xaionaro-go/deepcopy v0.0.0-20241022201332-e9025712e972/go.mod h1:FdcXb6OQaB+zrL4xtd+AW85iDYnAuZ7iP2qDGFTX6ok=
|
||||
github.com/xaionaro-go/fyne/v2 v2.0.0-20241020235352-fd61e4920f24 h1:eewdCRMkJmK2ipI9653XL2dE3EFS2I3GTFRadIyyEo4=
|
||||
@@ -683,6 +689,8 @@ github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970 h1:QmbvVR2Jt+
|
||||
github.com/xaionaro-go/mediamtx v0.0.0-20241009124606-94c22c603970/go.mod h1:3J9s+wGt6CV4MDnoXApKEdY3kdc5sd6AYEndLJSAIYI=
|
||||
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a h1:PyX7XpLkj+eAwrPMFMGpvZIG4zBfzAfwNhwTtbORqN0=
|
||||
github.com/xaionaro-go/obs-grpc-proxy v0.0.0-20241018162120-5faf4e7a684a/go.mod h1:exSKIlCibB0ww+ABDwH+YG/iNdqVfdzXBBg5LYxkxGw=
|
||||
github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb h1:9iHPI27CYbmJDhzEuCABQthE/DGVNvT60ybWvv3BV8w=
|
||||
github.com/xaionaro-go/pulse v0.0.0-20241023202712-7151fa00d4bb/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20190309154744-55278e21e817/go.mod h1:Nb/15eS0BMty6TMuWgRQM8WCDIUlyPZagcpchHT6c9Y=
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1 h1:1Kqw9dv2LnznIhJoMt3dNzc/ctSj6VHjyGh4YZHjpE4=
|
||||
github.com/xaionaro-go/spinlock v0.0.0-20200518175509-30e6d1ce68a1/go.mod h1:UwmTXX+EpoEYHuy0rSys1Rp5PW+eVTgZSjgMVLJENKg=
|
||||
|
59
pkg/audio/audio.go
Normal file
59
pkg/audio/audio.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/jfreymuth/oggvorbis"
|
||||
)
|
||||
|
||||
const BufferSize = 200 * time.Millisecond
|
||||
|
||||
type Audio struct {
|
||||
PlayerPCM
|
||||
}
|
||||
|
||||
func NewAudio(playerPCM PlayerPCM) *Audio {
|
||||
return &Audio{
|
||||
PlayerPCM: playerPCM,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAudioAuto() *Audio {
|
||||
for _, factory := range []func() PlayerPCM{
|
||||
NewPlayerPulse,
|
||||
NewPlayerOto,
|
||||
} {
|
||||
player := factory()
|
||||
if player.Ping() == nil {
|
||||
return &Audio{
|
||||
PlayerPCM: player,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the default backend:
|
||||
return &Audio{
|
||||
PlayerPCM: NewPlayerOto(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Audio) PlayVorbis(rawReader io.Reader) error {
|
||||
oggReader, err := oggvorbis.NewReader(rawReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize a vorbis reader: %w", err)
|
||||
}
|
||||
|
||||
err = a.PlayerPCM.PlayPCM(
|
||||
uint32(oggReader.SampleRate()),
|
||||
uint16(oggReader.Channels()),
|
||||
PCMFormatFloat32LE,
|
||||
BufferSize,
|
||||
newReaderFromFloat32Reader(oggReader),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to playback as PCM: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
30
pkg/audio/audio_unsafe.go
Normal file
30
pkg/audio/audio_unsafe.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type float32Reader interface {
|
||||
Read([]float32) (int, error)
|
||||
}
|
||||
|
||||
type readerFromFloat32Reader struct {
|
||||
float32Reader
|
||||
}
|
||||
|
||||
var _ io.Reader = (*readerFromFloat32Reader)(nil)
|
||||
|
||||
func newReaderFromFloat32Reader(r float32Reader) readerFromFloat32Reader {
|
||||
return readerFromFloat32Reader{
|
||||
float32Reader: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (r readerFromFloat32Reader) Read(b []byte) (int, error) {
|
||||
ptr := unsafe.SliceData(b)
|
||||
f := unsafe.Slice((*float32)(unsafe.Pointer(ptr)), len(b)/4)
|
||||
n, err := r.float32Reader.Read(f)
|
||||
n *= int(unsafe.Sizeof(float32(0)))
|
||||
return n, err
|
||||
}
|
17
pkg/audio/cmd/beep/main.go
Normal file
17
pkg/audio/cmd/beep/main.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/xaionaro-go/streamctl/pkg/audio"
|
||||
"github.com/xaionaro-go/streamctl/pkg/audiotheme/defaultaudiotheme"
|
||||
)
|
||||
|
||||
func main() {
|
||||
audiotheme := defaultaudiotheme.AudioTheme()
|
||||
a := audio.NewAudioAuto()
|
||||
err := a.PlayVorbis(bytes.NewReader(audiotheme.ChatMessage))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
36
pkg/audio/player.go
Normal file
36
pkg/audio/player.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PlayerPCM interface {
|
||||
Ping() error
|
||||
PlayPCM(
|
||||
sampleRate uint32,
|
||||
channels uint16,
|
||||
format PCMFormat,
|
||||
bufferSize time.Duration,
|
||||
reader io.Reader,
|
||||
) error
|
||||
}
|
||||
|
||||
type PCMFormat uint
|
||||
|
||||
const (
|
||||
PCMFormatUndefined = PCMFormat(iota)
|
||||
PCMFormatFloat32LE
|
||||
)
|
||||
|
||||
func (f PCMFormat) String() string {
|
||||
switch f {
|
||||
case PCMFormatUndefined:
|
||||
return "<undefined>"
|
||||
case PCMFormatFloat32LE:
|
||||
return "f32le"
|
||||
default:
|
||||
return fmt.Sprintf("<unexpected_value_%d>", f)
|
||||
}
|
||||
}
|
56
pkg/audio/player_oto.go
Normal file
56
pkg/audio/player_oto.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/ebitengine/oto/v3"
|
||||
)
|
||||
|
||||
type PlayerOto struct {
|
||||
}
|
||||
|
||||
var _ PlayerPCM = (*PlayerOto)(nil)
|
||||
|
||||
func NewPlayerOto() PlayerPCM {
|
||||
return PlayerOto{}
|
||||
}
|
||||
|
||||
func (PlayerOto) Ping() error {
|
||||
return fmt.Errorf("not implemented: do not know how to do that, yet")
|
||||
}
|
||||
|
||||
func (PlayerOto) PlayPCM(
|
||||
sampleRate uint32,
|
||||
channels uint16,
|
||||
format PCMFormat,
|
||||
bufferSize time.Duration,
|
||||
reader io.Reader,
|
||||
) error {
|
||||
op := &oto.NewContextOptions{
|
||||
SampleRate: int(sampleRate),
|
||||
ChannelCount: int(channels),
|
||||
Format: oto.FormatFloat32LE,
|
||||
BufferSize: bufferSize,
|
||||
}
|
||||
|
||||
otoCtx, readyChan, err := oto.NewContext(op)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize an oto context: %w", err)
|
||||
}
|
||||
<-readyChan
|
||||
|
||||
player := otoCtx.NewPlayer(reader)
|
||||
player.Play()
|
||||
for player.IsPlaying() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
err = player.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close the player: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
89
pkg/audio/player_pulse.go
Normal file
89
pkg/audio/player_pulse.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/jfreymuth/pulse"
|
||||
"github.com/jfreymuth/pulse/proto"
|
||||
)
|
||||
|
||||
type PlayerPulse struct {
|
||||
}
|
||||
|
||||
var _ PlayerPCM = (*PlayerPulse)(nil)
|
||||
|
||||
func NewPlayerPulse() PlayerPCM {
|
||||
return PlayerPulse{}
|
||||
}
|
||||
|
||||
func (PlayerPulse) Ping() error {
|
||||
c, err := pulse.NewClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open a client to Pulse: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (PlayerPulse) PlayPCM(
|
||||
sampleRate uint32,
|
||||
channels uint16,
|
||||
format PCMFormat,
|
||||
bufferSize time.Duration,
|
||||
rawReader io.Reader,
|
||||
) error {
|
||||
reader, err := newPulseReader(format, rawReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize a reader for Pulse: %w", err)
|
||||
}
|
||||
|
||||
c, err := pulse.NewClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open a client to Pulse: %w", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
stream, err := c.NewPlayback(reader, pulse.PlaybackLatency(bufferSize.Seconds()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to initialize a playback: %w", err)
|
||||
}
|
||||
|
||||
stream.Start()
|
||||
stream.Drain()
|
||||
if stream.Error() != nil {
|
||||
return fmt.Errorf("an error occurred during playback: %w", stream.Error())
|
||||
}
|
||||
if stream.Underflow() {
|
||||
return fmt.Errorf("underflow")
|
||||
}
|
||||
stream.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
type pulseReader struct {
|
||||
pulseFormat byte
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func newPulseReader(pcmFormat PCMFormat, reader io.Reader) (*pulseReader, error) {
|
||||
var pulseFormat byte
|
||||
switch pcmFormat {
|
||||
case PCMFormatFloat32LE:
|
||||
pulseFormat = proto.FormatInt32LE
|
||||
default:
|
||||
return nil, fmt.Errorf("received an unexpected format: %v", pcmFormat)
|
||||
}
|
||||
return &pulseReader{
|
||||
pulseFormat: pulseFormat,
|
||||
Reader: reader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ pulse.Reader = (*pulseReader)(nil)
|
||||
|
||||
func (r pulseReader) Format() byte {
|
||||
return r.pulseFormat
|
||||
}
|
7
pkg/audiotheme/audio_theme.go
Normal file
7
pkg/audiotheme/audio_theme.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package audiotheme
|
||||
|
||||
type VorbisFile []byte
|
||||
|
||||
type AudioTheme struct {
|
||||
ChatMessage VorbisFile
|
||||
}
|
19
pkg/audiotheme/defaultaudiotheme/audio_theme.go
Normal file
19
pkg/audiotheme/defaultaudiotheme/audio_theme.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package defaultaudiotheme
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"slices"
|
||||
|
||||
"github.com/xaionaro-go/streamctl/pkg/audiotheme"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed resources/chat_message.ogg
|
||||
chatMessage []byte
|
||||
)
|
||||
|
||||
func AudioTheme() audiotheme.AudioTheme {
|
||||
return audiotheme.AudioTheme{
|
||||
ChatMessage: slices.Clone(chatMessage),
|
||||
}
|
||||
}
|
BIN
pkg/audiotheme/defaultaudiotheme/resources/chat_message.ogg
Normal file
BIN
pkg/audiotheme/defaultaudiotheme/resources/chat_message.ogg
Normal file
Binary file not shown.
24
pkg/streampanel/audio/audio.go
Normal file
24
pkg/streampanel/audio/audio.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
audioSubsystem "github.com/xaionaro-go/streamctl/pkg/audio"
|
||||
"github.com/xaionaro-go/streamctl/pkg/audiotheme"
|
||||
"github.com/xaionaro-go/streamctl/pkg/audiotheme/defaultaudiotheme"
|
||||
)
|
||||
|
||||
type Audio struct {
|
||||
playbacker audioSubsystem.Audio
|
||||
audioTheme audiotheme.AudioTheme
|
||||
}
|
||||
|
||||
func NewAudio() *Audio {
|
||||
return &Audio{
|
||||
audioTheme: defaultaudiotheme.AudioTheme(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Audio) PlayChatMessage() error {
|
||||
return a.playbacker.PlayVorbis(bytes.NewReader(a.audioTheme.ChatMessage))
|
||||
}
|
10
pkg/streampanel/audio/cmd/beep/main.go
Normal file
10
pkg/streampanel/audio/cmd/beep/main.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/xaionaro-go/streamctl/pkg/streampanel/audio"
|
||||
)
|
||||
|
||||
func main() {
|
||||
a := audio.NewAudio()
|
||||
a.PlayChatMessage()
|
||||
}
|
Reference in New Issue
Block a user