Chapter 0: Extended example - home audio system

This chapter considers an extended example, using Jini in a home audio situation. It uses many of the concepts of earlier chapters and shows how Jini can be used to build non-trivial systems

1. Introduction

Traditional information systems concentrated on modelling information flows, and quite explicitly avoided physical systems. The advent of object-oriented systems changed this emphasis, with increased emphasis on the behaviour of real objects and how this could form the basis of an information system. In the meantime, there has been a huge amount of work in control systems, and increasing the computational power of everyday things such as dishwashers, washing machines and so on. One area in which the convergence of computer systems and devices has become a major commercial area is that of audio/visual systems. The change from analogue to digital sytems has opened up a large area, which goes far beyond copying MP3 files from one computer to another

The home A/V area is a battleground for ideologies and commercial interests. On the one hand are the set-top vendors, owning the cable systems that pump entertainment into many homes. Their vision is to widen their pipe, while still maintaining control. The professional audio and hifi community on the other hand, see the hifi system as control centre. And of course, the computer community sees the PC as centre of any home A/V system, due to its processing power, well-developed software systems, and the ability to handle digital signals without difficulty.

I belong to the PC-centric community to some extent - for even there, there are divergences of opinion. Most current A/V systems such as the Java Media Framework and the Microsoft Media Platform treat the A/V sources and sinks as though they are on the same machine, so that all processing is done locally. Agreed, JMF allows network access using HTTP or RTP, but it tries to hide the network layer and make all components appear to be local.

The mantra from Sun for many years is "the network is the computer." This could be applied to the A/V world: "the network is the A/V system." What makes it interesting for the A/V system is what a network can do: a wireless network can support friends visiting with their own A/V systems, joining in with yours to share music; it can support music following you around the house, switching from one set of speakers to another. This chapter is my attempt at building a network wireless audio system using Jini.

2. Distributed audio

There have been many efforts to distribute A/V. Much of this is concerned with large servers, and these efforts have paid off with streaming media systems such as RealAudio. I want to look at a more local situation: my house is a medium size house, and now that I have a wireless network I can work in the lounge room, the family room, my study or even in one of the bedrooms. I like "music while you work" - either CDs, or the various community radio stations that I subscribe to, and possibly streaming audio from other stations in the world later. I don't have any children or partner there at the moment, but if I did, then they would have their own music sources and sinks and would share the house network. Friends might come and visit, with their own A/V sources and sinks and just join the house network. In a little while, guitars and microphones will have Bluetooth cards,so we will be able to have a local network band.

The wireless network density in my neighbourhood is low, but eventually I should be able to join a local community network, which should give me metropolitan access. I live in a city rich in music (Melbourne, Australia) and sometimes feel that I hardly need to go out because the local radio stations (RRR, PBS-FM) are so good, but soon I would also hope to tune into the folk concert on the other side of town through the community wireless network.

Okay: so how do we build middleware for an A/V network that is network-centric, rather than proprieter-centric? There has been one attempt that I know of to build a network-based A/V system, by Marco Lohse ("An open.middleware architecture for network-integrated multimedia"). This is CORBA-based, which gives it network objects. But a lot of their system has to be built on top of CORBA because it doesn't quite support what they want. Much of this extra structure seems to fall out quite easily under Jini.

I am approaching the rest of this chapter from a software-engineering viewpoint, trying to make a system as simple as possible for consumers (clients). If you have any comments on this, please let me know - after all, this is the system I using in my house right now, so if it can be made better, then I at least will be grateful!

3. Parameters for A/V

There are many variables that affect how A/V is sourced, moved around a network and delivered

Transport
The transport layer may be reliable (slow) TCP, unreliable (faster) UDP, HTTP (even slower), with some QOS such as RTP or some other network technology protocol such as Bluetooth or FireWire
Format
There are an enormous number of formats, from encumbered formats such as MP3 (for which you are supposed to pay license fees for encoders and decoders), unencumbered equivalents such as Ogg-Vorbis, compressed (MP3 and Ogg-Vorbis) or uncompressed (Sun AU or M/S WAV), lossy or lossless. In addition, there are many wrinkles in each format: little- or big-endian; 8, 16 or 32 bit; mono, stereo, 5-1,...; sample rate such as 44.1khz, 8khz, etc
Content description
Audio comes from many different sources: tracks off a CD, streaming audio from an FM station, speech off a telephone line. The MPEG-7 concentrates on technical aspects of an audio signal in attempts to classify it, while the CD databases (CDDB) such as freedb classify CDs by Artist/Title - which breaks down with compilation CDs and most classical CDs (who is the artist - the composer, the conductor or the orchestra?)
Push/pull
An audio stream may be "pushed", such as an FM radio stream that is always playing. Or it may be "pulled" by a client from a server, such as in fetching an MP3 file from an HTTP server

4. Source/sink interfaces

Interfaces should contain all the information about how to access services. With audio, all the information about a service can be quite complex: for example, a service might offer a CD track encoded in 16-bit stereo, big-endian, 44.1khz sampling in WAV format from an HTTP server. This information may be needed by a consumer that wants to play the file.

But in the type of A/V system I want to build there are three players:

  1. Sources of A/V data
  2. Sinks for A/V data
  3. Controller clients to link sources and sinks
From the controller viewpoint, most of this information is irrelevant: it will just want to link sources to sinks, and leave it to them to decide how and if they can communicate.

For simplicity we define two interfaces: Source and Sink. To avoid making implementation decisions about pull versus push, we have methods to tell a source about a sink, a sink about a source, to tell the source to play and the sink to record. Again, how they decide how to do this is upto the source and sink. Sometimes this won't work: an HTTP source may not be able to deliver to an RTP sink, or a WAV file may not be managed by an MP3 player. If they don't succeed in negotiating tranport and content, then an exception should be thrown. This violates the principle that a service should be usable based on its interface alone, but considerably simplifies matters for controller clients.

A controller that wants to play a sequence of audio tracks to a sink will need to know when one track is finished in order to start the next. The play() and record() methods could block till finished, or return immediately and post an event on completion. The second method allows more flexibility, and so needs add/remove listener methods for the events.

Finally, there are the exceptions that can be thrown by the methods. Attempting to add a source that a sink cannot handle should throw an exception such as IncompatableSourceException. A sink that can handle only a small number of sources (for example, only one) could throw an exception if too many sources are added. A source that is already playing may not be able to satisfy a new request to play.

These considerations lead to a pair of high-level interfaces which seem to be suitable for controllers to manage sources and sinks:



/**
 * Source.java
 */

package common;

import java.rmi.RemoteException;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;

/**
 * A source for A/V data
 */

public interface Source extends java.rmi.Remote {

    int STOP = 1;

    void play() throws 
                RemoteException,
	        AlreadyPlayingException;
    void stop() throws 
                RemoteException,
                NotPlayingException;
    void addSink(Sink sink) throws
                RemoteException,
                TooManySinksException,
                IncompatableSinkException;
    void removeSink(Sink sink) throws
                RemoteException,
                NoSuchSinkException;
    EventRegistration addSourceListener(RemoteEventListener listener,
					MarshalledObject handback) throws
					    RemoteException;
}// Source

and


/**
 * Sink.java
 */

package common;

import java.rmi.RemoteException;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;

/**
 * A sink for audio
 */

public interface Sink extends java.rmi.Remote {

    int STOP = 1;

    void record() throws
	        RemoteException,
	        AlreadyRecordingException;
    void stop() throws 
                RemoteException,
	        NotRecordingException;
    void addSource(Source src) throws
                RemoteException,
                TooManySourcesException,
                IncompatableSourceException;
    void removeSource(Source src) throws
                RemoteException,
                NoSuchSourceException;
    EventRegistration addSinkListener(RemoteEventListener listener, 
				      MarshalledObject handback) throws
	        RemoteException;
    void removeSinkListener(RemoteEventListener listener) throws
	        RemoteException,
		NoSuchListenerException;

}// Sink

5. Content interfaces

The Java Media Framework (JMF) has methods such as getSupportedContentTypes() which returns an array of strings. Other media toolkits have similar mechanisms. This isn't type-safe: it relies on all parties having the same strings and attaching the same meaning to each. In addition to this, if a new type comes along, there isn't a reliable means of specifying this information to others. A type-safe system can at least specify this by class files.

