Parent: [7a7ab2] (diff)

Download this file

scmakempdsender    221 lines (196 with data), 6.0 kB

#!/usr/bin/env python
# Copyright (C) 2015 J.F.Dockes
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the
#   Free Software Foundation, Inc.,
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
from __future__ import print_function

# Auxiliary script for the Unix Songcast mpd-based sender: we create
# an mpd process which writes to a fifo only, and an uxsender process
# (Songcast sender) which reads from the fifo
#
# The MPD process is in turn controlled from the upmpdcli process
# which initiated the whole thing, which will normally stop the
# current mpd playing (can't use it: the audio would not be in sync
# with the Songcast one), copy the current playlist to the new mpd,
# and start a local Receiver for playing. Other Receivers can then
# connect to the Sender to achieve multiroom synchronized play

import tempfile
import time
import subprocess
import os
import sys
import uuid
import getopt
import signal
import socket
PY3 = sys.version > '3'

uxsender = "mpd2sc"
def usage(f):
    print("Usage: scmakempdsender [-h] [-f friendlyname] [-p mpdport] [-u] [-e]",
          file=f)
    sys.exit(1)
    
upmpdclitoo = False
mpdport = 6700
externalvolume = False
# Upmpdcli friendly-name. Used to compute a Uuid in
# conjunction with the node name
upmpdcli_fname = "UpMpd"
codec_name = ""

def logerr(s):
    print("%s" % s, file=sys.stderr)
    
args = sys.argv[1:]
opts, args = getopt.getopt(args, "hup:f:e:C:")
for opt, arg in opts:
    if opt in ['-h']:
        usage(sys.stdout)
    elif opt in ['-C']:
        codec_name = arg
    elif opt in ['-e']:
        externalvolume = True
    elif opt in ['-f']:
        upmpdcli_fname = arg
    elif opt in ['-p']:
        mpdport = int(arg)
    elif opt in ['-u']:
        upmpdclitoo = True
    else:
        logerr("unknown option %s\n" % opt)
        usage(sys.stderr)

# Temp fifo name and temporary file for mpd configuration
mpdfifo = tempfile.mktemp(suffix=".fifo")
mpdconf = tempfile.NamedTemporaryFile(delete=False)

# UDN and name for the Sender UPnP device.
# We used to call:
#  uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname() + upmpdcli_fname).urn
# which yielded something like urn:uuid:bd5d9ae1-5775-5cda-8d14-2f87d64c4798,
# but this seems to cause issues for recent versions of ohNet (??). So now use
# the shorter output of uuid1().hex (32 chars)
sender_udn = uuid.uuid1().hex
sender_name = "%s UxSender" % upmpdcli_fname

mpdproc = None
senderproc = None
upmpdproc = None

parentpid = os.getppid()

def cleanup(xval):
    # Clean up
    try:
        senderproc.terminate()
    except:
        pass
    try:
        mpdproc.terminate()
    except:
        pass
    try:
        upmpdproc.terminate()
    except:
        pass
    # MPD normally deletes the fifo. Just in case
    try:
        os.remove(mpdfifo)
    except:
        pass
    try:
        os.remove(mpdconf.name)
    except:
        pass
    sys.exit(xval)

def sighandler(signum, frame):
    cleanup(1)

signal.signal(signal.SIGINT, sighandler)
signal.signal(signal.SIGTERM, sighandler)

# MPD configuration in temporary file
if externalvolume:
    mixertype = 'null'
else:
    mixertype = 'software'
    
mpdconf_template='''
bind_to_address	 "localhost"
port		 "%d"
zeroconf_enabled "no"
input {
    plugin "curl"
}
audio_output {
    type    "fifo"
    name    "FIFO"
    path    "%s"
    format  "44100:16:2"
}
mixer_type  "%s"
'''
data = mpdconf_template % (mpdport, mpdfifo, mixertype)
if type(data) == type(u''):
    data = data.encode('utf-8')
mpdconf.write(data)
mpdconf.flush()


# Start mpd
try:
    mpdproc = subprocess.Popen(["mpd", "--no-daemon", "--stderr", mpdconf.name],
                               bufsize = -1)
except Exception as err:
    logerr("Can't start mpd: %s" % err)
    cleanup(1)

# Wait for the fifo to appear
while True:
    if os.path.exists(mpdfifo):
        break
    if mpdproc.poll() is not None:
        sys.exit(1)
    time.sleep(0.1)

# Start the Sender
try:
    cmdvec = [uxsender, "-f", mpdfifo,
              "-A", "44100:16:2:1",
              "-u", sender_udn, "-n", sender_name]
    if codec_name:
        cmdvec += ["-C", codec_name]
    logerr("STARTING %s" % cmdvec)
    senderproc = subprocess.Popen(cmdvec, stdout=subprocess.PIPE, bufsize = -1)
except Exception as err:
    logerr("Can't start %s: %s" % (uxsender, err))
    cleanup(1)

# Get the Uri and Metadata values from the sender. These get written to stdout
urimeta = senderproc.stdout.readline()

# Optionally also start an upmpdcli to control the mpd. Not normally
# used (becaue the upmpdcli which is switching to sender mode would do
# it) but handy when developping
if upmpdclitoo:
    upname = "upmpdcli-sender-%d"% os.getpid()
    try:
        upmpdproc = subprocess.Popen(["upmpdcli", "-c", "", "-h", "localhost",
                                      "-p", "%d"%mpdport,
                                      "-f", upname], bufsize = -1)
    except Exception as err:
        logerr("Can't start upmpdcli: %s" % err)
        cleanup(1)

# Tell the world we're set
data = b"Ok %d %s" % (mpdport, urimeta)
if PY3:
    sys.stdout.buffer.write(data)
else:
    sys.stdout.write(data)
sys.stdout.flush()

# Wait for either process, also checking that our parent is not dead
while True:
    if mpdproc.poll() is not None or senderproc.poll() is not None:
        break
    try:
        os.kill(parentpid, 0)
    except:
        break
    time.sleep(0.5)

time.sleep(1)
cleanup(0)