Updated Kirika, new option system for codecs
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
24e2750cbe
commit
8eb0bf95c6
83
README.md
83
README.md
|
@ -14,6 +14,7 @@ Radio streamer ([kawa](https://github.com/Luminarys/kawa) drop-in compatible).
|
|||
* Implements `queue.nr` and `/random` (to be deprecated/changed)
|
||||
* Supports max queue length via `queue.length` config.
|
||||
* Supports extra encoder bitrate control settings (CBR, VBR, auto, etc.)
|
||||
* Can set custom sample rate / channel count / bitdepth / compression level per stream mount.
|
||||
* Can read and apply ReplayGain tags, or normalize audio loudness.
|
||||
* Can have audio sources over HTTP(s) URLs on `path` property, and supports seeking.
|
||||
* Precise metadata and timing information packet stream, trigger via `x-audio-packet-stream: 1` HTTP header.
|
||||
|
@ -94,3 +95,85 @@ Same as kawa's, but `queue_id` is added to response directly.
|
|||
"queue_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
## Mount API
|
||||
### `NEW` GET /mounts
|
||||
A simple listing of the working mounts + settings are made available.
|
||||
|
||||
#### Response
|
||||
```json
|
||||
[
|
||||
{
|
||||
"mount": "/stream128.mp3",
|
||||
"mime": "audio/mpeg;codecs=mp3",
|
||||
"sampleRate": 44100,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitrate": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"mount": "/stream192.mp3",
|
||||
"mime": "audio/mpeg;codecs=mp3",
|
||||
"sampleRate": 44100,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitrate": 192
|
||||
}
|
||||
},
|
||||
{
|
||||
"mount": "/stream128.aac",
|
||||
"mime": "audio/aac",
|
||||
"sampleRate": 44100,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitrate": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"mount": "/stream128.opus",
|
||||
"mime": "audio/ogg;codecs=opus",
|
||||
"sampleRate": 48000,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitrate": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"mount": "/stream192.opus",
|
||||
"mime": "audio/ogg;codecs=opus",
|
||||
"sampleRate": 48000,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitrate": 192
|
||||
}
|
||||
},
|
||||
{
|
||||
"mount": "/stream256.opus",
|
||||
"mime": "audio/ogg;codecs=opus",
|
||||
"sampleRate": 48000,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitrate": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"mount": "/stream.flac",
|
||||
"mime": "audio/flac",
|
||||
"sampleRate": 44100,
|
||||
"channels": 2,
|
||||
"listeners": 0,
|
||||
"options": {
|
||||
"bitdepth": 16,
|
||||
"block_size": 0,
|
||||
"compression_level": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
6
api.go
6
api.go
|
@ -4,7 +4,7 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/queue"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
@ -152,7 +152,7 @@ func (a *API) listen() {
|
|||
|
||||
type queueResultResponse struct {
|
||||
resultResponse
|
||||
QueueId audio.QueueIdentifier `json:"queue_id"`
|
||||
QueueId queue.QueueIdentifier `json:"queue_id"`
|
||||
}
|
||||
|
||||
a.wg.Add(1)
|
||||
|
@ -290,7 +290,7 @@ func (a *API) listen() {
|
|||
if i, err := strconv.ParseInt(pathSegments[2], 10, 0); err == nil {
|
||||
result := resultResponse{}
|
||||
|
||||
result.Success = a.queue.Remove(audio.QueueIdentifier(i))
|
||||
result.Success = a.queue.Remove(queue.QueueIdentifier(i))
|
||||
|
||||
jsonData, _ := json.Marshal(result)
|
||||
writer.Write(jsonData)
|
||||
|
|
97
config.go
97
config.go
|
@ -1,6 +1,10 @@
|
|||
package main
|
||||
|
||||
import "github.com/BurntSushi/toml"
|
||||
import (
|
||||
"errors"
|
||||
"github.com/BurntSushi/toml"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Api struct {
|
||||
|
@ -16,6 +20,7 @@ type Config struct {
|
|||
BufferSeconds int `toml:"buffer_size"`
|
||||
ReplayGain bool `toml:"replaygain"`
|
||||
Length int `toml:"length"`
|
||||
SampleRate int `toml:"samplerate"`
|
||||
} `toml:"queue"`
|
||||
Radio struct {
|
||||
Port int `toml:"port"`
|
||||
|
@ -25,12 +30,90 @@ type Config struct {
|
|||
Logo string `toml:"logo"`
|
||||
Private bool `toml:"private"`
|
||||
} `toml:"radio"`
|
||||
Streams []struct {
|
||||
MountPath string `toml:"mount"`
|
||||
Container string `toml:"container"`
|
||||
Bitrate interface{} `toml:"bitrate"`
|
||||
Codec string `toml:"codec"`
|
||||
} `toml:"streams"`
|
||||
Streams []StreamConfig `toml:"streams"`
|
||||
}
|
||||
|
||||
type StreamConfig struct {
|
||||
MountPath string
|
||||
Container string
|
||||
Codec string
|
||||
|
||||
Options map[string]interface{}
|
||||
}
|
||||
|
||||
func (s *StreamConfig) GetIntOption(name string, defaultValue int) int {
|
||||
if v, ok := s.Options[name].(int); ok {
|
||||
return v
|
||||
}
|
||||
|
||||
if v, ok := s.Options[name].(int64); ok {
|
||||
return int(v)
|
||||
}
|
||||
|
||||
if v, ok := s.Options[name].(string); ok {
|
||||
if val, err := strconv.Atoi(v); err == nil {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (s *StreamConfig) GetStringOption(name string, defaultValue string) string {
|
||||
if v, ok := s.Options[name].(string); ok {
|
||||
return v
|
||||
}
|
||||
|
||||
if v, ok := s.Options[name].(int); ok {
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
|
||||
if v, ok := s.Options[name].(int64); ok {
|
||||
return strconv.Itoa(int(v))
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (s *StreamConfig) GetOption(name string, defaultValue interface{}) interface{} {
|
||||
|
||||
if v, ok := s.Options[name]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (s *StreamConfig) UnmarshalTOML(value interface{}) error {
|
||||
if v, ok := value.(map[string]interface{}); ok {
|
||||
|
||||
if mount, ok := v["mount"].(string); ok {
|
||||
s.MountPath = mount
|
||||
} else {
|
||||
return errors.New("could not find mount path")
|
||||
}
|
||||
|
||||
if container, ok := v["container"].(string); ok {
|
||||
s.Container = container
|
||||
}
|
||||
|
||||
if codec, ok := v["codec"].(string); ok {
|
||||
s.Codec = codec
|
||||
}
|
||||
|
||||
s.Options = make(map[string]interface{})
|
||||
|
||||
for k, val := range v {
|
||||
if k == "mount" || k == "container" || k == "codec" {
|
||||
continue
|
||||
}
|
||||
s.Options[k] = val
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("could not decode")
|
||||
}
|
||||
|
||||
func GetConfig(pathName string) (*Config, error) {
|
||||
|
|
|
@ -53,6 +53,10 @@ buffer_duration=0
|
|||
# }
|
||||
replaygain=false
|
||||
|
||||
# Set the sample rate of the queue. Default is 44100
|
||||
# Some codecs (example: Opus) will output at a different samplerate.
|
||||
#samplerate=44100
|
||||
|
||||
[radio]
|
||||
#
|
||||
# The port to stream actual audio on. MeteorLight will listen on localhost.
|
||||
|
@ -73,11 +77,15 @@ private=false
|
|||
# following properties are available:
|
||||
#
|
||||
# mount: the HTTP address to serve the stream from
|
||||
# container: the container format to use (ogg, flac, aac, or mp3). See Kirika's supported format list.
|
||||
# codec: the audio codec to use (opus, flac, aac, do not specify for mp3 streams)
|
||||
# container: the container format to use (ogg, flac, aac/adts, or mp3). See Kirika's supported format list.
|
||||
# codec: the audio codec to use (opus, flac, aac, he-aacv2, or mp3)
|
||||
# bitrate: the desired bitrate of the stream in Kb/s, if not specified (or 0) an appropriate
|
||||
# bitrate will be automatically selected based on the container/codec
|
||||
# MeteorLight extension: bitrate can be a string (for example, v0/v1/v2/v3 on MP3). codec can also be he-aacv2. No vorbis support
|
||||
# MeteorLight extension: bitrate can be a string (for example, v0/v1/v2/v3 on MP3). codec can also be he-aacv2. No vorbis support.
|
||||
# Additionally further options can be set on some codecs.
|
||||
# - FLAC supports bitdepth (int), compression_level (int), block_size (int)
|
||||
# - All but Opus support setting samplerate
|
||||
# - All support setting channels to 1 or 2
|
||||
[[streams]]
|
||||
mount="stream128.mp3"
|
||||
container="mp3"
|
||||
|
@ -90,6 +98,8 @@ bitrate=192
|
|||
|
||||
[[streams]]
|
||||
mount="stream128.aac"
|
||||
# Also supports he-aacv2
|
||||
codec="aac"
|
||||
container="aac"
|
||||
bitrate=128
|
||||
|
||||
|
@ -113,4 +123,6 @@ bitrate=256
|
|||
|
||||
[[streams]]
|
||||
mount="stream.flac"
|
||||
codec="flac"
|
||||
# Also supports ogg
|
||||
container="flac"
|
11
go.mod
11
go.mod
|
@ -3,21 +3,22 @@ module git.gammaspectra.live/S.O.N.G/MeteorLight
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220421115636-4612766b64e8
|
||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220515143733-468ec9b491d1
|
||||
github.com/BurntSushi/toml v1.1.0
|
||||
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63
|
||||
github.com/enriquebris/goconcurrentqueue v0.6.3
|
||||
)
|
||||
|
||||
require (
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1 // indirect
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f // indirect
|
||||
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255 // indirect
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220417020459-7018d91e3eed // indirect
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94 // indirect
|
||||
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba // indirect
|
||||
git.gammaspectra.live/S.O.N.G/go-tta v0.2.1-0.20220226150007-096de1072bd6 // indirect
|
||||
git.gammaspectra.live/S.O.N.G/goflac v0.0.0-20220417181802-3057bde44c07 // indirect
|
||||
github.com/dh1tw/gosamplerate v0.1.2 // indirect
|
||||
github.com/edgeware/mp4ff v0.27.0 // indirect
|
||||
github.com/edgeware/mp4ff v0.28.0 // indirect
|
||||
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565 // indirect
|
||||
github.com/icza/bitio v1.1.0 // indirect
|
||||
github.com/jfreymuth/oggvorbis v1.0.3 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
|
@ -27,4 +28,6 @@ require (
|
|||
github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect
|
||||
github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081 // indirect
|
||||
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d // indirect
|
||||
github.com/youpy/go-riff v0.1.0 // indirect
|
||||
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 // indirect
|
||||
)
|
||||
|
|
27
go.sum
27
go.sum
|
@ -1,11 +1,11 @@
|
|||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220421115636-4612766b64e8 h1:OMLUwQmbveOsXlQU0XUfA7tZMAzz98B7rac4GKmrY90=
|
||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220421115636-4612766b64e8/go.mod h1:YAH1ahOjNcVgGGeudk5q0bNqcSesc19ItsQeiuNART8=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1 h1:D1VyacBGUBfvFD4Fq2eO6RQ5eZPUdC2YsIv9gXun9aQ=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421110341-7839cd4c1da1/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
|
||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220515143733-468ec9b491d1 h1:3k49C82QXtvONyQk51XPz2cGIOkmhmmMAggWM/RW8tg=
|
||||
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220515143733-468ec9b491d1/go.mod h1:65QohfAwsgMkXS7PZO2cm8VgPEZlhKC+JAeZfJBYpFE=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f h1:CxN7zlk5FdAieyRKQSbwBGBsvQ2cDF8JVCODZpzcRkA=
|
||||
git.gammaspectra.live/S.O.N.G/go-alac v0.0.0-20220421115623-d0b3bfe57e0f/go.mod h1:f1+h7KOnuM9zcEQp7ri4UaVvgX4m1NFFIXgReIyjGMA=
|
||||
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255 h1:BWRx2ZFyhp5+rsXhdDZtk5Gld+L44lxlN9ASqB9Oj0M=
|
||||
git.gammaspectra.live/S.O.N.G/go-ebur128 v0.0.0-20220418202343-73a167e76255/go.mod h1:5H4eVW9uknpn8REFr+C3ejhvXdncgm/pbGqKGC43gFY=
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220417020459-7018d91e3eed h1:aJPCb3LS4Wai34S5bKUGgAm+/hPt0J2tvWPOl6RnbbE=
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220417020459-7018d91e3eed/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94 h1:gD6lOyQwwuyJilwbLC8lAEWpUSZvndJsUGwxxDatCAE=
|
||||
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220421165127-c4b73b260d94/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
|
||||
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba h1:JEaxCVgdr3XXAuDCPAx7ttLFZaaHzTEzG+oRnVUtUKU=
|
||||
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba/go.mod h1:vkoHSHVM9p6vAUmXAik0gvaLcIfiQYrD6bQqVpOulUk=
|
||||
git.gammaspectra.live/S.O.N.G/go-tta v0.2.1-0.20220226150007-096de1072bd6 h1:ITVVisbHPnUclp3PBkCbXFeBhOCBcOjPdgjJ9wRH3TI=
|
||||
|
@ -21,15 +21,17 @@ github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/
|
|||
github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM=
|
||||
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 h1:/u5RVRk3Nh7Zw1QQnPtUH5kzcc8JmSSRpHSlGU/zGTE=
|
||||
github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
||||
github.com/edgeware/mp4ff v0.27.0 h1:Lv773H6c4pUt3Zj9z44TOICv6fjd6gKzi1sVl8GbcYE=
|
||||
github.com/edgeware/mp4ff v0.27.0/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
|
||||
github.com/edgeware/mp4ff v0.28.0 h1:iAtPUXtENORvCPCLIdDNeh5Lb6GXS1hIyYUx2iBkygE=
|
||||
github.com/edgeware/mp4ff v0.28.0/go.mod h1:GNUeA6tEFksH2CrjJF2FSGdJolba8yPGmo16qZTXsm8=
|
||||
github.com/enriquebris/goconcurrentqueue v0.6.3 h1:+ma7EEEFMDmJBIS6Q4KNJruChctgwYQFqlxdveIoEE4=
|
||||
github.com/enriquebris/goconcurrentqueue v0.6.3/go.mod h1:OZ+KC2BcRYzjg0vgoUs1GFqdAjkD9mz2Ots7Jbm1yS4=
|
||||
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565 h1:5cGj3eU0uEzpHDFwqmcKc6zM1+HNvzPIR6ZY7T1/MsA=
|
||||
github.com/gen2brain/aac-go v0.0.0-20180306134136-400c68157565/go.mod h1:Tymn6PFuf0SYIYLOWIZWtatGOmi/5FLUNawJt9OgxJw=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
|
||||
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
|
||||
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
|
@ -56,6 +58,11 @@ github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081/go.mod h1:ExwZtltybPz8z
|
|||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d h1:LptdD7GTUZeklomtW5vZ1AHwBvDBUCZ2Ftpaz7uEI7g=
|
||||
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d/go.mod h1:EqTcYM7y4JlSfeTI47pmNu3EZQuCuLQefsQyg1Imlz8=
|
||||
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
|
||||
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
|
||||
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
|
||||
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM=
|
||||
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
112
mount.go
112
mount.go
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/filter"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/aac"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/flac"
|
||||
|
@ -32,18 +33,20 @@ type StreamListener struct {
|
|||
Close func()
|
||||
}
|
||||
type StreamMount struct {
|
||||
Mount string
|
||||
MimeType string
|
||||
Packetizer packetizer.Packetizer
|
||||
Options map[string]interface{}
|
||||
SampleRate int
|
||||
MetadataQueue *goconcurrentqueue.FIFO
|
||||
listeners []*StreamListener
|
||||
listenersLock sync.Mutex
|
||||
keepBuffer []packetizer.Packet
|
||||
Mount string
|
||||
MimeType string
|
||||
FormatDescription string
|
||||
Packetizer packetizer.Packetizer
|
||||
Options map[string]interface{}
|
||||
SampleRate int
|
||||
Channels int
|
||||
MetadataQueue *goconcurrentqueue.FIFO
|
||||
listeners []*StreamListener
|
||||
listenersLock sync.Mutex
|
||||
keepBuffer []packetizer.Packet
|
||||
}
|
||||
|
||||
func NewStreamMount(source audio.Source, mount string, codec string, container string, bitrate interface{}) *StreamMount {
|
||||
func NewStreamMount(source audio.Source, config *StreamConfig) *StreamMount {
|
||||
var encoderFormat format.Encoder
|
||||
options := make(map[string]interface{})
|
||||
var mimeType string
|
||||
|
@ -51,14 +54,21 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
|||
reader, writer := io.Pipe()
|
||||
var packetizerEntry packetizer.Packetizer
|
||||
|
||||
sampleRate := source.SampleRate
|
||||
bitrate := config.GetOption("bitrate", nil)
|
||||
|
||||
switch codec {
|
||||
sampleRate := config.GetIntOption("samplerate", source.SampleRate)
|
||||
|
||||
channels := config.GetIntOption("channels", source.Channels)
|
||||
|
||||
switch config.Codec {
|
||||
case "vorbis":
|
||||
return nil
|
||||
case "opus":
|
||||
encoderFormat = opus.NewFormat()
|
||||
if bitrate != nil {
|
||||
options["bitrate"] = bitrate
|
||||
}
|
||||
sampleRate = opus.FixedSampleRate
|
||||
mimeType = "audio/ogg;codecs=opus"
|
||||
packetizerEntry = packetizer.NewOggPacketizer(reader)
|
||||
case "mp3":
|
||||
|
@ -70,18 +80,23 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
|||
packetizerEntry = packetizer.NewMp3Packetizer(reader)
|
||||
case "flac":
|
||||
encoderFormat = flac.NewFormat()
|
||||
if bitrate != nil {
|
||||
options["bitdepth"] = bitrate
|
||||
}
|
||||
options["compression_level"] = 8
|
||||
|
||||
options["bitdepth"] = config.GetIntOption("bitdepth", 16)
|
||||
options["compression_level"] = config.GetIntOption("compression_level", 8)
|
||||
options["block_size"] = config.GetIntOption("block_size", 0)
|
||||
|
||||
mimeType = "audio/flac"
|
||||
if config.Container == "ogg" {
|
||||
options["ogg"] = true
|
||||
mimeType = "audio/ogg;codecs=flac"
|
||||
}
|
||||
packetizerEntry = packetizer.NewFLACPacketizer(reader)
|
||||
case "aac":
|
||||
encoderFormat = aac.NewFormat()
|
||||
if bitrate != nil {
|
||||
options["bitrate"] = bitrate
|
||||
}
|
||||
mimeType = "audio/aac"
|
||||
mimeType = "audio/aac;codecs=mp4a.40.2"
|
||||
packetizerEntry = packetizer.NewAdtsPacketizer(reader)
|
||||
case "he-aacv2":
|
||||
encoderFormat = aac.NewFormat()
|
||||
|
@ -89,17 +104,18 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
|||
options["bitrate"] = bitrate
|
||||
}
|
||||
options["mode"] = "hev2"
|
||||
mimeType = "audio/aac"
|
||||
mimeType = "audio/aac;codecs=mp4a.40.29"
|
||||
packetizerEntry = packetizer.NewAdtsPacketizer(reader)
|
||||
}
|
||||
|
||||
if encoderFormat == nil {
|
||||
switch container {
|
||||
switch config.Container {
|
||||
case "ogg":
|
||||
encoderFormat = opus.NewFormat()
|
||||
if bitrate != nil {
|
||||
options["bitrate"] = bitrate
|
||||
}
|
||||
sampleRate = opus.FixedSampleRate
|
||||
mimeType = "audio/ogg;codecs=opus"
|
||||
packetizerEntry = packetizer.NewOggPacketizer(reader)
|
||||
case "mp3":
|
||||
|
@ -111,10 +127,11 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
|||
packetizerEntry = packetizer.NewMp3Packetizer(reader)
|
||||
case "flac":
|
||||
encoderFormat = flac.NewFormat()
|
||||
if bitrate != nil {
|
||||
options["bitdepth"] = bitrate
|
||||
}
|
||||
options["compression_level"] = 8
|
||||
|
||||
options["bitdepth"] = config.GetIntOption("bitdepth", 16)
|
||||
options["compression_level"] = config.GetIntOption("compression_level", 8)
|
||||
options["block_size"] = config.GetIntOption("block_size", 0)
|
||||
|
||||
mimeType = "audio/flac"
|
||||
packetizerEntry = packetizer.NewFLACPacketizer(reader)
|
||||
case "adts", "aac":
|
||||
|
@ -122,7 +139,7 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
|||
if bitrate != nil {
|
||||
options["bitrate"] = bitrate
|
||||
}
|
||||
mimeType = "audio/aac"
|
||||
mimeType = "audio/aac;codecs=mp4a.40.2"
|
||||
packetizerEntry = packetizer.NewAdtsPacketizer(reader)
|
||||
}
|
||||
}
|
||||
|
@ -131,30 +148,35 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
|||
return nil
|
||||
}
|
||||
|
||||
if opusFormat, ok := encoderFormat.(opus.Format); ok {
|
||||
sampleRate = opus.FixedSampleRate
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
if err := opusFormat.Encode(audio.NewResampleFilter(opus.FixedSampleRate, audio.Linear, 0).Process(source), writer, options); err != nil {
|
||||
log.Panic(err)
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
|
||||
if channels != source.Channels {
|
||||
if channels == 1 {
|
||||
source = filter.MonoFilter{}.Process(source)
|
||||
} else if channels == 2 {
|
||||
source = filter.StereoFilter{}.Process(source)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
if err := encoderFormat.Encode(source, writer, options); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if sampleRate != source.SampleRate {
|
||||
source = filter.NewResampleFilter(sampleRate, filter.Linear, 0).Process(source)
|
||||
}
|
||||
|
||||
if err := encoderFormat.Encode(source, writer, options); err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return &StreamMount{
|
||||
Mount: mount,
|
||||
MimeType: mimeType,
|
||||
Packetizer: packetizerEntry,
|
||||
SampleRate: sampleRate,
|
||||
Options: options,
|
||||
MetadataQueue: goconcurrentqueue.NewFIFO(),
|
||||
Mount: config.MountPath,
|
||||
MimeType: mimeType,
|
||||
FormatDescription: encoderFormat.Description(),
|
||||
Packetizer: packetizerEntry,
|
||||
SampleRate: sampleRate,
|
||||
Channels: channels,
|
||||
Options: options,
|
||||
MetadataQueue: goconcurrentqueue.NewFIFO(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
68
queue.go
68
queue.go
|
@ -7,8 +7,10 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/filter"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/guess"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/queue"
|
||||
"git.gammaspectra.live/S.O.N.G/Kirika/audio/replaygain"
|
||||
"github.com/dhowden/tag"
|
||||
"io"
|
||||
|
@ -26,7 +28,7 @@ import (
|
|||
const maxBufferSize = 10
|
||||
|
||||
type QueueTrackEntry struct {
|
||||
QueueIdentifier audio.QueueIdentifier
|
||||
QueueIdentifier queue.QueueIdentifier
|
||||
Path string
|
||||
Metadata struct {
|
||||
Title string `json:"title"`
|
||||
|
@ -180,7 +182,7 @@ type Queue struct {
|
|||
QueueEmpty chan *QueueTrackEntry
|
||||
Duration time.Duration
|
||||
durationError int64
|
||||
audioQueue *audio.Queue
|
||||
audioQueue *queue.Queue
|
||||
mounts []*StreamMount
|
||||
queue []*QueueTrackEntry
|
||||
mutex sync.RWMutex
|
||||
|
@ -189,17 +191,20 @@ type Queue struct {
|
|||
}
|
||||
|
||||
func NewQueue(config *Config) *Queue {
|
||||
if config.Queue.SampleRate <= 0 {
|
||||
config.Queue.SampleRate = 44100
|
||||
}
|
||||
q := &Queue{
|
||||
NowPlaying: make(chan *QueueTrackEntry, 1),
|
||||
QueueEmpty: make(chan *QueueTrackEntry),
|
||||
config: config,
|
||||
audioQueue: audio.NewQueue(44100, 2),
|
||||
audioQueue: queue.NewQueue(config.Queue.SampleRate, 2),
|
||||
}
|
||||
blocksPerSecond := 20
|
||||
|
||||
sources := SplitAudioSource(audio.NewFilterChain(q.audioQueue.GetSource(), audio.NewBufferFilter(16), audio.NewRealTimeFilter(blocksPerSecond), audio.NewBufferFilter(maxBufferSize*blocksPerSecond)), len(config.Streams))
|
||||
sources := SplitAudioSource(filter.NewFilterChain(q.audioQueue.GetSource(), filter.NewBufferFilter(16), filter.NewRealTimeFilter(blocksPerSecond), filter.NewBufferFilter(maxBufferSize*blocksPerSecond)), len(config.Streams))
|
||||
for i, s := range q.config.Streams {
|
||||
mount := NewStreamMount(sources[i], s.MountPath, s.Codec, s.Container, s.Bitrate)
|
||||
mount := NewStreamMount(sources[i], &s)
|
||||
if mount == nil {
|
||||
log.Panicf("could not initialize %s\n", s.MountPath)
|
||||
}
|
||||
|
@ -222,7 +227,7 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
startCallback := func(queue *audio.Queue, queueEntry *audio.QueueEntry) {
|
||||
startCallback := func(queue *queue.Queue, queueEntry *queue.QueueEntry) {
|
||||
if e := q.Get(queueEntry.Identifier); e != nil { //is this needed?
|
||||
log.Printf("now playing %s\n", e.Path)
|
||||
q.NowPlaying <- e
|
||||
|
@ -238,11 +243,11 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
endCallback := func(queue *audio.Queue, entry *audio.QueueEntry) {
|
||||
endCallback := func(queue *queue.Queue, entry *queue.QueueEntry) {
|
||||
|
||||
}
|
||||
|
||||
removeCallback := func(queue *audio.Queue, entry *audio.QueueEntry) {
|
||||
removeCallback := func(queue *queue.Queue, entry *queue.QueueEntry) {
|
||||
atomic.AddInt64((*int64)(&q.Duration), int64((time.Second*time.Duration(entry.ReadSamples))/time.Duration(entry.Source.SampleRate)))
|
||||
|
||||
q.Remove(entry.Identifier)
|
||||
|
@ -307,7 +312,7 @@ func (q *Queue) GetQueue() (result []*QueueTrackEntry) {
|
|||
return
|
||||
}
|
||||
|
||||
func (q *Queue) Get(identifier audio.QueueIdentifier) *QueueTrackEntry {
|
||||
func (q *Queue) Get(identifier queue.QueueIdentifier) *QueueTrackEntry {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
for _, e := range q.queue {
|
||||
|
@ -359,7 +364,7 @@ func (q *Queue) GetTail() *QueueTrackEntry {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (q *Queue) Remove(identifier audio.QueueIdentifier) bool {
|
||||
func (q *Queue) Remove(identifier queue.QueueIdentifier) bool {
|
||||
q.mutex.Lock()
|
||||
for i, e := range q.queue {
|
||||
if e.QueueIdentifier == identifier {
|
||||
|
@ -434,6 +439,45 @@ func (p *packetStreamFrame) Encode() []byte {
|
|||
}
|
||||
|
||||
func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Request) {
|
||||
if strings.HasSuffix(request.URL.Path, "mounts") {
|
||||
writer.Header().Set("Server", "MeteorLight/radio")
|
||||
writer.Header().Set("Accept-Ranges", "none")
|
||||
writer.Header().Set("Connection", "close")
|
||||
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
writer.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Icy-Metadata")
|
||||
writer.Header().Set("Access-Control-Expose-Headers", "Accept-Ranges, Server, Content-Type")
|
||||
|
||||
type mountData struct {
|
||||
Path string `json:"mount"`
|
||||
MimeType string `json:"mime"`
|
||||
FormatDescription string `json:"formatDescription"`
|
||||
SampleRate int `json:"sampleRate"`
|
||||
Channels int `json:"channels"`
|
||||
Listeners int `json:"listeners"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
}
|
||||
|
||||
var mounts []mountData
|
||||
|
||||
for _, mount := range q.mounts {
|
||||
mounts = append(mounts, mountData{
|
||||
Path: strings.TrimSuffix(request.URL.Path, "mounts") + mount.Mount,
|
||||
MimeType: mount.MimeType,
|
||||
SampleRate: mount.SampleRate,
|
||||
FormatDescription: mount.FormatDescription,
|
||||
Channels: mount.Channels,
|
||||
Listeners: len(mount.listeners),
|
||||
Options: mount.Options,
|
||||
})
|
||||
}
|
||||
|
||||
jsonBytes, _ := json.MarshalIndent(mounts, "", " ")
|
||||
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.Write(jsonBytes)
|
||||
return
|
||||
}
|
||||
|
||||
for _, mount := range q.mounts {
|
||||
if strings.HasSuffix(request.URL.Path, mount.Mount) {
|
||||
writer.Header().Set("Server", "MeteorLight/radio")
|
||||
|
@ -510,7 +554,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
|
|||
}
|
||||
writer.Header().Set("icy-br", fmt.Sprintf("%d", bitrate))
|
||||
writer.Header().Set("icy-sr", fmt.Sprintf("%d", mount.SampleRate))
|
||||
writer.Header().Set("icy-audio-info", fmt.Sprintf("ice-channels=%d;ice-samplerate=%d;ice-bitrate=%d", 2, mount.SampleRate, bitrate))
|
||||
writer.Header().Set("icy-audio-info", fmt.Sprintf("ice-channels=%d;ice-samplerate=%d;ice-bitrate=%d", mount.Channels, mount.SampleRate, bitrate))
|
||||
if q.config.Radio.Private {
|
||||
writer.Header().Set("icy-pub", "0")
|
||||
writer.Header().Set("icy-do-not-index", "1")
|
||||
|
@ -614,7 +658,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
|
|||
|
||||
headerBytes := new(bytes.Buffer)
|
||||
|
||||
binary.Write(headerBytes, binary.LittleEndian, int64(2))
|
||||
binary.Write(headerBytes, binary.LittleEndian, int64(mount.Channels))
|
||||
binary.Write(headerBytes, binary.LittleEndian, int64(mount.SampleRate))
|
||||
binary.Write(headerBytes, binary.LittleEndian, int32(len(mount.MimeType)))
|
||||
headerBytes.Write([]byte(mount.MimeType))
|
||||
|
|
Loading…
Reference in a new issue