I have chosen to use interfaces instead of strings: a WAV interface, an Ogg interface, etc. This doesn't easily allow extension to the multiplicity of content type variations (bit size, sampling rate, etc), but the current content handlers seem to be able to handle most of these variations anyway, so it seems feasible to ignore them at an application level.

The content interfaces are just place-holders:


package presentation;

public interface Ogg extends java.rmi.Remote {
}
A source that could make an audio stream available in OggVorbis format would signal this by implementing the Ogg interface. A sink that can manage OggVorbis streams would also implement this interface.

6. Transport interfaces

In a similar way, I have chosen to represent the transport mechanisms by interfaces. A transport sink will get the information from a source using some unspecified network transport mechanism. The audio stream can be made available to any other object by exposing an InputStream. This is a standard Java stream, not the special one used by JMF. Similarly, a transport source would make an output stream available for source-side objects to write data into.



/**
 * TransportSink.java
 */

package transport;

import java.io.*;

public interface TransportSink {

    public InputStream getInputStream();
}// TransportSink

and


/**
 * TransportSource.java
 */

package transport;

import java.io.*;

public interface TransportSource {

    public OutputStream getOutputStream();
}// TransportSource

7. Linkages

By separating the transport and content layers, we have a model that follows a part of the ISO 7-layer model: transport and presentation layers. The communication paths for a "pull" sink are

The classes involved in a "pull" sink could look like

where the choice of transport and content implementation is based on the interfaces supported by the source.

8. An HTTP source

An HTTP source makes an audio stream available as a document from an HTTP server. All that it needs to tell a sink about is the URL for the document. There is a little hiccup in this: a Java URL can be an http URL, or a file URL, an ftp URL, etc. So I have defined a subclass HttpURL to enforce that it is a URL accessible by the HTTP protocol. The Java URL class is final, so we can't extend it and have to wrap around it.



/**
 * HttpURL.java
 */

package transport;

import java.net.MalformedURLException;
import java.net.*;
import java.io.*;

public class HttpURL implements java.io.Serializable {
    private URL url;

    public HttpURL(URL url) throws MalformedURLException {
	this.url = url;
	if (! url.getProtocol().equals("http")) {
	    throw new MalformedURLException("Not http URL");
	}
    }

    public HttpURL(String spec) throws MalformedURLException {
	url = new URL(spec);
	if (! url.getProtocol().equals("http")) {
	    throw new MalformedURLException("Not http URL");
	}
    }

    public URLConnection openConnection()
	throws IOException {
	return url.openConnection();
    }

    public InputStream openStream()
	throws IOException {
	return url.openStream();
    }
}// HttpURL

The HttpSource interface just exposes an HttpURL:



/**
 * HttpSource.java
 */

package transport;

import common.*;
import java.net.URL;
import java.rmi.RemoteException;

public interface HttpSource extends Source {

    HttpURL getHttpURL() throws RemoteException;
}// HttpSource

The interface allows a sink to determine that the transport protocol it should use is the HTTP protocol, and what URL it should use to fetch the document.

An HTTP source is a "pull" source: that is, a sink will fetch the data from it. A source of this type doesn't need to worry about listeners or playing the source data. All it needs to do is store the URL. An implementation of this could be



/**
 * HttpSourceImpl.java
 */

package http;

import common.*;
import transport.*;

import java.net.*;
import java.rmi.*;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;

/**
 * Stores an HTTP reference
 */

public class HttpSourceImpl implements HttpSource, Remote {
    private HttpURL url;

    private HttpSourceImpl() {
    }

    public HttpSourceImpl(HttpURL url) {
	this.url = url;
    }

    public HttpSourceImpl(URL url) throws MalformedURLException {
	this.url = new HttpURL(url);
    }

    public HttpURL getHttpURL() {
	return url;
    }

    public void play() {}
    public void stop() {}
    public void addSink(Sink sink) throws IncompatableSinkException { }

    public void removeSink(Sink sink) {}

    public EventRegistration addSourceListener(RemoteEventListener listener,
					       MarshalledObject handback) {
	return null;
    }
}// HttpSourceImpl

9. An HTTP OggVorbis source

If the document is an OggVorbis document, then the service signals this by implementing the Ogg interface



/**
 * HttpOggSourceImpl.java
 */

package http;

import presentation.Ogg;
import transport.HttpURL;

import java.net.*;

/**
 * Adds Ogg interface to HttpSourceImpl for Ogg files
 */

public class HttpOggSourceImpl extends HttpSourceImpl
    implements Ogg{

    public HttpOggSourceImpl(HttpURL url) throws MalformedURLException {
	super(url);
    }

    public HttpOggSourceImpl(URL url) throws MalformedURLException {
	super(url);
    }
}// HttpOggSourceImpl

10. An HTTP sink

An HTTP sink needs to find the URL from an HttpSource, open a connection to it and get an InputStream. The Java URL class makes this quite straightforward:



/**
 * HttpSinkImpl.java
 */

package transport;

import java.io.*;
import java.net.*;

public class HttpSinkImpl implements TransportSink {

    protected HttpSource source;

    public HttpSinkImpl(HttpSource source) {
	this.source = source;
    }

    public InputStream getInputStream() {
	try {
	    HttpURL url = source.getHttpURL();
	    URLConnection connect = url.openConnection();

	    connect.setDoInput(true);
	    connect.setDoOutput(false);
	    InputStream in = connect.getInputStream();
	    return in;
	} catch (IOException e) {
	    System.err.println("Getting in stream " + e.toString());
	    return null;
	}
    }
}// HttpSinkImpl

11. Content sinks

A ContentSink will get an InputStream from a TransportSink. Then it can read bytes from this stream and interpret it based on the content type it understands. There are some content handlers in the Java JMF. But some are missing: MP3 files are encumbered by patent rights, and encoders and decoders should cost (someone!) money to use, so currently there is no MP3 player in JMF (well, there is an MPEG movie player which can be called with no video stream...); there is no current OggVorbis player. So it is easier to make O/S system calls into players such as mpg123 or sox. I have generically labelled these as playmp3, etc:



/**
 * ContentSink.java
 */

package pull;

import java.io.*;
import presentation.*;
import common.*;

public class ContentSink {

    private InputStream in;
    private OutputStream out;
    private String cmd;
    private boolean stopped = false;
    private SinkImpl sink;

    public static ContentSink createSink(SinkImpl sink, 
					 InputStream in, Source source) {
	String cmd = "";

	if (source instanceof WAV) {
	    cmd = "playwav";
	} else if (source instanceof Ogg) {
	    cmd = "playogg";
	} else if (source instanceof MP3) {
	    cmd = "playmp3";
	} else {
	    cmd = "true";
	}

	ContentSink csink = new ContentSink(sink, in, cmd);
	return csink;
    }

    /**
     * There should really be a
     * WAVContentSink, OggContentSink, etc
     * I cheated since they would be so simple
     */
    private ContentSink(SinkImpl sink, InputStream in, String cmd) {
	this.sink = sink;
	this.in = in;
	this.cmd = cmd;
    }

    public void record() {
	
	Process proc = null;
	InputStream err = null;
	try {
	    proc = Runtime.getRuntime().exec(cmd);
	    out = proc.getOutputStream();
	    err = proc.getErrorStream();
	} catch(IOException e) {
	    System.err.println("Playing " + e.toString());
	    // ignore
	    return;
	}
	
	int ch;
	try {
	    while (((ch = in.read()) != -1) &&
		   (! stopped)) {
		out.write(ch);
	    }
	} catch(IOException e) {
	    // ignore
	    System.err.println("Exception writing " + e.toString());
	    int navail = 0;
	    try {
		if ((navail = err.available()) > 0 ) {
		    byte avail[] = new byte[navail];
		    int nread = err.read(avail, 0, navail);
		    System.out.println("Error channel " + 
				       new String(avail));
		}
	    } catch(IOException ee) {
		ee.printStackTrace();
	    }
	    return;
	} finally {
	    if (stopped) {
		System.out.println("Record stop called");
	    } else {
		System.out.println("Record finished naturally");
		stopped = true;
	    }
	    try {
		if (proc != null) {
		    proc.destroy();
		    try {
			// wait for soundcard to be released
			proc.waitFor();
		    } catch(InterruptedException ei) {
			System.out.println("Int " + ei);
		    }
		}
		in.close();
		out.close();
	    } catch(IOException e) {
		// ignore
		System.out.println("Finally " + e);
	    }

	    sink.contentStopped();
	}
    }

