/*
* DudelDu
*
* Copyright 2016 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
Package dudeldu is a simple audio streaming server using the SHOUTcast protocol.
Server
Server is the main server object which runs a shoutcast server instance.
Using a WaitGroup a client can wait for the start and shutdown of the server.
Incoming new connections are served with a ConnectionHandler method. The
default implementation for this is the HandleRequest method of the
DefaultRequestHandler object.
DefaultRequestHandler
DefaultRequestHandler is the default request handler implementation for the
DudelDu server. DefaultRequestHandler has a customizable ServeRequest function.
ServeRequest is called once a request was successfully decoded.
The default implementation supports sending meta data while streaming audio. The
metadata implementation is according to:
http://www.smackfu.com/stuff/programming/shoutcast.html
Playlists
Playlists provide the data which is send to the client. A simple implementation
will just read .mp3 files and send them in chunks (via the Frame() method) to
the client.
A request handler uses a PlaylistFactory to produce a Playlist for each new
connection.
*/
package dudeldu
import (
"encoding/base64"
"regexp"
)
/*
requestAuthPattern is the pattern which is used to extract the request authentication
(i case-insensitive / m multi-line mode: ^ and $ match begin/end line)
*/
var requestAuthPattern = regexp.MustCompile("(?im)^Authorization: Basic (\\S+).*$")
/*
checkAuth checks the authentication header of a client request.
*/
func (drh *DefaultRequestHandler) checkAuth(bufStr string, clientString string) (string, string, bool) {
auth := ""
res := requestAuthPattern.FindStringSubmatch(bufStr)
origBufStr, hasAuth := drh.authPeers.Get(clientString)
if len(res) > 1 {
// Decode authentication
b, err := base64.StdEncoding.DecodeString(res[1])
if err != nil {
drh.logger.PrintDebug("Invalid request (cannot decode authentication): ", bufStr)
return auth, bufStr, false
}
auth = string(b)
// Authorize request
if auth != drh.auth && drh.auth != "" {
drh.logger.PrintDebug("Wrong authentication:", auth)
return auth, bufStr, false
}
// Peer is now authorized store this so it can connect again
drh.authPeers.Put(clientString, bufStr)
} else if drh.auth != "" && !hasAuth {
// No authorization
drh.logger.PrintDebug("No authentication found")
return auth, bufStr, false
} else if bufStr == "" && hasAuth {
// Workaround for strange clients like VLC which send first the
// authentication then connect again on a different port and just
// expect the stream
bufStr = origBufStr.(string)
// Get again the authentication
res = requestAuthPattern.FindStringSubmatch(bufStr)
if len(res) > 1 {
if b, err := base64.StdEncoding.DecodeString(res[1]); err == nil {
auth = string(b)
}
}
}
return auth, bufStr, true
}
/*
* DudelDu
*
* Copyright 2016 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
Package playlist contains the default playlist implementation.
FilePlaylistFactory
FilePlaylistFactory is a PlaylistFactory which reads its definition from
a file. The definition file is expected to be a JSON encoded datastructure of the form:
{
<web path> : [
{
"artist" : <artist>
"title" : <title>
"path" : <file path / url>
}
]
}
The web path is the absolute path which may be requested by the streaming
client (e.g. /foo/bar would be http://myserver:1234/foo/bar).
The path is either a physical file or a web url reachable by the server process.
The file ending determines the content type which is send to the client.
*/
package playlist
import (
"bytes"
"crypto/tls"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"devt.de/krotik/common/stringutil"
"devt.de/krotik/dudeldu"
)
/*
FileExtContentTypes maps file extensions to content types
*/
var FileExtContentTypes = map[string]string{
".mp3": "audio/mpeg",
".flac": "audio/flac",
".aac": "audio/x-aac",
".mp4a": "audio/mp4",
".mp4": "video/mp4",
".nsv": "video/nsv",
".ogg": "audio/ogg",
".spx": "audio/ogg",
".opus": "audio/ogg",
".oga": "audio/ogg",
".ogv": "video/ogg",
".weba": "audio/webm",
".webm": "video/webm",
".axa": "audio/annodex",
".axv": "video/annodex",
}
/*
FrameSize is the frame size which is used by the playlists
*/
var FrameSize = dudeldu.FrameSize
/*
FilePlaylistFactory data structure
*/
type FilePlaylistFactory struct {
data map[string][]map[string]string
itemPathPrefix string
}
/*
NewFilePlaylistFactory creates a new FilePlaylistFactory from a given definition
file.
*/
func NewFilePlaylistFactory(path string, itemPathPrefix string) (*FilePlaylistFactory, error) {
// Try to read the playlist file
pl, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// Unmarshal json
ret := &FilePlaylistFactory{
data: nil,
itemPathPrefix: itemPathPrefix,
}
err = json.Unmarshal(pl, &ret.data)
if err != nil {
// Try again and strip out comments
pl = stringutil.StripCStyleComments(pl)
err = json.Unmarshal(pl, &ret.data)
}
if err != nil {
return nil, err
}
return ret, nil
}
/*
Playlist returns a playlist for a given path.
*/
func (fp *FilePlaylistFactory) Playlist(path string, shuffle bool) dudeldu.Playlist {
if data, ok := fp.data[path]; ok {
// Check if the playlist should be shuffled
if shuffle {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
shuffledData := make([]map[string]string, len(data), len(data))
for i, j := range r.Perm(len(data)) {
shuffledData[i] = data[j]
}
data = shuffledData
}
return &FilePlaylist{path, fp.itemPathPrefix, 0, data, nil, false,
&sync.Pool{New: func() interface{} { return make([]byte, FrameSize, FrameSize) }}}
}
return nil
}
/*
FilePlaylist data structure
*/
type FilePlaylist struct {
path string // Path of this playlist
pathPrefix string // Prefix for all paths
current int // Pointer to the current playing item
data []map[string]string // Playlist items
stream io.ReadCloser // Current open stream
finished bool // Flag if this playlist has finished
framePool *sync.Pool // Pool for byte arrays
}
/*
currentItem returns the current playlist item
*/
func (fp *FilePlaylist) currentItem() map[string]string {
if fp.current < len(fp.data) {
return fp.data[fp.current]
}
return fp.data[len(fp.data)-1]
}
/*
Name is the name of the playlist.
*/
func (fp *FilePlaylist) Name() string {
return fp.path
}
/*
ContentType returns the content type of this playlist e.g. audio/mpeg.
*/
func (fp *FilePlaylist) ContentType() string {
ext := filepath.Ext(fp.currentItem()["path"])
if ctype, ok := FileExtContentTypes[ext]; ok {
return ctype
}
return "audio"
}
/*
Artist returns the artist which is currently playing.
*/
func (fp *FilePlaylist) Artist() string {
return fp.currentItem()["artist"]
}
/*
Title returns the title which is currently playing.
*/
func (fp *FilePlaylist) Title() string {
return fp.currentItem()["title"]
}
/*
Frame returns the current audio frame which is playing.
*/
func (fp *FilePlaylist) Frame() ([]byte, error) {
var err error
var frame []byte
if fp.finished {
return nil, dudeldu.ErrPlaylistEnd
}
if fp.stream == nil {
// Make sure first file is loaded
err = fp.nextFile()
}
if err == nil {
// Get new byte array from a pool
frame = fp.framePool.Get().([]byte)
n := 0
nn := 0
for n < len(frame) && err == nil {
nn, err = fp.stream.Read(frame[n:])
n += nn
// Check if we need to read the next file
if n < len(frame) || err == io.EOF {
err = fp.nextFile()
}
}
// Make sure the frame has no old data if it was only partially filled
if n == 0 {
// Special case we reached the end of the playlist
frame = nil
if err != nil {
err = dudeldu.ErrPlaylistEnd
}
} else if n < len(frame) {
// Resize frame if we have less data
frame = frame[:n]
}
}
if err == dudeldu.ErrPlaylistEnd {
fp.finished = true
}
return frame, err
}
/*
nextFile jumps to the next file for the playlist.
*/
func (fp *FilePlaylist) nextFile() error {
var err error
var stream io.ReadCloser
// Except for the first call advance the current pointer
if fp.stream != nil {
fp.current++
fp.stream.Close()
fp.stream = nil
// Return special error if the end of the playlist has been reached
if fp.current >= len(fp.data) {
return dudeldu.ErrPlaylistEnd
}
}
// Check if a file is already open
if fp.stream == nil {
item := fp.pathPrefix + fp.currentItem()["path"]
if _, err = url.ParseRequestURI(item); err == nil {
var resp *http.Response
// We got an url - access it without SSL verification
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}}
if resp, err = client.Get(item); err == nil {
buf := &StreamBuffer{}
buf.ReadFrom(resp.Body)
stream = buf
}
} else {
// Open a new file
stream, err = os.Open(item)
}
if err != nil {
// Jump to the next file if there is an error
fp.current++
return err
}
fp.stream = stream
}
return err
}
/*
ReleaseFrame releases a frame which has been written to the client.
*/
func (fp *FilePlaylist) ReleaseFrame(frame []byte) {
if len(frame) == FrameSize {
fp.framePool.Put(frame)
}
}
/*
Finished returns if the playlist has finished playing.
*/
func (fp *FilePlaylist) Finished() bool {
return fp.finished
}
/*
Close any open files by this playlist and reset the current pointer. After this
call the playlist can be played again.
*/
func (fp *FilePlaylist) Close() error {
if fp.stream != nil {
fp.stream.Close()
fp.stream = nil
}
fp.current = 0
fp.finished = false
return nil
}
/*
StreamBuffer is a buffer which implements io.ReadCloser and can be used to stream
one stream into another. The buffer detects a potential underflow and waits
until enough bytes were read from the source stream.
*/
type StreamBuffer struct {
bytes.Buffer // Buffer which is used to hold the data
readFromOngoing bool
}
func (b *StreamBuffer) Read(p []byte) (int, error) {
if b.readFromOngoing && b.Buffer.Len() < len(p) {
// Prevent buffer underflow and wait until we got enough data for
// the next read
time.Sleep(10 * time.Millisecond)
return b.Read(p)
}
n, err := b.Buffer.Read(p)
// Return EOF if the buffer is empty
if err == nil {
if _, err = b.ReadByte(); err == nil {
b.UnreadByte()
}
}
return n, err
}
/*
ReadFrom reads the source stream into the buffer.
*/
func (b *StreamBuffer) ReadFrom(r io.Reader) (int64, error) {
b.readFromOngoing = true
go func() {
b.Buffer.ReadFrom(r)
b.readFromOngoing = false
}()
return 0, nil
}
/*
Close does nothing but must be there to implement io.ReadCloser.
*/
func (b *StreamBuffer) Close() error {
// We are in memory so no need to close anything
return nil
}
/*
* DudelDu
*
* Copyright 2016 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package dudeldu
import (
"bytes"
"fmt"
"io"
"math"
"net"
"regexp"
"strconv"
"strings"
"devt.de/krotik/common/datautil"
)
/*
MaxRequestSize is the maximum size for a request
*/
const MaxRequestSize = 1024
/*
MetaDataInterval is the data interval in which meta data is send
*/
var MetaDataInterval uint64 = 65536
/*
peerNoAuthTimeout is the time in seconds a peer can open new connections without
sending new authentication information.
*/
const peerNoAuthTimeout = 10
/*
MaxMetaDataSize is the maximum size for meta data (everything over is truncated)
Must be a multiple of 16 which fits into one byte. Maximum: 16 * 255 = 4080
*/
var MaxMetaDataSize = 4080
/*
requestPathPattern is the pattern which is used to extract the requested path
(i case-insensitive / m multi-line mode: ^ and $ match begin/end line)
*/
var requestPathPattern = regexp.MustCompile("(?im)get\\s+([^\\s]+).*")
/*
requestOffsetPattern is the pattern which is used to extract the requested offset
(i case-insensitive / m multi-line mode: ^ and $ match begin/end line)
*/
var requestOffsetPattern = regexp.MustCompile("(?im)^Range: bytes=([0-9]+)-.*$")
/*
DefaultRequestHandler data structure
*/
type DefaultRequestHandler struct {
PlaylistFactory PlaylistFactory // Factory for playlists
ServeRequest func(c net.Conn, path string,
metaDataSupport bool, offset int, auth string) // Function to serve requests
loop bool // Flag if the playlist should be looped
LoopTimes int // Number of loops -1 loops forever
shuffle bool // Flag if the playlist should be shuffled
auth string // Required (basic) authentication string - may be empty
authPeers *datautil.MapCache // Peers which have been authenticated
logger DebugLogger // Logger for debug output
}
/*
NewDefaultRequestHandler creates a new default request handler object.
*/
func NewDefaultRequestHandler(pf PlaylistFactory, loop bool,
shuffle bool, auth string) *DefaultRequestHandler {
drh := &DefaultRequestHandler{
PlaylistFactory: pf,
loop: loop,
LoopTimes: -1,
shuffle: shuffle,
auth: auth,
authPeers: datautil.NewMapCache(0, peerNoAuthTimeout),
logger: nil,
}
drh.ServeRequest = drh.defaultServeRequest
return drh
}
/*
SetDebugLogger sets the debug logger for this request handler.
*/
func (drh *DefaultRequestHandler) SetDebugLogger(logger DebugLogger) {
drh.logger = logger
}
/*
HandleRequest handles requests from streaming clients. It tries to extract
the path and if meta data is supported. Once a request has been successfully
decoded ServeRequest is called. The connection is closed once HandleRequest
finishes.
*/
func (drh *DefaultRequestHandler) HandleRequest(c net.Conn, nerr net.Error) {
drh.logger.PrintDebug("Handling request from: ", c.RemoteAddr())
defer func() {
c.Close()
}()
// Check if there was an error
if nerr != nil {
drh.logger.PrintDebug(nerr)
return
}
buf, err := drh.decodeRequestHeader(c)
if err != nil {
drh.logger.PrintDebug(err)
return
}
// Add ending sequence in case the client "forgets"
bufStr := buf.String() + "\r\n\r\n"
// Determine the remote string
clientString := "-"
if c.RemoteAddr() != nil {
clientString, _, _ = net.SplitHostPort(c.RemoteAddr().String())
}
drh.logger.PrintDebug("Client:", c.RemoteAddr(), " Request:", bufStr)
if i := strings.Index(bufStr, "\r\n\r\n"); i >= 0 {
var auth string
var ok bool
bufStr = strings.TrimSpace(bufStr[:i])
// Check authentication
if auth, bufStr, ok = drh.checkAuth(bufStr, clientString); !ok {
drh.writeUnauthorized(c)
return
}
// Check if the client supports meta data
metaDataSupport := false
if strings.Contains(strings.ToLower(bufStr), "icy-metadata: 1") {
metaDataSupport = true
}
// Extract offset
offset := 0
res := requestOffsetPattern.FindStringSubmatch(bufStr)
if len(res) > 1 {
if o, err := strconv.Atoi(res[1]); err == nil {
offset = o
}
}
// Extract the path
res = requestPathPattern.FindStringSubmatch(bufStr)
if len(res) > 1 {
// Now serve the request
drh.ServeRequest(c, res[1], metaDataSupport, offset, auth)
return
}
}
drh.logger.PrintDebug("Invalid request: ", bufStr)
}
/*
decodeRequestHeader decodes the header of an incoming request.
*/
func (drh *DefaultRequestHandler) decodeRequestHeader(c net.Conn) (*bytes.Buffer, error) {
var buf bytes.Buffer
rbuf := make([]byte, 512, 512)
// Decode request
n, err := c.Read(rbuf)
for n > 0 || err != nil && err != io.EOF {
// Do some error checking
if err != nil {
return nil, err
} else if buf.Len() > MaxRequestSize {
return nil, fmt.Errorf("Illegal request: Request is too long")
}
buf.Write(rbuf[:n])
if strings.Contains(string(rbuf), "\r\n\r\n") {
break
}
n, err = c.Read(rbuf)
}
return &buf, nil
}
/*
defaultServeRequest is called once a request was successfully decoded.
*/
func (drh *DefaultRequestHandler) defaultServeRequest(c net.Conn, path string, metaDataSupport bool, offset int, auth string) {
var writtenBytes uint64
var currentPlaying string
var err error
drh.logger.PrintDebug("Serve request path:", path, " Metadata support:", metaDataSupport, " Offset:", offset)
pl := drh.PlaylistFactory.Playlist(path, drh.shuffle)
if pl == nil {
// Stream was not found - no error checking here (don't care)
drh.writeStreamNotFoundResponse(c)
return
}
err = drh.writeStreamStartResponse(c, pl.Name(), pl.ContentType(), metaDataSupport)
frameOffset := offset
for {
for !pl.Finished() {
if drh.logger.IsDebugOutputEnabled() {
playingString := fmt.Sprintf("%v - %v", pl.Title(), pl.Artist())
if playingString != currentPlaying {
currentPlaying = playingString
drh.logger.PrintDebug("Written bytes: ", writtenBytes)
drh.logger.PrintDebug("Sending: ", currentPlaying)
}
}
// Check if there were any errors
if err != nil {
drh.logger.PrintDebug(err)
return
}
frameOffset, writtenBytes, err = drh.writeFrame(c, pl, frameOffset,
writtenBytes, metaDataSupport)
}
// Handle looping - do not loop if close returns an error
if pl.Close() != nil || !drh.loop {
break
} else if drh.LoopTimes != -1 {
drh.LoopTimes--
if drh.LoopTimes == 0 {
break
}
}
}
drh.logger.PrintDebug("Serve request path:", path, " complete")
}
/*
prepareFrame prepares a frame before it can be written to a client.
*/
func (drh *DefaultRequestHandler) prepareFrame(c net.Conn, pl Playlist, frameOffset int,
writtenBytes uint64, metaDataSupport bool) ([]byte, int, error) {
frame, err := pl.Frame()
// Handle offsets
if frameOffset > 0 && err == nil {
for frameOffset > len(frame) && err == nil {
frameOffset -= len(frame)
frame, err = pl.Frame()
}
if err == nil {
frame = frame[frameOffset:]
frameOffset = 0
if len(frame) == 0 {
frame, err = pl.Frame()
}
}
}
if frame == nil {
if !pl.Finished() {
drh.logger.PrintDebug(fmt.Sprintf("Empty frame for: %v - %v (Error: %v)", pl.Title(), pl.Artist(), err))
}
} else if err != nil {
if err != ErrPlaylistEnd {
drh.logger.PrintDebug(fmt.Sprintf("Error while retrieving playlist data: %v", err))
}
err = nil
}
return frame, frameOffset, err
}
/*
writeFrame writes a frame to a client.
*/
func (drh *DefaultRequestHandler) writeFrame(c net.Conn, pl Playlist, frameOffset int,
writtenBytes uint64, metaDataSupport bool) (int, uint64, error) {
frame, frameOffset, err := drh.prepareFrame(c, pl, frameOffset, writtenBytes, metaDataSupport)
if frame == nil {
return frameOffset, writtenBytes, err
}
// Check if meta data should be send
if metaDataSupport && writtenBytes+uint64(len(frame)) >= MetaDataInterval {
// Write rest data before sending meta data
if preMetaDataLength := MetaDataInterval - writtenBytes; preMetaDataLength > 0 {
if err == nil {
_, err = c.Write(frame[:preMetaDataLength])
frame = frame[preMetaDataLength:]
writtenBytes += preMetaDataLength
}
}
if err == nil {
// Write meta data - no error checking (next write should fail)
drh.writeStreamMetaData(c, pl)
// Write rest of the frame
c.Write(frame)
writtenBytes += uint64(len(frame))
}
writtenBytes -= MetaDataInterval
} else {
// Just write the frame to the client
if err == nil {
clientWritten, _ := c.Write(frame)
// Abort if the client does not accept more data
if clientWritten == 0 && len(frame) > 0 {
return frameOffset, writtenBytes,
fmt.Errorf("Could not write to client - closing connection")
}
}
pl.ReleaseFrame(frame)
writtenBytes += uint64(len(frame))
}
return frameOffset, writtenBytes, err
}
/*
writeStreamMetaData writes meta data information into the stream.
*/
func (drh *DefaultRequestHandler) writeStreamMetaData(c net.Conn, playlist Playlist) {
streamTitle := fmt.Sprintf("StreamTitle='%v - %v';", playlist.Title(), playlist.Artist())
// Truncate stream title if necessary
if len(streamTitle) > MaxMetaDataSize {
streamTitle = streamTitle[:MaxMetaDataSize-2] + "';"
}
// Calculate the meta data frame size as a multiple of 16
metaDataFrameSize := byte(math.Ceil(float64(len(streamTitle)) / 16.0))
// Write meta data to the client
metaData := make([]byte, 16.0*metaDataFrameSize+1, 16.0*metaDataFrameSize+1)
metaData[0] = metaDataFrameSize
copy(metaData[1:], streamTitle)
c.Write(metaData)
}
/*
writeStreamStartResponse writes the start response to the client.
*/
func (drh *DefaultRequestHandler) writeStreamStartResponse(c net.Conn,
name, contentType string, metaDataSupport bool) error {
c.Write([]byte("ICY 200 OK\r\n"))
c.Write([]byte(fmt.Sprintf("Content-Type: %v\r\n", contentType)))
c.Write([]byte(fmt.Sprintf("icy-name: %v\r\n", name)))
if metaDataSupport {
c.Write([]byte("icy-metadata: 1\r\n"))
c.Write([]byte(fmt.Sprintf("icy-metaint: %v\r\n", MetaDataInterval)))
}
_, err := c.Write([]byte("\r\n"))
return err
}
/*
writeStreamNotFoundResponse writes the not found response to the client.
*/
func (drh *DefaultRequestHandler) writeStreamNotFoundResponse(c net.Conn) error {
_, err := c.Write([]byte("HTTP/1.1 404 Not found\r\n\r\n"))
return err
}
/*
writeUnauthorized writes the Unauthorized response to the client.
*/
func (drh *DefaultRequestHandler) writeUnauthorized(c net.Conn) error {
_, err := c.Write([]byte("HTTP/1.1 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"DudelDu Streaming Server\"\r\n\r\n"))
return err
}
/*
* DudelDu
*
* Copyright 2016 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package dudeldu
import (
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
/*
ProductVersion is the current version of DudelDu
*/
const ProductVersion = "1.3.1"
/*
ConnectionHandler is a function to handle new connections
*/
type ConnectionHandler func(net.Conn, net.Error)
/*
DebugLogger is the debug logging interface of the Server
*/
type DebugLogger interface {
/*
IsDebugOutputEnabled returns true if debug output is enabled.
*/
IsDebugOutputEnabled() bool
/*
PrintDebug will print debug output if `DebugOutput` is enabled.
*/
PrintDebug(v ...interface{})
}
/*
Server data structure
*/
type Server struct {
Running bool // Flag indicating if the server is running
Handler ConnectionHandler // Handler function for new connections
DebugOutput bool // Enable additional debugging output
LogPrint func(v ...interface{}) // Print logger method.
signalling chan os.Signal // Channel for receiving signals
tcpListener *net.TCPListener // TCP listener which accepts connections
serving bool // Internal flag indicating if the socket should be served
wgStatus *sync.WaitGroup // Optional wait group which should be notified once the server has started
}
/*
NewServer creates a new DudelDu server.
*/
func NewServer(handler ConnectionHandler) *Server {
return &Server{
Running: false,
Handler: handler,
DebugOutput: false,
LogPrint: log.Print,
}
}
/*
IsDebugOutputEnabled returns true if debug output is enabled.
*/
func (ds *Server) IsDebugOutputEnabled() bool {
return ds.DebugOutput
}
/*
PrintDebug will print debug output if `DebugOutput` is enabled.
*/
func (ds *Server) PrintDebug(v ...interface{}) {
if ds.DebugOutput {
ds.LogPrint(v...)
}
}
/*
Run starts the DudelDu Server which can be stopped via ^C (Control-C).
laddr should be the local address which should be given to net.Listen.
wgStatus is an optional wait group which will be notified once the server is listening
and once the server has shutdown.
This function will not return unless the server is shutdown.
*/
func (ds *Server) Run(laddr string, wgStatus *sync.WaitGroup) error {
// Create listener
listener, err := net.Listen("tcp", laddr)
if err != nil {
if wgStatus != nil {
wgStatus.Done()
}
return err
}
ds.tcpListener = listener.(*net.TCPListener)
ds.wgStatus = wgStatus
// Attach SIGINT handler - on unix and windows this is send
// when the user presses ^C (Control-C).
ds.signalling = make(chan os.Signal)
signal.Notify(ds.signalling, syscall.SIGINT)
// Put the serve call into a wait group so we can wait until shutdown
// completed
var wg sync.WaitGroup
wg.Add(1)
// Kick off the serve thread
go func() {
defer wg.Done()
ds.Running = true
ds.serv()
}()
for {
// Listen for shutdown signal
if ds.IsDebugOutputEnabled() {
ds.PrintDebug("Listen for shutdown signal")
}
signal := <-ds.signalling
if signal == syscall.SIGINT {
// Shutdown the server
ds.serving = false
// Wait until the server has shut down
wg.Wait()
ds.Running = false
break
}
}
if wgStatus != nil {
wgStatus.Done()
}
return nil
}
/*
Shutdown sends a shutdown signal.
*/
func (ds *Server) Shutdown() {
if ds.serving {
ds.signalling <- syscall.SIGINT
}
}
/*
serv waits for new connections and assigns a handler to them.
*/
func (ds *Server) serv() {
ds.serving = true
for ds.serving {
// Wait up to a second for a new connection
ds.tcpListener.SetDeadline(time.Now().Add(time.Second))
newConn, err := ds.tcpListener.Accept()
// Notify wgStatus if it was specified
if ds.wgStatus != nil {
ds.wgStatus.Done()
ds.wgStatus = nil
}
netErr, ok := err.(net.Error)
// Check if got an error and notify an error handler
if newConn != nil || (ok && !(netErr.Timeout() || netErr.Temporary())) {
go ds.Handler(newConn, netErr)
}
}
ds.tcpListener.Close()
}
/*
* DudelDu
*
* Copyright 2016 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
DudelDu main entry point for the standalone server.
Features:
- Supports various streaming clients: VLC, ServeStream, ... and most Icecast clients.
- Supports sending of meta data (sending artist and title to the streaming client).
- Playlists are simple json files and data files are normal media (e.g. .mp3) files on disk.
- Supports basic authentication.
*/
package main
import (
"flag"
"fmt"
"log"
"os"
"devt.de/krotik/dudeldu"
"devt.de/krotik/dudeldu/playlist"
)
// Global variables
// ================
/*
ConfigFile is the config file which will be used to configure DudelDu
*/
var ConfigFile = "dudeldu.config.json"
/*
Known configuration options for DudelDu
*/
const (
ThreadPoolSize = "ThreadPoolSize"
FrameQueueSize = "FrameQueueSize"
ServerPort = "ServerPort"
ServerHost = "ServerHost"
PathPrefix = "PathPrefix"
)
/*
DefaultConfig is the defaut configuration
*/
var DefaultConfig = map[string]interface{}{
ThreadPoolSize: 10,
FrameQueueSize: 10000,
ServerPort: "9091",
ServerHost: "127.0.0.1",
PathPrefix: "",
}
type consolelogger func(v ...interface{})
/*
Fatal/print logger methods. Using a custom type so we can test calls with unit
tests.
*/
var fatal = consolelogger(log.Fatal)
var print = consolelogger(func(a ...interface{}) {
fmt.Fprint(os.Stderr, a...)
fmt.Fprint(os.Stderr, "\n")
})
var lookupEnv func(string) (string, bool) = os.LookupEnv
/*
DudelDu server instance (used by unit tests)
*/
var dds *dudeldu.Server
/*
Main entry point for DudelDu.
*/
func main() {
var err error
var plf dudeldu.PlaylistFactory
print(fmt.Sprintf("DudelDu %v", dudeldu.ProductVersion))
auth := flag.String("auth", "", "Authentication as <user>:<pass>")
serverHost := flag.String("host", DefaultConfig[ServerHost].(string), "Server hostname to listen on")
serverPort := flag.String("port", DefaultConfig[ServerPort].(string), "Server port to listen on")
threadPoolSize := flag.Int("tps", DefaultConfig[ThreadPoolSize].(int), "Thread pool size")
frameQueueSize := flag.Int("fqs", DefaultConfig[FrameQueueSize].(int), "Frame queue size")
pathPrefix := flag.String("pp", DefaultConfig[PathPrefix].(string), "Prefix all paths with a string")
enableDebug := flag.Bool("debug", false, "Enable extra debugging output")
loopPlaylist := flag.Bool("loop", false, "Loop playlists")
shufflePlaylist := flag.Bool("shuffle", false, "Shuffle playlists")
showHelp := flag.Bool("?", false, "Show this help message")
flag.Usage = func() {
print(fmt.Sprintf("Usage of %s [options] <playlist>", os.Args[0]))
flag.PrintDefaults()
print()
print(fmt.Sprint("Authentication can also be defined via the environment variable: DUDELDU_AUTH=\"<user>:<pass>\""))
}
flag.Parse()
if len(flag.Args()) != 1 || *showHelp {
flag.Usage()
return
}
// Check for auth environment variable
if envAuth, ok := lookupEnv("DUDELDU_AUTH"); ok && *auth == "" {
*auth = envAuth
}
laddr := fmt.Sprintf("%v:%v", *serverHost, *serverPort)
print(fmt.Sprintf("Serving playlist %v on %v", flag.Arg(0), laddr))
print(fmt.Sprintf("Thread pool size: %v", *threadPoolSize))
print(fmt.Sprintf("Frame queue size: %v", *frameQueueSize))
print(fmt.Sprintf("Loop playlist: %v", *loopPlaylist))
print(fmt.Sprintf("Shuffle playlist: %v", *shufflePlaylist))
print(fmt.Sprintf("Path prefix: %v", *pathPrefix))
if *auth != "" {
print(fmt.Sprintf("Required authentication: %v", *auth))
}
// Create server and listen
plf, err = playlist.NewFilePlaylistFactory(flag.Arg(0), *pathPrefix)
if err == nil {
rh := dudeldu.NewDefaultRequestHandler(plf, *loopPlaylist, *shufflePlaylist, *auth)
dds = dudeldu.NewServer(rh.HandleRequest)
dds.DebugOutput = *enableDebug
rh.SetDebugLogger(dds)
defer print("Shutting down")
err = dds.Run(laddr, nil)
}
if err != nil {
fatal(err)
}
}