Initial commit.
commit
966dd7b507
|
@ -0,0 +1,9 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"git.icedream.tech/icedream/uplink/internal/channels"
|
||||
)
|
||||
|
||||
type Authenticator interface {
|
||||
VerifyUsernameAndPassword(channel *channels.Channel, username string, password string) bool
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"git.icedream.tech/icedream/uplink/internal/channels"
|
||||
)
|
||||
|
||||
type DummyAuthenticator struct{}
|
||||
|
||||
func (authenticator *DummyAuthenticator) VerifyUsernameAndPassword(*channels.Channel, string, string) bool {
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package channels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"git.icedream.tech/icedream/uplink/internal"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
metadataLock sync.RWMutex
|
||||
metadata map[string]string
|
||||
metadataChannel chan map[string]string
|
||||
Name string
|
||||
Description string
|
||||
MimeType string
|
||||
InputStream *internal.Stream
|
||||
OutputStreams map[string]ChannelOutputStream
|
||||
}
|
||||
|
||||
func (channel *Channel) SetMetadata(data map[string]string) {
|
||||
channel.metadataLock.Lock()
|
||||
defer channel.metadataLock.Unlock()
|
||||
channel.metadata = data
|
||||
channel.metadataChannel <- data
|
||||
}
|
||||
|
||||
func (channel *Channel) Metadata(ctx context.Context) <-chan map[string]string {
|
||||
channel.metadataLock.Lock()
|
||||
defer channel.metadataLock.Unlock()
|
||||
metadataChan := make(chan map[string]string, 1)
|
||||
if channel.metadata != nil {
|
||||
metadataChan <- channel.metadata
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data := <-channel.metadataChannel:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func NewChannel() *Channel {
|
||||
return &Channel{
|
||||
metadataChannel: make(chan map[string]string),
|
||||
OutputStreams: map[string]ChannelOutputStream
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelOutputStream struct {
|
||||
*internal.Stream
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package channels
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ChannelManager struct {
|
||||
channels map[string]*Channel
|
||||
channelsLock sync.RWMutex
|
||||
|
||||
channelStreams map[string]*ChannelStreams
|
||||
channelStreamsLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (manager *ChannelManager) Channel(uuid string) *Channel {
|
||||
manager.channelsLock.RLock()
|
||||
defer manager.channelsLock.RUnlock()
|
||||
|
||||
channel, ok := manager.channels[uuid]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
func (manager *ChannelManager) Streams(uuid string) *ChannelStreams {
|
||||
manager.channelStreamsLock.RLock()
|
||||
defer manager.channelStreamsLock.RUnlock()
|
||||
|
||||
streams, ok := manager.channelStreams[uuid]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return streams
|
||||
}
|
||||
|
||||
func (manager *ChannelManager) Close(uuid string) (err error) {
|
||||
manager.channelsLock.Lock()
|
||||
defer manager.channelsLock.Unlock()
|
||||
|
||||
manager.channelStreamsLock.Lock()
|
||||
defer manager.channelStreamsLock.Unlock()
|
||||
|
||||
_, ok := manager.channels[uuid]
|
||||
if !ok {
|
||||
err = errors.New("channel uuid is not known")
|
||||
return
|
||||
}
|
||||
|
||||
delete(manager.channels, uuid)
|
||||
delete(manager.channelStreams, uuid)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (manager *ChannelManager) Open(uuid string) (channel *Channel, err error) {
|
||||
manager.channelsLock.Lock()
|
||||
defer manager.channelsLock.Unlock()
|
||||
|
||||
if _, ok := manager.channels[uuid]; ok {
|
||||
err = errors.New("channel uuid is already in use")
|
||||
return
|
||||
}
|
||||
|
||||
manager.channelStreamsLock.Lock()
|
||||
defer manager.channelStreamsLock.Unlock()
|
||||
|
||||
channel = new(Channel)
|
||||
manager.channels[uuid] = channel
|
||||
|
||||
manager.channelStreams[uuid] = new(ChannelStreams)
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package media
|
||||
|
||||
type StreamCodecInfo struct {
|
||||
CodecName string
|
||||
Type StreamMediaType
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"git.icedream.tech/icedream/uplink/internal/pubsub"
|
||||
)
|
||||
|
||||
type DemuxedStream struct {
|
||||
StreamId int
|
||||
Pts int64
|
||||
CodecInfo StreamCodecInfo
|
||||
pubsub *pubsub.PubSubWriter
|
||||
}
|
||||
|
||||
func (stream *DemuxedStream) Sub() io.ReadCloser {
|
||||
return stream.pubsub.Sub()
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"git.icedream.tech/icedream/uplink/internal/pubsub"
|
||||
|
||||
"github.com/3d0c/gmf"
|
||||
)
|
||||
|
||||
type Demuxer struct {
|
||||
streams chan *DemuxedStream
|
||||
err chan error
|
||||
}
|
||||
|
||||
func (demuxer *Demuxer) Error() <-chan error {
|
||||
return demuxer.err
|
||||
}
|
||||
|
||||
func (demuxer *Demuxer) Streams() <-chan *DemuxedStream {
|
||||
return demuxer.streams
|
||||
}
|
||||
|
||||
func Demux(r io.ReadCloser) (demuxer *Demuxer) {
|
||||
buffer := make([]byte, 8*1024)
|
||||
|
||||
demuxer = &Demuxer{
|
||||
err: make(chan error),
|
||||
streams: make(chan *DemuxedStream),
|
||||
}
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
select {
|
||||
case demuxer.err <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := gmf.NewCtx()
|
||||
defer ctx.CloseInputAndRelease()
|
||||
|
||||
avioCtx, err := gmf.NewAVIOContext(ctx, &gmf.AVIOHandlers{
|
||||
ReadPacket: func() ([]byte, int) {
|
||||
n, err := r.Read(buffer)
|
||||
//log.Println("DemuxStream: AVIOHandlers.ReadPacket:", n, err)
|
||||
if err != nil {
|
||||
n = -1
|
||||
}
|
||||
return buffer, n
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer avioCtx.Release()
|
||||
ctx.SetPb(avioCtx)
|
||||
ctx.OpenInput("")
|
||||
|
||||
// fmt.Println("=== FFMPEG DUMP OF INPUT ===")
|
||||
// ctx.Dump()
|
||||
// fmt.Println("============================")
|
||||
|
||||
// Find out order of streams and store info about them
|
||||
streams := []*gmf.Stream{}
|
||||
pubsubs := []*pubsub.PubSubWriter{}
|
||||
pubsubMap := map[int]io.WriteCloser{}
|
||||
for i := 0; i < ctx.StreamsCnt(); i++ {
|
||||
stream, err := ctx.GetStream(i)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
streamCodec := stream.CodecCtx()
|
||||
streams = append(streams, stream)
|
||||
|
||||
if stream.IsVideo() || stream.IsAudio() {
|
||||
ps := pubsub.NewPubSubWriter()
|
||||
dmxStream := &DemuxedStream{
|
||||
CodecInfo: StreamCodecInfo{
|
||||
CodecName: streamCodec.Codec().Name(),
|
||||
},
|
||||
Pts: stream.Pts,
|
||||
StreamId: i,
|
||||
pubsub: ps,
|
||||
}
|
||||
defer ps.Close()
|
||||
if stream.IsVideo() {
|
||||
dmxStream.CodecInfo.Type = Video
|
||||
} else {
|
||||
dmxStream.CodecInfo.Type = Audio
|
||||
}
|
||||
pubsubMap[i] = ps
|
||||
pubsubs = append(pubsubs, ps)
|
||||
demuxer.streams <- dmxStream
|
||||
}
|
||||
}
|
||||
|
||||
demuxer.err <- nil
|
||||
|
||||
packetsChan := ctx.GetNewPackets()
|
||||
for packet := range packetsChan {
|
||||
writer, shouldCapture := pubsubMap[packet.StreamIndex()]
|
||||
if !shouldCapture {
|
||||
packet.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
data := packet.Data()
|
||||
packet.Release()
|
||||
|
||||
if _, err := writer.Write(data); err != nil {
|
||||
log.Println("demuxer stream-out error:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Demux(t *testing.T) {
|
||||
Convey("Demux", t, func() {
|
||||
Convey("audio-only", func() {
|
||||
reader, _ := os.Open("mpthreetest.mp3")
|
||||
defer reader.Close()
|
||||
|
||||
demuxer := Demux(reader)
|
||||
var audioStream *DemuxedStream
|
||||
var err error
|
||||
forloop:
|
||||
for {
|
||||
select {
|
||||
case err = <-demuxer.Error():
|
||||
break forloop
|
||||
case stream := <-demuxer.Streams():
|
||||
So(audioStream, ShouldBeNil)
|
||||
So(stream.StreamId, ShouldEqual, 0)
|
||||
So(stream.Pts, ShouldEqual, 0)
|
||||
So(stream.CodecInfo.CodecName, ShouldEqual, "mp3")
|
||||
So(stream.CodecInfo.Type, ShouldEqual, Audio)
|
||||
audioStream = stream
|
||||
}
|
||||
}
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
audioReader := audioStream.Sub()
|
||||
|
||||
n, err := io.Copy(ioutil.Discard, audioReader)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("video and audio", func() {
|
||||
reader, _ := os.Open("small.ogv")
|
||||
defer reader.Close()
|
||||
|
||||
demuxer := Demux(reader)
|
||||
var audioStream, videoStream *DemuxedStream
|
||||
var err error
|
||||
forloop:
|
||||
for {
|
||||
select {
|
||||
case err = <-demuxer.Error():
|
||||
break forloop
|
||||
case stream := <-demuxer.Streams():
|
||||
So(stream.Pts, ShouldEqual, 0)
|
||||
switch stream.CodecInfo.Type {
|
||||
case Audio:
|
||||
So(stream.StreamId, ShouldEqual, 1)
|
||||
So(audioStream, ShouldBeNil)
|
||||
So(stream.CodecInfo.CodecName, ShouldEqual, "vorbis")
|
||||
audioStream = stream
|
||||
case Video:
|
||||
So(stream.StreamId, ShouldEqual, 0)
|
||||
So(videoStream, ShouldBeNil)
|
||||
So(stream.CodecInfo.CodecName, ShouldEqual, "theora")
|
||||
videoStream = stream
|
||||
}
|
||||
}
|
||||
}
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
audioReader := audioStream.Sub()
|
||||
videoReader := videoStream.Sub()
|
||||
|
||||
var videoN, audioN int64
|
||||
var videoErr, audioErr error
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
audioN, audioErr = io.Copy(ioutil.Discard, audioReader)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
videoN, videoErr = io.Copy(ioutil.Discard, videoReader)
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
t.Log("Audio read:", audioN)
|
||||
t.Log("Video read:", videoN)
|
||||
|
||||
So(audioErr, ShouldBeNil)
|
||||
So(audioN, ShouldBeGreaterThan, 0)
|
||||
So(videoErr, ShouldBeNil)
|
||||
So(videoN, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/3d0c/gmf"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
gmf.LogSetLevel(gmf.AV_LOG_DEBUG)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package media
|
||||
|
||||
type StreamMediaType byte
|
||||
|
||||
const (
|
||||
Audio StreamMediaType = iota
|
||||
Video
|
||||
)
|
Binary file not shown.
|
@ -0,0 +1,120 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/3d0c/gmf"
|
||||
)
|
||||
|
||||
type Muxer struct {
|
||||
writers []io.WriteCloser
|
||||
err chan error
|
||||
}
|
||||
|
||||
func Mux(muxer string, readers ...io.ReadCloser) (retval io.Reader) {
|
||||
retval, w := io.Pipe()
|
||||
|
||||
type Instance struct {
|
||||
Ctx *gmf.FmtCtx
|
||||
AvioCtx *gmf.AVIOContext
|
||||
}
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
w.CloseWithError(err)
|
||||
} else {
|
||||
w.CloseWithError(io.EOF)
|
||||
}
|
||||
}()
|
||||
|
||||
output := Instance{}
|
||||
output.Ctx, err = gmf.NewOutputCtxWithFormatName("test.dat", muxer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer output.Ctx.CloseOutputAndRelease()
|
||||
if output.AvioCtx, err = gmf.NewAVIOContext(output.Ctx, &gmf.AVIOHandlers{
|
||||
WritePacket: func(p []byte) {
|
||||
log.Println("WritePacket:", p)
|
||||
n, err := w.Write(p)
|
||||
log.Println("WritePacket:", n, err)
|
||||
},
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
defer output.AvioCtx.Release()
|
||||
output.Ctx.SetPb(output.AvioCtx)
|
||||
|
||||
inputs := make([]Instance, len(readers))
|
||||
|
||||
for i, r := range readers {
|
||||
input := Instance{Ctx: gmf.NewCtx()}
|
||||
defer input.Ctx.CloseInputAndRelease()
|
||||
buffer := make([]byte, 8*1024)
|
||||
if input.AvioCtx, err = gmf.NewAVIOContext(input.Ctx, &gmf.AVIOHandlers{
|
||||
ReadPacket: func(r io.Reader) func() ([]byte, int) {
|
||||
return func() ([]byte, int) {
|
||||
n, err := r.Read(buffer)
|
||||
if err != nil {
|
||||
n = -1
|
||||
}
|
||||
return buffer, n
|
||||
}
|
||||
}(r),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
defer input.AvioCtx.Release()
|
||||
input.Ctx.SetPb(input.AvioCtx)
|
||||
if err = input.Ctx.OpenInput(""); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inputs[i] = input
|
||||
|
||||
var stream *gmf.Stream
|
||||
if stream, err = input.Ctx.GetStream(0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stream, err = output.Ctx.AddStreamWithCodeCtx(stream.CodecCtx())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = output.Ctx.WriteHeader(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//first := true
|
||||
cases := make([]reflect.SelectCase, len(readers))
|
||||
for i, c := range inputs {
|
||||
cases[i] = reflect.SelectCase{
|
||||
Dir: reflect.SelectRecv,
|
||||
Chan: reflect.ValueOf(c.Ctx.GetNewPackets()),
|
||||
}
|
||||
}
|
||||
for err == nil {
|
||||
streamIndex, packetVal, ok := reflect.Select(cases)
|
||||
if !ok {
|
||||
break // some stream has been closed, just close them all
|
||||
}
|
||||
packet := packetVal.Interface().(*gmf.Packet)
|
||||
packet.SetStreamIndex(streamIndex)
|
||||
err = output.Ctx.WritePacket(packet)
|
||||
packet.Release()
|
||||
}
|
||||
log.Println("Bailing out")
|
||||
for _, r := range readers {
|
||||
r.Close()
|
||||
}
|
||||
output.Ctx.WriteTrailer()
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package media
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Muxer(t *testing.T) {
|
||||
Convey("Muxer", t, func() {
|
||||
Convey("audio-only", func() {
|
||||
reader, _ := os.Open("mpthreetest.mp3")
|
||||
defer reader.Close()
|
||||
|
||||
demuxer := Demux(reader)
|
||||
var audioStream *DemuxedStream
|
||||
var err error
|
||||
forloop:
|
||||
for {
|
||||
select {
|
||||
case err = <-demuxer.Error():
|
||||
break forloop
|
||||
case stream := <-demuxer.Streams():
|
||||
So(audioStream, ShouldBeNil)
|
||||
So(stream.StreamId, ShouldEqual, 0)
|
||||
So(stream.Pts, ShouldEqual, 0)
|
||||
So(stream.CodecInfo.CodecName, ShouldEqual, "mp3")
|
||||
So(stream.CodecInfo.Type, ShouldEqual, Audio)
|
||||
audioStream = stream
|
||||
}
|
||||
}
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
audioReader := audioStream.Sub()
|
||||
|
||||
muxer := Mux("mpegts", audioReader)
|
||||
|
||||
n, err := io.Copy(ioutil.Discard, muxer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,84 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func quote(text string) string {
|
||||
text = strings.Replace(text, "\\", "\\\\", -1)
|
||||
text = strings.Replace(text, "'", "\\'", -1)
|
||||
text = "'" + text + "'"
|
||||
return text
|
||||
}
|
||||
|
||||
type MetadataInjector struct {
|
||||
io.Reader
|
||||
MetadataInterval int
|
||||
blockOffset int
|
||||
Metadata map[string]string
|
||||
metadataBuf []byte
|
||||
}
|
||||
|
||||
func NewMetadataInjector(r io.Reader, metadataInterval int) *MetadataInjector {
|
||||
return &MetadataInjector{
|
||||
Reader: r,
|
||||
MetadataInterval: metadataInterval,
|
||||
}
|
||||
}
|
||||
|
||||
func (mi *MetadataInjector) generateMetadataBuf() {
|
||||
mstr := ""
|
||||
if mi.Metadata != nil {
|
||||
for key, value := range mi.Metadata {
|
||||
mstr += fmt.Sprintf("%s=%s;", key, quote(value))
|
||||
}
|
||||
}
|
||||
if len(mstr) > 16*256-1 {
|
||||
mstr = mstr[0 : 16*256]
|
||||
}
|
||||
lengthDiv := int(math.Ceil(float64(len(mstr)) / 16))
|
||||
lengthByte := byte(lengthDiv)
|
||||
mi.metadataBuf = make([]byte, lengthDiv*16+1)
|
||||
mi.metadataBuf[0] = lengthByte
|
||||
copy(mi.metadataBuf[1:], []byte(mstr))
|
||||
}
|
||||
|
||||
func (mi *MetadataInjector) Read(data []byte) (n int, err error) {
|
||||
if mi.metadataBuf != nil && len(mi.metadataBuf) > 0 {
|
||||
bytesToRead := len(data)
|
||||
if bytesToRead < len(mi.metadataBuf) {
|
||||
// only read as much as possible
|
||||
copy(data, mi.metadataBuf[0:bytesToRead])
|
||||
n = bytesToRead
|
||||
mi.metadataBuf = mi.metadataBuf[bytesToRead:]
|
||||
return
|
||||
}
|
||||
// read everything
|
||||
copy(data, mi.metadataBuf)
|
||||
n = len(mi.metadataBuf)
|
||||
mi.metadataBuf = nil
|
||||
return
|
||||
}
|
||||
|
||||
bytesToRead := mi.MetadataInterval - mi.blockOffset
|
||||
if bytesToRead > len(data) {
|
||||
bytesToRead = len(data)
|
||||
}
|
||||
if bytesToRead > 0 {
|
||||
n, err = mi.Reader.Read(data[0:bytesToRead])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mi.blockOffset += n
|
||||
}
|
||||
if mi.blockOffset == mi.MetadataInterval {
|
||||
mi.generateMetadataBuf() // will be read in on next Read call
|
||||
mi.blockOffset = 0
|
||||
} else if mi.blockOffset > mi.MetadataInterval {
|
||||
panic("block offset higher than metadata interval, logical error")
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_MetadataInjector(t *testing.T) {
|
||||
reader := bytes.NewReader(make([]byte, 1024))
|
||||
buffer := make([]byte, 128)
|
||||
|
||||
Convey("MetadataInjector", t, func() {
|
||||
mi := NewMetadataInjector(reader, 192)
|
||||
|
||||
// 128
|
||||
// 64
|
||||
// [metadata]
|
||||
// 128
|
||||
// 64
|
||||
// [metadata]
|
||||
|
||||
n, err := mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 128)
|
||||
|
||||
n, err = mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 64)
|
||||
|
||||
n, err = mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 1)
|
||||
So(buffer[0], ShouldEqual, 0) // no metadata => zero length!
|
||||
|
||||
mi.Metadata = map[string]string{
|
||||
"StreamTitle": "Testing",
|
||||
}
|
||||
|
||||
n, err = mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 128)
|
||||
|
||||
n, err = mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 64)
|
||||
|
||||
n, err = mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 1+32)
|
||||
So(buffer[0], ShouldEqual, 2) // "StreamTitle='Testing';" => 22 bytes => quantized to 2 * 16 bytes
|
||||
So(string(buffer[1:23]), ShouldEqual, "StreamTitle='Testing';")
|
||||
So(buffer[1:23], ShouldResemble, []byte("StreamTitle='Testing';"))
|
||||
So(buffer[24:32], ShouldResemble, make([]byte, 8)) // 8 zeroes
|
||||
|
||||
n, err = mi.Read(buffer)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 128)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
cskrpubsub "github.com/cskr/pubsub"
|
||||
)
|
||||
|
||||
type PubSubReader struct {
|
||||
pubsub *cskrpubsub.PubSub
|
||||
channel chan interface{}
|
||||
buf []byte
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (r *PubSubReader) Read(p []byte) (n int, err error) {
|
||||
if r.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
if r.buf == nil {
|
||||
data, ok := <-r.channel
|
||||
if !ok {
|
||||
r.closed = true
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
dataBytes := data.([]byte)
|
||||
if len(dataBytes) == 0 {
|
||||
return
|
||||
}
|
||||
r.buf = dataBytes
|
||||
}
|
||||
|
||||
if r.buf != nil {
|
||||
n = len(p)
|
||||
if len(r.buf) < n {
|
||||
n = len(r.buf)
|
||||
}
|
||||
copy(p, r.buf[0:n])
|
||||
if len(r.buf) == n {
|
||||
r.buf = nil
|
||||
} else {
|
||||
r.buf = r.buf[n:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *PubSubReader) Close() (err error) {
|
||||
r.closed = true
|
||||
r.pubsub.Unsub(r.channel, "")
|
||||
return
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
cskrpubsub "github.com/cskr/pubsub"
|
||||
)
|
||||
|
||||
type PubSubWriter struct {
|
||||
*cskrpubsub.PubSub
|
||||
topic string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewPubSubWriter() *PubSubWriter {
|
||||
pipe := new(PubSubWriter)
|
||||
pipe.PubSub = cskrpubsub.New(1)
|
||||
return pipe
|
||||
}
|
||||
|
||||
func (pipe *PubSubWriter) Write(p []byte) (n int, err error) {
|
||||
if pipe.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
pipe.PubSub.Pub(p, "")
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
|
||||
func (pipe *PubSubWriter) Close() (err error) {
|
||||
if pipe.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
pipe.PubSub.Shutdown()
|
||||
pipe.closed = true
|
||||
return
|
||||
}
|
||||
|
||||
func (pipe *PubSubWriter) Sub() io.ReadCloser {
|
||||
return &PubSubReader{
|
||||
channel: pipe.PubSub.Sub(""),
|
||||
pubsub: pipe.PubSub,
|
||||
closed: pipe.closed,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_PubSubWriter(t *testing.T) {
|
||||
Convey("PubSubWriter", t, func() {
|
||||
Convey("without subscribers", func() {
|
||||
psw := NewPubSubWriter()
|
||||
|
||||
n, err := psw.Write([]byte{})
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 0)
|
||||
|
||||
n, err = psw.Write([]byte{0, 0, 0, 0})
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 4)
|
||||
|
||||
So(psw.Close(), ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("with subscribers (writer-side close)", func() {
|
||||
buf := make([]byte, 2)
|
||||
|
||||
psw := NewPubSubWriter()
|
||||
psr := psw.Sub()
|
||||
|
||||
n, err := psw.Write([]byte{})
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 0)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 0)
|
||||
|
||||
n, err = psw.Write([]byte{0, 0, 0, 0})
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 4)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 2)
|
||||
So(psr.(*PubSubReader).buf, ShouldHaveLength, 2)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 2)
|
||||
|
||||
So(psw.Close(), ShouldBeNil)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldEqual, io.EOF)
|
||||
So(n, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("with subscribers (reader-side close)", func() {
|
||||
buf := make([]byte, 2)
|
||||
|
||||
psw := NewPubSubWriter()
|
||||
psr := psw.Sub()
|
||||
|
||||
n, err := psw.Write([]byte{0, 0, 0, 0})
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 4)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 2)
|
||||
So(psr.(*PubSubReader).buf, ShouldHaveLength, 2)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 2)
|
||||
|
||||
So(psr.Close(), ShouldBeNil)
|
||||
|
||||
n, err = psw.Write([]byte{0, 0, 0, 0})
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 4)
|
||||
|
||||
n, err = psr.Read(buf)
|
||||
So(err, ShouldEqual, io.EOF)
|
||||
So(n, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
_ "git.icedream.tech/icedream/uplink/internal"
|
||||
"git.icedream.tech/icedream/uplink/internal/authentication"
|
||||
channels "git.icedream.tech/icedream/uplink/internal/channels"
|
||||
_ "git.icedream.tech/icedream/uplink/internal/transcoders"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Authenticator authentication.Authenticator
|
||||
ChannelManager *channels.ChannelManager
|
||||
}
|
||||
|
||||
func (server *Server) Run() {
|
||||
httpServer := new(http.Server)
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/:channel", func(ctx *gin.Context) {
|
||||
channel := server.ChannelManager.Channel(ctx.Param("channel"))
|
||||
if channel == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
if user, password, ok := ctx.Request.BasicAuth(); ok {
|
||||
if !server.Authenticator.VerifyUsernameAndPassword(channel, user, password) {
|
||||
ctx.Status(401)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.Status(401)
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
router.GET("/:channel", func(ctx *gin.Context) {
|
||||
channel := server.ChannelManager.Channel(ctx.Param("channel"))
|
||||
if channel == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
router.GET("/:channel/:stream", func(ctx *gin.Context) {
|
||||
channel := server.ChannelManager.Channel(ctx.Param("channel"))
|
||||
if channel == nil {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
httpServer.Handler = router
|
||||
httpServer.Addr = ":8000"
|
||||
httpServer.ListenAndServe()
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package sources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SineStream struct {
|
||||
Frequency float64
|
||||
Samplerate int
|
||||
State uint64
|
||||
Beep bool
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func makeSample(channelValues ...float64) (ret []byte) {
|
||||
// target format: s16le
|
||||
buf := new(bytes.Buffer)
|
||||
for _, value := range channelValues {
|
||||
intValue := int16(value * math.MaxInt16)
|
||||
binary.Write(buf, binary.LittleEndian, intValue)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (stream *SineStream) Read(data []byte) (n int, err error) {
|
||||
n = 0
|
||||
for (len(data) - n) >= 4 { // at least 2 bytes per channel need to be available
|
||||
var sampleValue float64
|
||||
if stream.Beep && stream.State%uint64(stream.Samplerate) > uint64(float64(stream.Samplerate)*0.15) {
|
||||
sampleValue = 0
|
||||
} else {
|
||||
sampleValue = math.Sin(stream.Frequency * 2. * math.Pi * (float64(stream.State) / float64(stream.Samplerate)))
|
||||
}
|
||||
|
||||
b := makeSample(sampleValue, sampleValue)
|
||||
copy(data[n:], b)
|
||||
|
||||
n += len(b)
|
||||
|
||||
targetTime := stream.Timestamp.
|
||||
Add(time.Duration(float64(time.Second) * float64(stream.State) / float64(stream.Samplerate)))
|
||||
delay := targetTime.Sub(time.Now())
|
||||
/*log.Println("state", stream.State, "value", sampleValue, "time", targetTime, "delay", delay)
|
||||
time.Sleep(time.Second)*/
|
||||
if delay > 0 {
|
||||
<-time.After(delay)
|
||||
}
|
||||
|
||||
/*if stream.State%uint64(stream.Samplerate) == 0 {
|
||||
log.Println("state", stream.State, "value", sampleValue, "time", targetTime, "delay", delay)
|
||||
}*/
|
||||
|
||||
stream.State++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package embedded
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
type Configurator struct {
|
||||
database *bolt.DB
|
||||
}
|
||||
|
||||
func (configurator *Configurator) CreateChannel(uuid string) {
|
||||
|
||||
}
|
||||
|
||||
func (configurator *Configurator) Channels() {
|
||||
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type StreamDataChannel chan []byte
|
||||
type StreamCancelChannel chan interface{}
|
||||
|
||||
type Stream struct {
|
||||
burstSize int
|
||||
|
||||
burstLock sync.RWMutex
|
||||
burst []byte
|
||||
|
||||
subscribersLock sync.RWMutex
|
||||
subscribers []io.WriteCloser
|
||||
}
|
||||
|
||||
func NewStream(burstSize int) *Stream {
|
||||
return &Stream{
|
||||
burstSize: burstSize,
|
||||
|
||||
subscribers: []io.WriteCloser{},
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *Stream) Subscribe(wc io.WriteCloser) {
|
||||
go func(wc io.WriteCloser) {
|
||||
stream.subscribersLock.Lock()
|
||||
defer stream.subscribersLock.Unlock()
|
||||
|
||||
// send burst data
|
||||
stream.burstLock.RLock()
|
||||
defer stream.burstLock.RUnlock()
|
||||
if stream.burst != nil {
|
||||
burstToSend := len(stream.burst)
|
||||
for burstToSend > 0 {
|
||||
burstSent, err := wc.Write(stream.burst)
|
||||
if err != nil {
|
||||
stream.unsubscribe(wc)
|
||||
if err == io.EOF {
|
||||
return // just end prematurely
|
||||
}
|
||||
log.Println("WARNING - Can not send burst data to subscriber:", err)
|
||||
return
|
||||
}
|
||||
burstToSend -= burstSent
|
||||
}
|
||||
}
|
||||
|
||||
// now subscribe to live broadcast
|
||||
stream.subscribers = append(stream.subscribers, wc)
|
||||
}(wc)
|
||||
runtime.Gosched()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (stream *Stream) unsubscribe(wc io.WriteCloser) error {
|
||||
stream.subscribersLock.Lock()
|
||||
defer stream.subscribersLock.Unlock()
|
||||
return stream.unsubscribeNoLock(wc)
|
||||
}
|
||||
|
||||
func (stream *Stream) unsubscribeNoLock(wc io.WriteCloser) error {
|
||||
// log.Println("About to remove subscriber", wc)
|
||||
for index, subscriber := range stream.subscribers {
|
||||
if subscriber == wc {
|
||||
// log.Println("Removing subscriber", wc, "at", index)
|
||||
stream.subscribers = append(stream.subscribers[0:index], stream.subscribers[index+1:]...)
|
||||
// log.Println("We now have", len(stream.subscribers), "subscribers")
|
||||
return subscriber.Close()
|
||||
}
|
||||
}
|
||||
return errors.New("Tried to unsubscribe stream that is not registered as subscriber")
|
||||
}
|
||||
|
||||
func (stream *Stream) SubscriberCount() int {
|
||||
stream.subscribersLock.RLock()
|
||||
defer stream.subscribersLock.RUnlock()
|
||||
|
||||
return len(stream.subscribers)
|
||||
}
|
||||
|
||||
func (stream *Stream) Write(data []byte) (n int, err error) {
|
||||
dataLength := len(data)
|
||||
|
||||
stream.burstLock.Lock()
|
||||
defer stream.burstLock.Unlock()
|
||||
|
||||
stream.subscribersLock.RLock()
|
||||
subscribers := make([]io.WriteCloser, len(stream.subscribers))
|
||||
copy(subscribers, stream.subscribers)
|
||||
defer stream.subscribersLock.RUnlock()
|
||||
|
||||
// Write data out to subscribers
|
||||
for _, subscriber := range subscribers {
|
||||
go func(subscriber io.WriteCloser) {
|
||||
stream.subscribersLock.Lock()
|
||||
defer stream.subscribersLock.Unlock()
|
||||
// TODO - absolutely ensure data is sent in the correct order
|
||||
totalWritten := 0
|
||||
for totalWritten < dataLength {
|
||||
currentWritten, err := subscriber.Write(data[totalWritten:])
|
||||
if err != nil {
|
||||
// just remove subscriber and go to next one
|
||||
// log.Println("WARNING: Failed to write data to subscriber, removing subscriber:", err)
|
||||
stream.unsubscribeNoLock(subscriber)
|
||||
return
|
||||
}
|
||||
totalWritten += currentWritten
|
||||
}
|
||||
}(subscriber)
|
||||
}
|
||||
runtime.Gosched()
|
||||
|
||||
// Store data into burst buffer
|
||||
if stream.burstSize > 0 {
|
||||
if stream.burst == nil {
|
||||
stream.burst = []byte{}
|
||||
}
|
||||
newBurst := append(stream.burst, data...)
|
||||
if len(newBurst) > stream.burstSize {
|
||||
newBurst = newBurst[len(newBurst)-stream.burstSize:]
|
||||
}
|
||||
stream.burst = newBurst
|
||||
}
|
||||
|
||||
n = len(data)
|
||||
err = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (stream *Stream) Close() error {
|
||||
stream.subscribersLock.RLock()
|
||||
defer stream.subscribersLock.RUnlock()
|
||||
|
||||
for _, subscriber := range stream.subscribers {
|
||||
if err := subscriber.Close(); err != nil {
|
||||
log.Println("WARNING: Failed to close subscriber stream, ignoring:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Test_Stream(t *testing.T) {
|
||||
Convey("Stream", t, func() {
|
||||
stream := NewStream(4)
|
||||
|
||||
// it writes burst prefill
|
||||
n, err := stream.Write([]byte{4, 5, 6, 7})
|
||||
So(n, ShouldEqual, 4)
|
||||
So(err, ShouldBeNil)
|
||||
So(stream.burst, ShouldResemble, []byte{4, 5, 6, 7})
|
||||
|
||||
// it writes normally
|
||||
n, err = stream.Write([]byte{0, 1, 2})
|
||||
So(n, ShouldEqual, 3)
|
||||
So(err, ShouldBeNil)
|
||||
So(stream.burst, ShouldResemble, []byte{7, 0, 1, 2})
|
||||
|
||||
// it has working subscriptions
|
||||
r, w := io.Pipe()
|
||||
stream.Subscribe(w)
|
||||
|
||||
//So(target, ShouldHaveLength, 4)
|
||||
data := make([]byte, 128)
|
||||
n, err = r.Read(data)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 4)
|
||||
So(data[0:4], ShouldResemble, []byte{7, 0, 1, 2})
|
||||
|
||||
n, err = stream.Write([]byte{0, 0, 0, 0, 1, 0, 255, 0})
|
||||
So(n, ShouldEqual, 8)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
//So(target, ShouldHaveLength, 8)
|
||||
n, err = r.Read(data)
|
||||
So(err, ShouldBeNil)
|
||||
So(n, ShouldEqual, 8)
|
||||
So(data[0:8], ShouldResemble, []byte{0, 0, 0, 0, 1, 0, 255, 0})
|
||||
|
||||
runtime.Gosched()
|
||||
|
||||
r.Close()
|
||||
n, err = r.Read(data)
|
||||
So(err, ShouldEqual, io.ErrClosedPipe)
|
||||
So(n, ShouldEqual, 0)
|
||||
|
||||
n, err = stream.Write([]byte{8})
|
||||
So(n, ShouldEqual, 1)
|
||||
So(err, ShouldBeNil)
|
||||
So(stream.SubscriberCount(), ShouldEqual, 0)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type StreamReader struct {
|
||||
dataChan <-chan []byte
|
||||
cancelChan chan<- interface{}
|
||||
extraData []byte
|
||||
}
|
||||
|
||||
func NewStreamReader(stream *Stream) io.ReadCloser {
|
||||
|
||||
r, w := io.Pipe()
|
||||
|
||||
stream.Subscribe(w)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (reader *StreamReader) Close() error {
|
||||
reader.cancelChan <- nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (reader *StreamReader) Read(data []byte) (n int, err error) {
|
||||
n = 0
|
||||
ok := false
|
||||
|
||||
// Do we have a buffer to read data from?
|
||||
if reader.extraData == nil {
|
||||
// Fill our buffer with new data.
|
||||
reader.extraData, ok = <-reader.dataChan
|
||||
if !ok { // EOF?
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Target array too small to fit all of our data? Keep the rest.
|
||||
if len(reader.extraData) > len(data) {
|
||||
copy(data, reader.extraData[0:len(data)])
|
||||
reader.extraData = reader.extraData[len(data):]
|
||||
n = len(data)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy all of the buffer and reset the buffer.
|
||||
copy(data, reader.extraData)
|
||||
n = len(reader.extraData)
|
||||
reader.extraData = nil
|
||||
return
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package transcoders
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"git.icedream.tech/icedream/uplink/internal"
|
||||
"git.icedream.tech/icedream/uplink/internal/transcoders/options"
|
||||
)
|
||||
|
||||
type Transcoder interface {
|
||||
Options() map[string]options.TranscoderOptionType
|
||||
New(options map[string]interface{}) *TranscoderInstance
|
||||
}
|
||||
|
||||
type TranscoderInstance interface {
|
||||
io.WriteCloser
|
||||
Init(out *internal.Stream)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package lametranscoder
|
||||
|
||||
import (
|
||||
"github.com/viert/lame"
|
||||
|
||||
"git.icedream.tech/icedream/uplink/internal"
|
||||
"git.icedream.tech/icedream/uplink/internal/transcoders"
|
||||
"git.icedream.tech/icedream/uplink/internal/transcoders/options"
|
||||
)
|
||||
|
||||
var transcoderOptions = map[string]options.TranscoderOptionType{
|
||||
"bitrate": &options.Int64TranscoderOption{DefaultValue: 128, Min: 32, Max: 320},
|
||||
"quality": &options.Int64TranscoderOption{DefaultValue: 1, Min: 0, Max: 9},
|
||||
}
|
||||
|
||||
type Transcoder struct{}
|
||||
|
||||
func (transcoder *Transcoder) Options() map[string]options.TranscoderOptionType {
|
||||
return transcoderOptions
|
||||
}
|
||||
|
||||
func (transcoder *Transcoder) New(options map[string]interface{}) transcoders.TranscoderInstance {
|
||||
return &TranscoderInstance{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
type TranscoderInstance struct {
|
||||
options map[string]interface{}
|
||||
*lame.LameWriter
|
||||
}
|
||||
|
||||
func (instance *TranscoderInstance) Init(out *internal.Stream) {
|
||||
instance.LameWriter = lame.NewWriter(out)
|
||||
instance.LameWriter.Encoder.SetBitrate(int(instance.options["bitrate"].(int64)))
|
||||
instance.LameWriter.Encoder.SetQuality(int(instance.options["quality"].(int64)))
|
||||
instance.LameWriter.Encoder.SetInSamplerate(samplerate)
|
||||
instance.LameWriter.Encoder.SetNumChannels(channels)
|
||||
instance.LameWriter.Encoder.InitParams()
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
## Descriptor
|
||||
|
||||
```json
|
||||
{
|
||||
"options": {
|
||||
"option-1": { "type": "boolean", "defaultValue": false },
|
||||
"option-2": { "type": "string", "defaultValue": "blubb blabb" },
|
||||
"option-3": { "type": "int32", "defaultValue": 50, "min": 0, "max": 100 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data
|
||||
|
||||
```json
|
||||
{
|
||||
"options": {
|
||||
"option-1": true,
|
||||
"option-2": "hello world",
|
||||
"option-3": 50
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,26 @@
|
|||
package options
|
||||
|
||||
import "errors"
|
||||
|
||||
type BooleanTranscoderOption struct {
|
||||
DefaultValue bool
|
||||
}
|
||||
|
||||
func (option *BooleanTranscoderOption) IsRequired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (option *BooleanTranscoderOption) Default() interface{} {
|
||||
return option.DefaultValue
|
||||
}
|
||||
|
||||
func (option *BooleanTranscoderOption) Validate(value interface{}) (err error) {
|
||||
_, ok := value.(bool)
|
||||
if !ok {
|
||||
err = errors.New("value is not a boolean")
|
||||
return
|
||||
}
|
||||
|
||||
err = nil
|
||||
return
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package options
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Int64TranscoderOption struct {
|
||||
DefaultValue int64
|
||||
Required bool
|
||||
Max int64
|
||||
Min int64
|
||||
}
|
||||
|
||||
func (option *Int64TranscoderOption) IsRequired() bool {
|
||||
return option.Required
|
||||
}
|
||||
|
||||
func (option *Int64TranscoderOption) Default() interface{} {
|
||||
return option.DefaultValue
|
||||
}
|
||||
|
||||
func (option *Int64TranscoderOption) Validate(value interface{}) (err error) {
|
||||
intValue, ok := value.(int64)
|
||||
if !ok {
|
||||
err = errors.New("value is not a 64-bit integer")
|
||||
return
|
||||
}
|
||||
|
||||
if intValue > option.Max {
|
||||
err = errors.New("number is too big")
|
||||
return
|
||||
}
|
||||
|
||||
if intValue < option.Min {
|
||||
err = errors.New("number is too small")
|
||||
return
|
||||
}
|
||||
|
||||
err = nil
|
||||
return
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package options
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type StringCharacterRange struct {
|
||||
Min rune
|
||||
Max rune
|
||||
}
|
||||
|
||||
func (crange *StringCharacterRange) Validate(value rune) bool {
|
||||
return value >= crange.Min && value <= crange.Max
|
||||
}
|
||||
|
||||
type StringTranscoderOption struct {
|
||||
DefaultValue string
|
||||
Required bool
|
||||
MaxLength int
|
||||
MinLength int
|
||||
AllowedCharacterRanges []StringCharacterRange
|
||||
}
|
||||
|
||||
func (option *StringTranscoderOption) IsRequired() bool {
|
||||
return option.Required
|
||||
}
|
||||
|
||||
func (option *StringTranscoderOption) Validate(value interface{}) (err error) {
|
||||
stringValue, ok := value.(string)
|
||||
if !ok {
|
||||
err = errors.New("value is not a string")
|
||||
return
|
||||
}
|
||||
|
||||
if option.MaxLength > 0 && len(stringValue) > option.MaxLength {
|
||||
err = errors.New("text is too long")
|
||||
return
|
||||
}
|
||||
|
||||
if len(stringValue) < option.MinLength {
|
||||
err = errors.New("text is too short")
|
||||
return
|
||||
}
|
||||
|
||||
if option.AllowedCharacterRanges != nil {
|
||||
for index, character := range stringValue {
|
||||
for _, crange := range option.AllowedCharacterRanges {
|
||||
if !crange.Validate(character) {
|
||||
err = fmt.Errorf("character \"%c\" at position %d is outside of valid character range", character, index)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = nil
|
||||
return
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package options
|
||||
|
||||
import "errors"
|
||||
|
||||
type TranscoderOptionTree struct {
|
||||
optionTypes map[string]TranscoderOptionType
|
||||
}
|
||||
|
||||
func (tree *TranscoderOptionTree) GenerateDefaultValues() (retval map[string]interface{}) {
|
||||
retval = map[string]interface{}{}
|
||||
for key, optionType := range tree.optionTypes {
|
||||
retval[key] = optionType.Default()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tree *TranscoderOptionTree) ValidateValues(values map[string]interface{}) (errs map[string]error) {
|
||||
for key, optionType := range tree.optionTypes {
|
||||
value, ok := tree.optionTypes[key]
|
||||
if !ok {
|
||||
if optionType.IsRequired() {
|
||||
errs[key] = errors.New("missing required option")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := optionType.Validate(value); err != nil {
|
||||
errs[key] = err
|
||||
}
|
||||
}
|
||||
|
||||
for key := range values {
|
||||
_, ok := tree.optionTypes[key]
|
||||
if !ok {
|
||||
errs[key] = errors.New("unrecognized option")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package options
|
||||
|
||||
type TranscoderOptionType interface {
|
||||
Validate(value interface{}) error
|
||||
IsRequired() bool
|
||||
Default() interface{}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package transcoders
|
|
@ -0,0 +1,119 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.icedream.tech/icedream/uplink/internal"
|
||||
"git.icedream.tech/icedream/uplink/internal/sources"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/viert/lame"
|
||||
)
|
||||
|
||||
func main() {
|
||||
stream := internal.NewStream(128 * 1024)
|
||||
|
||||
wr := lame.NewWriter(stream)
|
||||
wr.Encoder.SetBitrate(192)
|
||||
wr.Encoder.SetQuality(1)
|
||||
wr.Encoder.SetInSamplerate(44100)
|
||||
wr.Encoder.SetNumChannels(2)
|
||||
wr.Encoder.InitParams()
|
||||
|
||||
go func() {
|
||||
log.Println("Sine stream goroutine started")
|
||||
|
||||
sine := new(sources.SineStream)
|
||||
sine.Samplerate = 44100
|
||||
sine.Frequency = 990
|
||||
sine.Beep = true
|
||||
sine.Timestamp = time.Now()
|
||||
|
||||
log.Println("Will now broadcast sine stream")
|
||||
n, err := io.Copy(wr, sine)
|
||||
if err != nil {
|
||||
log.Fatal("Sine stream copy failed:", err)
|
||||
}
|
||||
log.Println("Sine stream finished, written", humanize.Bytes(uint64(n)), "bytes")
|
||||
}()
|
||||
|
||||
server := new(http.Server)
|
||||
mux := mux.NewRouter()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Got a listener")
|
||||
|
||||
w.Header().Set("content-type", "audio/mpeg")
|
||||
w.Header().Set("server", "Uplink/0.0.0")
|
||||
if r.Header.Get("icy-metadata") == "1" {
|
||||
w.Header().Set("icy-metadata", "1")
|
||||
w.Header().Set("icy-metaint", strconv.Itoa(2*1024))
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
|
||||
cancel := w.(http.CloseNotifier).CloseNotify()
|
||||
|
||||
sr := internal.NewStreamReader(stream)
|
||||
var n int64
|
||||
var err error
|
||||
if r.Header.Get("icy-metadata") == "1" {
|
||||
mstream := internal.NewMetadataInjector(sr, 2*1024)
|
||||
mstream.Metadata = map[string]string{
|
||||
"StreamTitle": "beep",
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-cancel:
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
mstream.Metadata["StreamTitle"] = "beep - time: " + time.Now().String()
|
||||
}
|
||||
}
|
||||
}()
|
||||
mstream.Metadata = map[string]string{
|
||||
"StreamTitle": "DreamNetwork - Testing",
|
||||
}
|
||||
n, err = io.Copy(w, mstream)
|
||||
} else {
|
||||
n, err = io.Copy(w, sr)
|
||||
}
|
||||
log.Println("Transmitted", humanize.Bytes(uint64(n)))
|
||||
if err != nil {
|
||||
log.Println("Client transmission error:", err)
|
||||
}
|
||||
|
||||
/*notify := w.(http.CloseNotifier).CloseNotify()
|
||||
data := make([]byte, 4096)
|
||||
|
||||
log.Println("Start client tx loop")
|
||||
for {
|
||||
select {
|
||||
case <-notify:
|
||||
log.Println("Stop client tx loop")
|
||||
sr.Close()
|
||||
return
|
||||
default:
|
||||
n, err := sr.Read(data)
|
||||
if err != nil {
|
||||
log.Println("Read from stream failed:", err)
|
||||
return
|
||||
}
|
||||
n, err = w.Write(data[0:n])
|
||||
if err != nil {
|
||||
log.Println("Write to client failed:", err)
|
||||
log.Println("Stop client tx loop")
|
||||
sr.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}*/
|
||||
})
|
||||
server.Handler = mux
|
||||
server.Addr = ":8080"
|
||||
server.ListenAndServe()
|
||||
}
|
Loading…
Reference in New Issue