    public void stop() {
	if (stopped) {
	    return;
	}
	stopped  = true;
    }
} // ContentSink

12. Sink implementation

A sink must create the appropriate transport and content handlers, and link the two together. It needs to look after listeners, and post events to them when they occur.



/**
 * SinkImpl.java
 */

package pull;

import transport.*;
import aumix.*;

import java.io.*;
import java.net.*;
import java.rmi.*;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEvent;
import java.util.Vector;
import java.util.Enumeration;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.UnknownEventException;
import java.util.Hashtable;

import common.*;

public class SinkImpl implements Sink, VolumeControl, Remote {
    private Source source;
    private boolean stopped;
    private CopyIO copyIO;
    private Hashtable listeners = new Hashtable();
    private int seqNum = 0;
    private Remote proxy;
    private MimeType contentType = null;
    private InputStream in = null;

    public SinkImpl() {

    }

    public void setProxy(Remote proxy) {
	this.proxy = proxy;
    }

    public void record() throws RemoteException, AlreadyRecordingException {

	if ((copyIO != null) && ( ! stopped)) {
	    throw new AlreadyRecordingException();
	}

	if (source == null) {
	    return;
	}

	stopped = false;

	if (in  == null) {
	    System.out.println("Couldn't get input stream");
	    stopped = true;
	    return;
	}

        // hand play over to a CopyIO object
        // This will run a ContentSink in its own thread
	copyIO = new CopyIO(this, in, source);
	copyIO.start();
	System.out.println("Play returning");
    }

    public void stop() throws RemoteException {
	stopped = true;
	if (copyIO != null) {
	    copyIO.stopRecord();
	}
    }

    public void contentStopped() {
	copyIO = null;
	fireNotify(Sink.STOP);
	System.out.println("Stopped");
    }

    public void addSource(Source source) throws
	IncompatableSourceException,
	TooManySourcesException {
	TransportSink transportSink = null;

	// if (this.source == null) {
	    this.source = source;
	    // } else {
	    // throw new TooManySourcesException();
	    // }

        // which transport sink to use?
	if (source instanceof HttpSource) {
	    transportSink = new HttpSinkImpl((HttpSource) source);
	    in = transportSink.getInputStream();
	} else if (source instanceof TcpSource) {
	    System.out.println("Setting up Tcp sink");
	    transportSink = new TcpSinkImpl((TcpSource) source);
	    in = transportSink.getInputStream();
	    System.out.println("Got tcp source input stream " + in);
	} else if (source instanceof PullSource) {
	    in = ((PullSource) source).getInputStream();
	} else {
	    throw new IncompatableSourceException();
	}
    }

    public void removeSource(Source source) throws
	RemoteException,
	NoSuchSourceException {
	if (this.source == source) {
	    this.source = null;
	} else {
	    throw new NoSuchSourceException();
	}
    }

    public EventRegistration addSinkListener(RemoteEventListener listener,
					      MarshalledObject handback) {
	System.out.println("Adding listener: " + listener);
	listeners.put(listener, handback);
	return new EventRegistration(0L, proxy, null, 0L);
    }

    public void removeSinkListener(RemoteEventListener listener) {
	listeners.remove(listener);
    }

    public void fireNotify(int evtType) {
	Enumeration elmts = listeners.keys();
	System.out.println("Fire notify");
	while (elmts.hasMoreElements()) {
	    RemoteEventListener listener = (RemoteEventListener) elmts.nextElement();
	    MarshalledObject handback = (MarshalledObject) listeners.get(listener);
	    RemoteEvent evt = new RemoteEvent(proxy, evtType, seqNum++, handback);
	    System.out.println("Updating listener " + listener);
	    try {
		listener.notify(evt);
	    } catch(UnknownEventException e) {
		// ??
	    } catch(RemoteException e) {
		// ?
	    }
	}
    }

    class CopyIO extends Thread {

	private SinkImpl sink;
	private ContentSink contentSink;

	CopyIO(SinkImpl sink, InputStream in, Source source) {
	    contentSink = ContentSink.createSink(sink, in, source);
	    this.sink = sink;
	}
	
	public void stopRecord() {
	    if (contentSink != null) {
		contentSink.stop();
	    }
	}

	public void run() {
	    contentSink.record();
	}
    }

    /* 
     * Volume control
     */
    public void setVolume(int vol) {
	Aumix.setVolume(vol);
    }

    public int getVolume() {
	return Aumix.getVolume();
    }

    public int getMaxVolume() {
	return Aumix.getMaxVolume();
    }
}// SinkImpl

(This sink also implements a VolumeControl interface - not dealt with here, but pretty obvious what it does.)

13. Servers

Each source will need a server to create, advertise it and keep it alive. So will each sink.

13.1 Sink server

A sink server is


package pull;

import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceRegistrar;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceIDListener;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;

import net.jini.config.*; 
import net.jini.export.*; 

import net.jini.id.UuidFactory;
import net.jini.id.Uuid;

import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;
import java.io.*;

/**
 * PullSinkServer.java
 */

public class SinkServer {

    // explicit proxy for Jini 2.0
    protected Remote proxy;
    protected SinkImpl impl;
    private String sinkName = "No name";
    private ServiceID serviceID;

    public static void main(String argv[]) {
	new SinkServer(argv);

        // stay around forever
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(InterruptedException e) {
		// do nothing
	    }
	}
    }

    public SinkServer(String[] argv) {
	File serviceIDFile = null;

	try {
	    impl = new SinkImpl();
	} catch(Exception e) {
            System.err.println("New impl: " + e.toString());
            System.exit(1);
	}

	String[] configArgs = new String[] {argv[0]};

	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 
	    
	    // and use this to construct an exporter
	    Exporter exporter = (Exporter) config.getEntry( "HttpSinkServer", 
							    "exporter", 
							    Exporter.class); 
	    // export an object of this class
	    proxy = exporter.export(impl);
	    impl.setProxy(proxy);

	    sinkName = (String) config.getEntry( "HttpSinkServer", 
						   "sinkName", 
						   String.class);

	    serviceIDFile = (File) config.getEntry("HttpSinkServer", 
						   "serviceIdFile", 
						   File.class); 
	    getOrMakeServiceID(serviceIDFile);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   null,  // unicast locators
					   null); // DiscoveryListener
	    joinMgr = new JoinManager(proxy, // service proxy
				      new Entry[] {new Name(sinkName)},  // attr sets
				      serviceID,  // ServiceID
				      mgr,   // DiscoveryManager
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    private void getOrMakeServiceID(File serviceIDFile) {
	try {
	    ObjectInputStream ois = 
		new ObjectInputStream(new FileInputStream(serviceIDFile));
	    serviceID = (ServiceID) ois.readObject();
	} catch(Exception e) {
	    System.out.println("Couldn't get service IDs - generating new ones");
	    try {
		ObjectOutputStream oos = 
		    new ObjectOutputStream(new FileOutputStream(serviceIDFile));

		Uuid uuid = UuidFactory.generate();
		serviceID = new ServiceID(uuid.getMostSignificantBits(), 
				      uuid.getLeastSignificantBits());
		oos.writeObject(serviceID);
	    } catch(Exception e2) {
		System.out.println("Couldn't save ids");
		e2.printStackTrace();
	    }
	}
    }
} // SinkServer

This gets information from a configuration file, such as

import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
import java.io.File;

HttpSinkServer {
    exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
                                     new BasicILFactory()); 
    sinkName = new String("Jan's laptop");
    serviceIdFile = new File("sinkServiceId.id");
}

13.2 Source server

A server could publish a single service, as we have done with previous examples. However, it could also publish multiple services. In my home system, every track on a CD is published as a separate service: a "CD service" publishes itself and all of its tracks as separate services. It relies on a configuration file to specify each track. The code look likes



/**
 * FileDirectory.java
 */

package filedir;

import common.*;
import java.io.*;
import java.util.Arrays;
import java.util.ArrayList;

import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.lease.LeaseRenewalManager;
import net.jini.lookup.entry.*;
import net.jini.core.entry.Entry;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import net.jini.core.discovery.LookupLocator;
import net.jini.discovery.LookupDiscovery;
import java.net.URL;
import net.jini.discovery.LookupDiscoveryManager;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceIDListener;
import net.jini.core.lookup.ServiceID;

import net.jini.id.UuidFactory;
import net.jini.id.Uuid;

import net.jini.config.*; 
import net.jini.export.*; 
import com.sun.jini.config.Config;
				       
import java.util.Vector;

import http.*;
import transport.*;

/**
 * Create and publish a set of services, as specified in a
 */

public class FileDirectory implements Directory {
    private ServiceID serviceID = null;
    private ServiceID[] serviceIDs;
    protected Remote proxy;
    protected HttpSourceImpl impl;
    private Entry[] dirEntries;
    private String cdInfo;

    // info pulled out of a Configuration
    private URL url = null;
    private Exporter exporter = null;
    private String cdIndexDiscID;
    private String cddbDiscID;
    private URL[] trackURLs;
    private String[] trackNames;
    private Configuration config;
    private File serviceIDFile;

    // JoinManager's to keep the services alive
    private JoinManager[] joinMgrs;
    private JoinManager dirJoinMgr;
    private LeaseRenewalManager leaseRenewalManager;

    private Vector sources = new Vector();
    /**
     * Uses the configuration files in the directory to produce
     * a list of services. Each file should contain a url and entries
     * <code><pre>     
     *     url = new URL("http://jannote.jan.home/soundfiles/raycharles/audio_01.wav");
     *     entries = new Entry[] {new Name("Ray Charles track 01")
     * </pre></code>
     * The natural order of files in the directory produces the ordering
     * of urls and of services
     */    
    public FileDirectory(String[] configArgs, LeaseRenewalManager leaseRenewalManager) {
	this.leaseRenewalManager = leaseRenewalManager;

	getConfiguration(configArgs);
	cdInfo = getCDInfo();

	joinMgrs = new JoinManager[trackURLs.length];

	for (int n = 0; n < trackURLs.length; n++) {
	    makeFileService(n, trackURLs[n]);
	}

	makeDirService();
    }

    public ServiceID[] getServiceIDs() throws RemoteException {
	return serviceIDs;
    }
    
    private void getConfiguration(String[] configArgs) {
	try {
	    // get the configuration (by default a FileConfiguration)
	    // in configArgs[0]
	    config = ConfigurationProvider.getInstance(configArgs); 
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	try {
	    Class cls = Entry[].class;
	    System.out.println(cls.toString());
	    dirEntries = (Entry []) config.getEntry("HttpFileSourceServer",
						 "entries",
						 cls,
						 null);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    cdIndexDiscID = (String) config.getEntry("HttpFileSourceServer",
						     "cdIndexDiscID",
						     String.class,
						     null);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    // This is an unsigned long - sometimes too big for Java long
	    cddbDiscID = (String) config.getEntry("HttpFileSourceServer",
						  "cddbDiscID",
						  String.class,
						  null);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}


	try {
	    Class cls = URL[].class;
	    System.out.println(cls.toString());
	    trackURLs = (URL []) config.getEntry("HttpFileSourceServer",
						 "trackURLs",
						 cls,
						 null);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    Class cls = String[].class;
	    System.out.println(cls.toString());
	    trackNames = (String []) config.getEntry("HttpFileSourceServer",
						     "trackNames",
						     cls,
						     null);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}

	try {
	    serviceIDFile = (File) config.getEntry("HttpFileSourceServer",
						   "serviceIDFile",
						   File.class,
						   null);
	    getOrMakeServiceIDs(serviceIDFile);
	} catch(Exception e) {
	    System.err.println("Config error: " + e.toString());
	}
    }

    private void getOrMakeServiceIDs(File serviceIDFile) {
	// try to read all the service IDs as
	// objects from the file
	serviceIDs = new ServiceID[trackURLs.length];
	try {
	    ObjectInputStream ois = 
		new ObjectInputStream(new FileInputStream(serviceIDFile));
	    serviceID = (ServiceID) ois.readObject();
	    System.out.println("Got dir service id " + serviceID);
	    for (int n = 0; n < trackURLs.length; n++) {
		serviceIDs[n] =  (ServiceID) ois.readObject();
		System.out.println("Got service id " + serviceIDs[n]);
	    }
	} catch(Exception e) {
	    System.out.println("Couldn't get service IDs - generating new ones");
	    try {
		ObjectOutputStream oos = 
		    new ObjectOutputStream(new FileOutputStream(serviceIDFile));

		Uuid uuid = UuidFactory.generate();
		serviceID = new ServiceID(uuid.getMostSignificantBits(), 
				      uuid.getLeastSignificantBits());
		oos.writeObject(serviceID);

		for (int n = 0; n < serviceIDs.length; n++) {
		    Uuid uuidFile = UuidFactory.generate();
		    ServiceID id = new ServiceID(uuidFile.getMostSignificantBits(), 
						 uuidFile.getLeastSignificantBits());
		    oos.writeObject(id);
		    serviceIDs[n] = id;
		    System.out.println("Generating service id " + serviceIDs[n]);
		}
	    } catch(Exception e2) {
		System.out.println("Couldn't save ids");
		e2.printStackTrace();
	    }
	}
    }

    private void makeDirService() {
	try {
	    exporter = (Exporter) config.getEntry( "HttpFileSourceServer", 
							    "exporter", 
							    Exporter.class);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	try {
	    System.out.println("Export dir using exporter " + exporter);
	    // export an object of this class
	    proxy = exporter.export(this);
	} catch(java.rmi.server.ExportException e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}
	
	dirJoinMgr = registerService(proxy, serviceID, dirEntries);
     }

    private void makeFileService(int index, URL url) {

	Exporter exporter = null;
	try {
	    exporter = (Exporter) config.getEntry( "HttpFileSourceServer", 
							    "exporter", 
							    Exporter.class);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	String urlStr = url.toString();
 	System.out.println("URL is " + urlStr);
	try {
	    if (urlStr.endsWith("wav")) {
		impl = new HttpWAVSourceImpl(url);
	    } else if (urlStr.endsWith("mp3")) {
		impl = new HttpMP3SourceImpl(url);
	    } else if (urlStr.endsWith("ogg")) {
		impl = new HttpOggSourceImpl(url);
	    } else {
		System.out.println("Can't handle presentation type: " + 
				   url);
		return;
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	    return;
	}
	sources.add(impl);

	try {
	    System.out.println("Export file " +  url + " using exporter " + exporter);
	    // export an object of this class
	    proxy = exporter.export(impl);
	} catch(java.rmi.server.ExportException e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	String  trackInfo = getTrackInfo(index);

	Entry[] entries = new Entry[] {new Name(trackInfo)};
	joinMgrs[index] = registerService(proxy, serviceIDs[index], entries);
    }

    private JoinManager registerService(Remote proxy, ServiceID serviceID, Entry[] entries) {
	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   //new LookupLocator[] {
					   //    new LookupLocator("jini://jannote.jan.home/")},
					   null,
					   // unicast locators
					   null); // DiscoveryListener

	    System.out.println("Registering with id " + serviceID);
	    joinMgr = new JoinManager(proxy,     // service proxy
				      entries,   // attr sets
				      serviceID, // ServiceID
				      mgr,       // DiscoveryManager
				      leaseRenewalManager);
	    
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	return joinMgr;
    }

    public String getTrackInfo(int index) {
	// get from trackNames if possible
	if (trackNames != null) {
	    if ((trackNames.length > index) &&
		(trackNames[index] != null)) {
		return(cdInfo + ": " + trackNames[index]);
	    }
	}

	// failing that, get from CDDB d/b if possible
	// ... not yet implemented

	// failing that...
	String indexStr = null;
	if (index < 9) indexStr = "0" + (index + 1);
	else indexStr = "" + (index + 1);

	return(cdInfo + ": Track " + indexStr);
    }

    public String getCDInfo() {
	// get from CDDB d/b if possible
	// failing that...
	for (int n = 0; n  < dirEntries.length; n++) {
	    if (dirEntries[n] instanceof Name) {
		return ((Name) dirEntries[n]).name;
	    }
	}
	return "";
    }

    public static void main(String[] argv) {
	LeaseRenewalManager leaseRenewalManager = new LeaseRenewalManager();

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	FileDirectory fd = new FileDirectory(argv, 
					     leaseRenewalManager);

       // stay around forever
        Object keepAlive = new Object();
        synchronized(keepAlive) {
            try {
                keepAlive.wait();
            } catch(InterruptedException e) {
                // do nothing
            }
        }
    }
}// FileDirectory

14. Clients

A client will locate sources and sinks and allow a user to make selections from them. Each sink will be told about the selected sources, and each source will be told about the selected sinks. The client may register itself as a listener for events (such as STOP) from the services. Then the client will ask the sources to play() and the sinks to record().

14.1 Basic client

A really basic client will just find a sink and a source (any source, any sink), tell each other about the other and then play/record to audio stream. This can be done by


package client;

import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;

import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.core.lookup.ServiceItem;
import net.jini.lease.LeaseRenewalManager;

import common.*;

/**
 * BasicClient.java
 */

public class BasicClient {

    private static final long WAITFOR = 100000L;
    private ServiceDiscoveryManager clientMgr = null;

    public static void main(String argv[]) {
	new BasicClient();

        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(2*WAITFOR);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public BasicClient() {
	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null, // unicast locators
                                           null); // DiscoveryListener
	    clientMgr = new ServiceDiscoveryManager(mgr, 
						new LeaseRenewalManager());
	} catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

	// find a source and sink
	Sink sink = (Sink) getService(Sink.class);
	Source source = (Source) getService(Source.class);

	// tell them about each other
	try {
	    source.addSink(sink);
	    sink.addSource(source);
	} catch(Exception e) {
	    System.err.println("Error setting source or sink " + e);
	    System.exit(1);
	}

	// play the audio
	try {
	    source.play();
	    sink.record();
	} catch(Exception e) {
	    System.out.println("Error in playing " + e);
	    System.exit(1);
	}
    }

    private Object getService(Class cls) {
  
	Class [] classes = new Class[] {cls};
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       null);

	ServiceItem item = null;
	// Try to find the service, blocking till timeout if necessary
	try {
	    item = clientMgr.lookup(template, 
				    null, // no filter 
				    WAITFOR); // timeout
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	if (item == null) {
	    // couldn't find a service in time
	    System.out.println("no service for class " + cls);
	    System.exit(1);
	}

	// Return the service
	return item.service;
    }
} // BasicClient


14.2 GUI client

A more complex client will monitor the sinks and sources (and directories) and display them in suitable panels. These panels will allow selections of sources and sink. The GUIClient looks after service management.


/**
 * GUIClient.java
 */

package client;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.lookup.ServiceDiscoveryListener;
import net.jini.lookup.ServiceDiscoveryEvent;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.lookup.ServiceItem;
import net.jini.lookup.ServiceDiscoveryManager;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lease.LeaseRenewalManager;
import net.jini.lookup.LookupCache;
import net.jini.core.lookup.ServiceID;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceItemFilter;
import java.rmi.Remote;

import net.jini.config.*;
import net.jini.export.*;

import common.*;

/** 
 * An A/V client that monitors sources and sinks and
 */

public class GUIClient implements ServiceDiscoveryListener {

    private ClientFrame clientFrame;
    private LookupCache cache;
    private static String CONFIG_FILE = "jeri/http_sink_server.config";

    public static void main(String argv[]) {
	ClientFrame cf = new ClientFrame();
	GUIClient client = new GUIClient(cf);
	cf.setClient(client);

	cf.setSize(600, 600);
	cf.setVisible(true);

	/*
        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(100000L);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
	*/
    }

    public GUIClient(ClientFrame cf) {
	clientFrame = cf;

        ServiceDiscoveryManager clientMgr = null;

	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null,  // unicast locators
                                           null); // DiscoveryListener
            clientMgr = new ServiceDiscoveryManager(mgr, 
                                                new LeaseRenewalManager());
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
  
        ServiceTemplate template = new ServiceTemplate(null, null, 
                                                       null);
        try {
            cache = clientMgr.createLookupCache(template, 
                                                null,  // no filter
                                                this); // listener
        } catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    // methods for ServiceDiscoveryListener
    public void serviceAdded(ServiceDiscoveryEvent evt) {
	// evt.getPreEventServiceItem() == null
	ServiceItem postItem = evt.getPostEventServiceItem();

	System.out.println("Service appeared: " +
			   postItem.service.getClass().toString());
	if (postItem.service instanceof Directory) {
	    System.out.println("  is dir");
	    addDirectory(postItem);

	}
	if (postItem.service instanceof Sink) {
	    System.out.println("  is sink");
	    clientFrame.addSink(postItem);
	}
	if (postItem.service instanceof Source) {
	    System.out.println("  is source");
	    clientFrame.addSource(postItem);
	}
    }

    public void serviceChanged(ServiceDiscoveryEvent evt) {
	ServiceItem preItem = evt.getPostEventServiceItem();
	ServiceItem postItem = evt.getPreEventServiceItem() ;
	System.out.println("Service changed: " +
			   postItem.service.getClass().toString());
    }
    public void serviceRemoved(ServiceDiscoveryEvent evt) {
	// evt.getPostEventServiceItem() == null
	ServiceItem preItem = evt.getPreEventServiceItem();
	System.out.println("Service disappeared: " +
			   preItem.service.getClass().toString());

	if (preItem.service instanceof Directory) {
	    System.out.println("  was dir");
	    clientFrame.removeDirectory(preItem);
	}
	if (preItem.service instanceof Sink) {
	    System.out.println("  was sink");
	    clientFrame.removeSink(preItem);
	}
	if (preItem.service instanceof Source) {
	    System.out.println("  was source");
	    clientFrame.removeSource(preItem);
	}

    }
    
    private void addDirectory(ServiceItem item) {
	Object node = clientFrame.addDirectory(item);
	ServiceID[] ids = null;
	try {
	    ids = ((Directory) item.service).getServiceIDs();
	} catch(RemoteException e) {
	    System.err.println(e.toString());
	    return;
	}

	for (int n = 0; n < ids.length; n++) {
	    final ServiceID id = ids[n];
	    // find a service in the cache that matches the service ID
	    // we are looking for - doesn't seem to be a way to retrieve
	    // by serviceID
	    ServiceItemFilter filter = new ServiceItemFilter() {
		    public boolean check(ServiceItem dirItem) {
			if (dirItem.serviceID.equals(id)) {
			    return true;
			} else {
			    return false;
			}
		    }
		};
	    ServiceItem dirItem = cache.lookup(filter);
	    if (dirItem != null) {
		clientFrame.addDirectoryElement(node, dirItem);
	    }
	}
    }

    public Remote export(PlayFrame cf) {
	String[] configArgs = new String[] {CONFIG_FILE};
	Remote proxy;

	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 
	    
	    // and use this to construct an exporter
	    Exporter exporter = (Exporter) config.getEntry( "HttpSinkServer", 
							    "exporter", 
							    Exporter.class); 
	    // export an object for the client listener
	    proxy = exporter.export(cf);
	    return proxy;

	} catch(Exception e) {
	    e.printStackTrace();
	    return null;
	}
    }
} // GUIClient

The ClientFrame looks after display and selection of sources/directories/sinks. It shows each of these in Swing JTabbedPane. The directory information is displayed using a JTree (this is good for playing a CD in track order).

The individual sources are shown in a JList with the services alphabetically sorted.
The individual sinks are shown in a JList.

The ClientFrame is



/**
 * ClientFrame.java
 */

package client;

import common.*;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceID;
import java.rmi.RemoteException;
import java.util.Enumeration;
import net.jini.core.event.RemoteEvent;
import java.rmi.Remote;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;
import java.io.IOException;

/**
 * The main Swing JFrame for the GUIClient
 */

public class ClientFrame extends JFrame
				 implements ActionListener, 
					    ListSelectionListener,
					    TreeSelectionListener,
					    TreeExpansionListener,
					    RemoteEventListener {

    private GUIClient client;

    private JList sources = new JList();
    private JList sinks = new JList();
    private JTree directories = new JTree();

    private JButton playFolderBtn = new JButton("Play folder");
    private JButton playSourcesBtn = new JButton("Play sources");
    private JButton stopBtn = new JButton("Stop");

    private Remote proxy;

    public ClientFrame() {
	super("Audio Router");
	makeLayout();
	addListeners();
	setupLists();
    }

    public void setClient(GUIClient client) {
	this.client = client;
    }

    private void makeLayout() {
	Container contentPane = getContentPane();
	contentPane.setLayout(new BorderLayout());

	JPanel bottom = new JPanel();
	JTabbedPane top = new JTabbedPane();
	contentPane.add(bottom, BorderLayout.SOUTH);
	contentPane.add(top, BorderLayout.CENTER);

	JPanel dirPane = new JPanel();
	JPanel sourcePane = new JPanel();
	JPanel sinkPane = new JPanel();
	top.add("Folders", dirPane);
	top.add("Sources", sourcePane);
	top.add("Sinks", sinkPane);
	
	dirPane.setLayout(new BorderLayout());
	JScrollPane js0 = new  JScrollPane();
	js0.getViewport().setView(directories);
	dirPane.add(js0,
		       BorderLayout.CENTER);

	sourcePane.setLayout(new BorderLayout());
	JScrollPane js1 = new  JScrollPane();
	js1.getViewport().setView(sources);
	sourcePane.add(js1,
		       BorderLayout.CENTER);

	sinkPane.setLayout(new BorderLayout());
	JScrollPane js2 = new  JScrollPane();
	js2.getViewport().setView(sinks);
	sinkPane.add(js2, BorderLayout.CENTER);

	bottom.setLayout(new BorderLayout());

	JPanel buttons = new JPanel();
	buttons.setLayout(new GridLayout(1, 3));
	buttons.add(playFolderBtn);
	buttons.add(playSourcesBtn);
	// buttons.add(stopBtn);

	bottom.add(buttons, BorderLayout.CENTER);
    }

    private void addListeners() {
	playFolderBtn.addActionListener(this);
	playSourcesBtn.addActionListener(this);
 	stopBtn.addActionListener(this);

	directories.addTreeSelectionListener(this);
	directories.addTreeExpansionListener(this);

	sources.addListSelectionListener(this);

	sinks.addListSelectionListener(this);
    }

    private void setupLists() {
	LabelCellRenderer labelRend = new LabelCellRenderer();
    
	directories.setCellRenderer(labelRend);
	DefaultMutableTreeNode root = new DefaultMutableTreeNode("Directories");
	directories.setModel(new DefaultTreeModel(root));

	sources.setCellRenderer(labelRend);
	sources.setModel(new DefaultListModel());

	sinks.setCellRenderer(labelRend);
	sinks.setModel(new DefaultListModel());
    }

    public void actionPerformed(ActionEvent evt) {
	Object[] sourceSels = null;
	if (evt.getSource() == playSourcesBtn) {
	    sourceSels = sources.getSelectedValues();
	} else if (evt.getSource() == playFolderBtn) {
	    TreePath[] selectionPaths = directories.getSelectionPaths();
	    if (selectionPaths == null) {
		sourceSels = null;
	    } else {
		sourceSels = new Object[selectionPaths.length];
		for (int n = 0; n < sourceSels.length; n++) {
		    DefaultMutableTreeNode node = (DefaultMutableTreeNode) 
			selectionPaths[n].
			getLastPathComponent();
		    Object lastComponent = node.getUserObject();
		    sourceSels[n] = lastComponent;
		    }
	    }
	}

	if ((sourceSels == null) || (sourceSels.length == 0)) {
	    JOptionPane.showMessageDialog(null, 
					  "No source selected", "Source is null",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	}
	
	Object[] sinkSels = sinks.getSelectedValues();
	if (sinkSels == null) {
	    JOptionPane.showMessageDialog(null, 
					  "No sink selected", "Sink is null",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	}
	
	ServiceItem[] sourceSelections = new ServiceItem[sourceSels.length];
	for (int n = 0; n < sourceSels.length; n++) {
	    sourceSelections[n] = (ServiceItem) sourceSels[n];
	}
	
	ServiceItem[] sinkSelections = new ServiceItem[sinkSels.length];
	for (int n = 0; n < sinkSels.length; n++) {
	    sinkSelections[n] = (ServiceItem) sinkSels[n];
	}
	// Sink sink = (Sink) sinkSelections[0].service;
	
	new PlayFrame(client).play(sourceSelections, sinkSelections[0]);
	// play(sourceSelections, sinkSelections[0]);
    }

    private void play(ServiceItem[] sourceSelections, ServiceItem sinkItem) {
	if ((sourceSelections == null) || (sourceSelections.length == 0)) {
	    System.out.println("Play: null sources");
	    return;
	}
	Source source = (Source) sourceSelections[0].service;
	Sink sink = (Sink) sinkItem.service;

	ServiceItem[] rest = new ServiceItem[sourceSelections.length - 1];
	for (int n = 0; n < rest.length; n++) {
	    rest[n] = sourceSelections[n + 1];
	}

	MarshalledObject handback = null;
	try {
	    handback = new MarshalledObject(new SourceSink(rest, sinkItem));
	} catch(java.io.IOException e) {
	    e.printStackTrace();
	    return;
	}

	try {
	    if (proxy == null) {
		// proxy = client.export(this);
	    }
	    // source.addSourceListener((RemoteEventListener) proxy, null);
	    System.out.println("Added source " + source + " proxy " + proxy);
	    sink.addSinkListener((RemoteEventListener) proxy, handback);
	    System.out.println("Added sink " + sink + " proxy " + proxy + " handback " + 
			       ((SourceSink) handback.get()).sources);
	} catch(RemoteException e) {
	    e.printStackTrace();
	} catch(Exception e) {
	    e.printStackTrace();
	}

	try{
	    System.out.println("Setting sink to: " + sink);
	    source.addSink(sink);
	    System.out.println("Setting source to: " + source);
	    sink.addSource(source);
	    System.out.println("Playing" + sourceSelections[0].attributeSets[0]);
	    source.play();
	    sink.record();
	} catch(AlreadyPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source already playing",
					  "Play error",
					  JOptionPane.ERROR_MESSAGE);
	} catch(AlreadyRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink already recording",
					  "Record error",
					  JOptionPane.ERROR_MESSAGE);
	} catch(Exception e) {
	    // IncompatableSink/Source
	    e.printStackTrace();
	}
    }

    private void stop(ServiceItem[] sourceSelections, ServiceItem sinkItem) {
	// Source source = (Source) sourceSelections[0].service;
	System.out.println("Stopping");
	Sink sink = (Sink) sinkItem.service;

	try{
	    // source.stop();
	    sink.stop();
	    /*
	} catch(NotPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source not playing",
					  "Play stop error",
					  JOptionPane.ERROR_MESSAGE);
	    */
	} catch(NotRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink not recording",
					  "Record stop error",
					  JOptionPane.ERROR_MESSAGE);
	} catch(Exception e) {
	    // ignore?
	    e.printStackTrace();
	}
    }

    public void notify(RemoteEvent evt) {
	Object src = evt.getSource();
	System.out.println("Updating " + src);
	if ((src instanceof Sink) && 
	    (evt.getID() == Sink.STOP)) {
	    System.out.println("Sink stopped event");
	    Sink sink = (Sink) src;

	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
	    } catch(RemoteException e) {
		// ignore
	    } catch(common.NoSuchListenerException e) {
		// ignore
	    }

	    MarshalledObject handback = evt.getRegistrationObject();
	    SourceSink ss = null;
	    try {
		ss = (SourceSink) handback.get();
	    } catch(Exception e) {
		e.printStackTrace();
		return;
	    }

	    ServiceItem[] sources = ss.sources;
	    ServiceItem sinkItem = ss.sink;
	    System.out.println("  stop -> play: sources " + sources + " sink " + sink);
	    play(sources, sinkItem);

	} else if ((src instanceof Source) && 
	    (evt.getID() == Source.STOP)) {
	    System.out.println("Source stopped event");
	}
    }

    /**********************************
     * Source tree manipulation methods
     */
    public void valueChanged(TreeSelectionEvent evt) {
	JTree tree = (JTree) evt.getSource();
	
	TreePath selectionPath = tree.getSelectionPath();
	if (selectionPath == null) {
	    return;
	}

	DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectionPath.
	    getLastPathComponent();
	Object lastComponent = node.getUserObject();
	System.out.println("Selection: " + lastComponent.toString());

	if ( ! node.isLeaf()) {
	    // select all children of this node
	    TreePath[] childPaths = new TreePath[node.getChildCount()];
	    Object[] selPathObjects = selectionPath.getPath();
	    int selPathObjectsLength = selPathObjects.length;
	    
	    int n = 0;
	    Enumeration children = node.children();
	    while (children.hasMoreElements()) {
		Object[] childPathObjects = new Object[selPathObjectsLength + 1]; 
		for (int m = 0; m < selPathObjectsLength; m++) {
		    childPathObjects[m] = selPathObjects[m];
		}
		childPathObjects[selPathObjectsLength] = children.nextElement();
		
		childPaths[n++] = new TreePath(childPathObjects);
	    }
	    tree.setSelectionPaths(childPaths);
	    
	}
    }
    
    public void treeCollapsed(TreeExpansionEvent evt) {
	System.out.println("Tree collapsed " + evt.getPath());
    }
    
    public void treeExpanded(TreeExpansionEvent evt) {
	System.out.println("Tree expanded " + evt.getPath());
    }
    
    public DefaultMutableTreeNode addDirectory(ServiceItem item) {
	DefaultTreeModel model = (DefaultTreeModel) directories.getModel();
	
	DefaultMutableTreeNode newNode =     
	    addDirectoryTreeNode(new ComparableServiceItem(item), model);
	return newNode;
    }
    
    public void addDirectoryElement(Object node, ServiceItem item) {
	final DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) node;
	final DefaultMutableTreeNode newNode =     
	    new DefaultMutableTreeNode(new ComparableServiceItem(item));
	Runnable addDirElmt = new Runnable() {
		public void run() {
		    parentNode.add(newNode);
		}
	    };
	// Swing isn't thread-safe: all model changes 
	// have to be placed in the event queue
	SwingUtilities.invokeLater(addDirElmt);
    }
    
    public void removeDirectory(ServiceItem item) {
 	DefaultTreeModel model = (DefaultTreeModel) directories.getModel();
	removeTreeItem(new ComparableServiceItem(item), model);
    }
    
    
    private DefaultMutableTreeNode addDirectoryTreeNode(final ComparableServiceItem item, 
							final DefaultTreeModel model) {
	final DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(item);
	
	Runnable insertElement = new Runnable() {
		public void run() {
		    System.out.println("Inserting dir tree node " + item);
		    DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
		    
		    Enumeration elmts = root.children();
		    
		    int n = 0;
		    while (elmts.hasMoreElements()) {
			DefaultMutableTreeNode node = (DefaultMutableTreeNode) elmts.nextElement();
			if (item.compareTo(node.getUserObject()) <= 0) {
			    break;
			}
			n++;
		    }
		    model.insertNodeInto(newNode, root, n);
		}
	    };
	try {
	    // Swing isn't thread-safe: all model changes 
	    // have to be placed in the event queue
	    SwingUtilities.invokeAndWait(insertElement);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	} catch(java.lang.reflect.InvocationTargetException e) {
	    e.printStackTrace();
	}
  	return newNode;
    }

    private DefaultMutableTreeNode removeTreeItem(final ComparableServiceItem item, 
						  final DefaultTreeModel model) {
	final DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(item);

	Runnable removeElement = new Runnable() {
		public void run() {
		    System.out.println("Removing tree node " + item);
		    DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
		    
		    Enumeration elmts = root.children();
		    
		    DefaultMutableTreeNode foundNode = null;
		    while (elmts.hasMoreElements()) {
			DefaultMutableTreeNode node = (DefaultMutableTreeNode) elmts.nextElement();
			if (item.compareTo(node.getUserObject()) == 0) {
			    System.out.println("Removing directory");
			    node.removeFromParent();
			    return;
			}
		    }
		    System.out.println("Failed to remove directory");
		}
	    };
	try {
	    // Swing isn't thread-safe: all model changes 
	    // have to be placed in the event queue
	    SwingUtilities.invokeAndWait(removeElement);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	} catch(java.lang.reflect.InvocationTargetException e) {
	    e.printStackTrace();
	}
  	return newNode;
    }



    /********************************** 
     * Source List manipulation methods
     */
    public void addSource(ServiceItem item) {
	DefaultListModel model = (DefaultListModel) sources.getModel();

	insertListItem(new ComparableServiceItem(item), model);
    }
    public void removeSource(final ServiceItem item) {
 	final DefaultListModel model = (DefaultListModel) sources.getModel();
	Runnable removeElement = new Runnable() {
		public void run() {
		    if (model.removeElement(new ComparableServiceItem(item))) {
			System.err.println("Removed source elmt " + item.service);
		    } else {
			System.err.println("Failed to remove source elmt " + item.service);
		    }
		}
	    };
	// Swing isn't thread-safe: all model changes 
	// have to be placed in the event queue
	SwingUtilities.invokeLater(removeElement);
   }



    /********************************
     * Sink list manipulation methods
     */

    public void addSink(ServiceItem item) {
	DefaultListModel model = (DefaultListModel) sinks.getModel();

	insertListItem(new ComparableServiceItem(item), model);
     }

    public void removeSink(final ServiceItem item) {
 	final DefaultListModel model = (DefaultListModel) sinks.getModel();
	Runnable removeElement = new Runnable() {
		public void run() {
		    if (model.removeElement(new ComparableServiceItem(item))) {
			System.err.println("Removed sink elmt " + item.service);
		    } else {
			System.err.println("Failed to remove sink elmt " + item.service);
		    }
		}
	    };
	// Swing isn't thread-safe: all model changes 
	// have to be placed in the event queue
	SwingUtilities.invokeLater(removeElement);
    }

    /*********************************
     * General list methods
     */
    public void valueChanged(ListSelectionEvent evt) {
	if (evt.getValueIsAdjusting()) {
	    return;
	}
    }


    private void insertListItem(final ComparableServiceItem item, 
				final DefaultListModel model) {
	Runnable insertElement = new Runnable() {
		public void run() {
		    Enumeration elmts = model.elements();
		    
		    int n = 0;
		    while (elmts.hasMoreElements()) {
			if (item.compareTo(elmts.nextElement()) <= 0) {
			    break;
			}
			n++;
		    }
		    model.add(n, item);
		}
	    };
	// there may be other insert's in the event queue, so we can't
	// just compare the current list with the item. We have to wait
	// till all the pending events have been processed, and then compare
	try {
	    SwingUtilities.invokeAndWait(insertElement);
	} catch(InterruptedException e) {
	    e.printStackTrace();
	} catch(java.lang.reflect.InvocationTargetException e) {
	    e.printStackTrace();
	}
    }
 }// ClientFrame

When source(s) and a sink are selected and a "play" button is pressed, a PlayFrame is displayed. This shows the currently chosen set of sources. This frame listens for events from the sink so that it can tell when a particular track has finished. Then it will start playing the next one. It gets the information about the "next one" (and all of its successors) from the "handback" object, which has a list of the remaining tracks to play



/**
 * PlayFrame.java
 */

package client;

import common.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import net.jini.core.event.RemoteEvent;
import java.rmi.Remote;
import net.jini.core.event.RemoteEventListener;
import java.rmi.MarshalledObject;
import java.io.IOException;
import java.rmi.RemoteException;
import javax.swing.event.*;

import net.jini.core.lookup.ServiceItem;

/**
 * Play a selection of sources to a single sink
 */

public class PlayFrame extends JFrame implements ActionListener,
						 ChangeListener,
						 RemoteEventListener {
    private ServiceItem[] sources;
    private ServiceItem sinkItem;
    private Sink sink;
    private Source source;

    private JList sourceList = new JList();
    private JButton stopBtn = new JButton("Stop");
    private JButton stopAllBtn = new JButton("Stop all");
    private JLabel sinkLabel = new JLabel();
    private JSlider sourceVolumeCtl = new JSlider();
    private JSlider sinkVolumeCtl = new JSlider();

    private DefaultListModel  model;
    private Remote proxy;
    private GUIClient client;

    public PlayFrame(GUIClient client) {
	setTitle("Player");
	this.client = client;

	JPanel bottomPanel = new JPanel();
	bottomPanel.setLayout(new BorderLayout());
	Container contentPane = getContentPane();
	contentPane.setLayout(new BorderLayout());
	contentPane.add(sinkLabel, BorderLayout.NORTH);
	contentPane.add(sourceList, BorderLayout.CENTER);
	contentPane.add(bottomPanel, BorderLayout.SOUTH);

	JPanel volumeControls = new JPanel();
	volumeControls.setLayout(new GridLayout(1, 2));
	volumeControls.add(sourceVolumeCtl);
	volumeControls.add(sinkVolumeCtl);

	JPanel buttons = new JPanel();
	buttons.setLayout(new GridLayout(1,3));
	buttons.add(stopBtn);
	buttons.add(stopAllBtn);

	bottomPanel.add(volumeControls, BorderLayout.NORTH);
	bottomPanel.add(buttons, BorderLayout.SOUTH);

	stopBtn.addActionListener(this);
	stopAllBtn.addActionListener(this);

	LabelCellRenderer labelRend = new LabelCellRenderer();
    
	sourceList.setCellRenderer(labelRend);
	model = new DefaultListModel();
	sourceList.setModel(model);

	sourceVolumeCtl.setEnabled(false);
	sinkVolumeCtl.setEnabled(false);
	sourceVolumeCtl.addChangeListener(this);
	sinkVolumeCtl.addChangeListener(this);
	sourceVolumeCtl.setToolTipText("Set volume on the source");
	sinkVolumeCtl.setToolTipText("Set volume on the sink");

	setSize(400, 300);
	setVisible(true);
    }

    public void actionPerformed(ActionEvent evt) {
	if (evt.getSource() == stopBtn) {
	    try {
		sink.stop();
		sinkVolumeCtl.setEnabled(false);
		sourceVolumeCtl.setEnabled(false);
	    } catch(RemoteException e) {
		e.printStackTrace();
	    } catch(NotRecordingException e) {
		e.printStackTrace();
	    }
	} else if (evt.getSource() == stopAllBtn) {
	    // kill the rest of the playlist by removing
	    // our listener and putting in a new one with
	    // null playback
	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
		System.out.println("Removed listener " + proxy);
		sink.addSinkListener((RemoteEventListener) proxy, 
				     new MarshalledObject(
					 new SourceSink(null, null)));
		System.out.println("Added listener with null playback");
	    } catch(RemoteException e) {
		// ignore
		e.printStackTrace();
	    } catch(NoSuchListenerException e) {
		System.err.println("Can't change listener " + proxy);
	    } catch(IOException e) {
		System.err.println("Can't marshall null " + proxy);
	    }

	    // now stop the play
	    try {
		sink.stop();
		sinkVolumeCtl.setEnabled(false);
		sourceVolumeCtl.setEnabled(false);
	    } catch(RemoteException e) {
		e.printStackTrace();
	    } catch(NotRecordingException e) {
		e.printStackTrace();
	    }
	}
    }

    public void stateChanged(ChangeEvent e) {
	JSlider slider = (JSlider) e.getSource();
	int vol = slider.getValue();
	VolumeControl volCtl = null;
	if (slider == sinkVolumeCtl) {
	    volCtl = (VolumeControl) sink;
	} else if (slider == sourceVolumeCtl) {
	    volCtl = (VolumeControl) source;
	}
	try {
	    volCtl.setVolume(vol);
	} catch(RemoteException ex) {
	    // ignore
	}
    }

    public void play(ServiceItem[] sources, ServiceItem sinkItem) {
	if ((sources == null) || (sources.length == 0)) {
	    System.out.println("Play: null sources");
	    // dispose();
	    Runnable doDispose = new Runnable() {
		    public void run() {
			dispose();
		    }
		};
	    SwingUtilities.invokeLater(doDispose);

	    return;
	}

	this.sources = sources;
	this.sinkItem = sinkItem;
	sink = (Sink) sinkItem.service;
	source = (Source) sources[0].service;

	sinkLabel.setText("Playing to " + sinkItem.toString());

	model.clear();
	for (int n = 0; n < sources.length; n++) {
	    model.addElement(sources[n]);
	}

	ServiceItem[] rest = new ServiceItem[sources.length - 1];
	for (int n = 0; n < rest.length; n++) {
	    rest[n] = sources[n + 1];
	}
	MarshalledObject handback = null;
	try {
	    handback = new MarshalledObject(new SourceSink(rest, sinkItem));
	} catch(java.io.IOException e) {
	    e.printStackTrace();
	    return;
	}

	try {
	    if (proxy == null) {
		proxy = client.export(this);
	    }
	    // source.addSourceListener((RemoteEventListener) proxy, null);
	    System.out.println("Added source " + source + " proxy " + proxy);
	    sink.addSinkListener((RemoteEventListener) proxy, handback);
	    System.out.println("Added sink " + sink + " proxy " + proxy + " handback " + 
			       ((SourceSink) handback.get()).sources);
	} catch(RemoteException e) {
	    e.printStackTrace();
	} catch(Exception e) {
	    e.printStackTrace();
	}

	try{
	    System.out.println("Setting sink to: " + sink);
	    source.addSink(sink);
	    System.out.println("Setting source to: " + source);
	    sink.addSource(source);
	    System.out.println("Playing" + sources[0].attributeSets[0]);
	    source.play();
	    sink.record();
	} catch(AlreadyPlayingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Source already playing",
					  "Play error",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	} catch(AlreadyRecordingException e) {
	    JOptionPane.showMessageDialog(null,
					  "Sink already recording",
					  "Record error",
					  JOptionPane.ERROR_MESSAGE);
	    return;
	} catch(Exception e) {
	    // IncompatableSink/Source
	    e.printStackTrace();
	    return;
	}

	System.out.println("Checking VC " + sink);

	if (source instanceof VolumeControl) {
	    System.out.println("Source is VC");
	    enableVolumeControl( (VolumeControl) source, sourceVolumeCtl);
	}
	if (sink instanceof VolumeControl) {
	    System.out.println("Sink is VC");
	    enableVolumeControl( (VolumeControl) sink, sinkVolumeCtl);
	}
    }

    public void notify(RemoteEvent evt) {
	Object src = evt.getSource();
	System.out.println("Updating " + src);
	if ((src instanceof Sink) && 
	    (evt.getID() == Sink.STOP)) {
	    System.out.println("Sink stopped event");
	    Sink sink = (Sink) src;

	    try {
		sink.removeSinkListener((RemoteEventListener) proxy);
		System.out.println("Removed listener " + proxy);
	    } catch(RemoteException e) {
		// ignore
		e.printStackTrace();
	    } catch(NoSuchListenerException e) {
		System.err.println("Can't remove listener " + proxy);
	    }

	    MarshalledObject handback = evt.getRegistrationObject();
	    SourceSink ss = null;
	    try {
		ss = (SourceSink) handback.get();
	    } catch(Exception e) {
		e.printStackTrace();
		return;
	    }
	    if (ss == null) {
		// no more playlist
		return;
	    }

	    final ServiceItem[] sources = ss.sources;
	    final ServiceItem sinkItem = ss.sink;
	    System.out.println("  stop -> play: sources " + sources + " sink " + sink);
	    Runnable doPlay = new Runnable() {
		    public void run() {
			play(sources, sinkItem);
		    }
		};
	    SwingUtilities.invokeLater(doPlay);

	} else if ((src instanceof Source) && 
	    (evt.getID() == Source.STOP)) {
	    System.out.println("Source stopped event");
	}
    }

    private void enableVolumeControl(VolumeControl vol, JSlider slider) {
	int maxVol = 0;
	int currVol = 0;
	try {
	    maxVol = vol.getMaxVolume();
	    if (maxVol <= 0) return;

	    currVol = vol.getVolume();
	    System.out.println("Current vol: " + currVol);
	    if (currVol < 0 || currVol > maxVol) return;
	} catch (RemoteException e) {
	    e.printStackTrace();
	    return;
	}
	slider.setMinimum(0);
	slider.setMaximum(maxVol);
	slider.setValue(currVol);
	slider.setEnabled(true);
    }
}// PlayFrame

15. Conclusion

This chapter has discussed a framework for distributed audio. Jini makes it fairly straightforward to handle service advertisement and discovery, telling services about each other and generatikng and handling remote events. The architecture is extensible just be adding in more interfaces and implementations. For example, I am currently adding a "file sink" so that I can record audio to file. If this is linked to an FM tuner card then I will be able to record my favourite shows from a timer program.

16.

17.

18.

19.

20.

21.


If you found this chapter of value, the full book is available from APress or Amazon . There is a review of the book at Java Zone . The current edition of the book does not yet deal with Jini 2.0, but the next edition will.


This file is Copyright (©) 1999, 2000, 2001, 2003 by Jan Newmarch (http://jan.netcomp.edu.au) jan.newmarch@jan.newmarch.name.

This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v0.4 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.