package com.raventech.airplayserver; import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import javax.jmdns.JmDNS; import javax.jmdns.ServiceInfo; import com.raventech.airplayserver.network.NetworkUtils; import com.raventech.airplayserver.network.raop.RaopRtspPipelineFactory; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.group.ChannelGroup; import org.jboss.netty.channel.group.ChannelGroupFuture; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; import org.jboss.netty.handler.execution.ExecutionHandler; import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor; /** * Android AirPlay Server Implementation * * @author Rafael Almeida * */ public class AirPlayServer implements Runnable { private static final Logger LOG = Logger.getLogger(AirPlayServer.class.getName()); /** * The AirTunes/RAOP service type */ static final String AIR_TUNES_SERVICE_TYPE = "_raop._tcp.local."; /** * The AirTunes/RAOP M-DNS service properties (TXT record) */ static final Map<String, String> AIRTUNES_SERVICE_PROPERTIES = Utils.map( "txtvers", "1", "tp", "UDP", "ch", "2", "ss", "16", "sr", "44100", "pw", "false", "sm", "false", "sv", "false", "ek", "1", "et", "0,1", "cn", "0,1", "vn", "3"); private static AirPlayServer instance = null; public static AirPlayServer getIstance(){ if(instance == null){ instance = new AirPlayServer(); } return instance; } public boolean isOn = false; /** * Global executor service. Used e.g. to initialize the various netty channel factories */ protected ExecutorService executorService; /** * Channel execution handler. Spreads channel message handling over multiple threads */ protected ExecutionHandler channelExecutionHandler; /** * All open RTSP channels. Used to close all open challens during shutdown. */ protected ChannelGroup channelGroup; /** * JmDNS instances (one per IP address). Used to unregister the mDNS services * during shutdown. */ protected List<JmDNS> jmDNSInstances; /** * The AirTunes/RAOP RTSP port */ private int rtspPort = 5000; //default value private AirPlayServer(){ //create executor service executorService = Executors.newCachedThreadPool(); //create channel execution handler channelExecutionHandler = new ExecutionHandler(new OrderedMemoryAwareThreadPoolExecutor(4, 0, 0)); //channel group channelGroup = new DefaultChannelGroup(); //list of mDNS services jmDNSInstances = new java.util.LinkedList<JmDNS>(); } public int getRtspPort() { return rtspPort; } public void setRtspPort(int rtspPort) { this.rtspPort = rtspPort; } public void run() { startService(); } private void startService() { /* Make sure AirPlay Server shuts down gracefully */ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { onShutdown(); } })); LOG.info("VM Shutdown Hook added sucessfully!"); /* Create AirTunes RTSP server */ final ServerBootstrap airTunesRtspBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(executorService, executorService)); airTunesRtspBootstrap.setPipelineFactory(new RaopRtspPipelineFactory()); airTunesRtspBootstrap.setOption("reuseAddress", true); airTunesRtspBootstrap.setOption("child.tcpNoDelay", true); airTunesRtspBootstrap.setOption("child.keepAlive", true); try { channelGroup.add(airTunesRtspBootstrap.bind(new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), getRtspPort()))); } catch (UnknownHostException e) { LOG.log(Level.SEVERE, "Failed to bind RTSP Bootstrap on port: " + getRtspPort(), e); } LOG.info("Launched RTSP service on port " + getRtspPort()); //get Network details NetworkUtils networkUtils = NetworkUtils.getInstance(); String hostName = networkUtils.getHostUtils(); String hardwareAddressString = networkUtils.getHardwareAddressString(); isOn = true; try { /* Create mDNS responders. */ synchronized(jmDNSInstances) { for(final NetworkInterface iface: Collections.list(NetworkInterface.getNetworkInterfaces())) { if ( iface.isLoopback() ){ continue; } if ( iface.isPointToPoint()){ continue; } if ( ! iface.isUp()){ continue; } for(final InetAddress addr: Collections.list(iface.getInetAddresses())) { if ( ! (addr instanceof Inet4Address) && ! (addr instanceof Inet6Address) ){ continue; } try { /* Create mDNS responder for address */ final JmDNS jmDNS = JmDNS.create(addr, hostName + "-jmdns"); jmDNSInstances.add(jmDNS); /* Publish RAOP service */ final ServiceInfo airTunesServiceInfo = ServiceInfo.create( AIR_TUNES_SERVICE_TYPE, hardwareAddressString + "@" + hostName, getRtspPort(), 0 /* weight */, 0 /* priority */, AIRTUNES_SERVICE_PROPERTIES ); jmDNS.registerService(airTunesServiceInfo); LOG.info("Registered AirTunes service '" + airTunesServiceInfo.getName() + "' on " + addr); } catch (final Throwable e) { LOG.log(Level.SEVERE, "Failed to publish service on " + addr, e); } } } } } catch (SocketException e) { LOG.log(Level.SEVERE, "Failed register mDNS services", e); } } //When the app is shutdown protected void onShutdown() { /* Close channels */ final ChannelGroupFuture allChannelsClosed = channelGroup.close(); /* Stop all mDNS responders */ synchronized(jmDNSInstances) { for(final JmDNS jmDNS: jmDNSInstances) { try { jmDNS.unregisterAllServices(); LOG.info("Unregistered all services on " + jmDNS.getInterface()); } catch (final IOException e) { LOG.log(Level.WARNING, "Failed to unregister some services", e); } } } /* Wait for all channels to finish closing */ allChannelsClosed.awaitUninterruptibly(); /* Stop the ExecutorService */ executorService.shutdown(); /* Release the OrderedMemoryAwareThreadPoolExecutor */ channelExecutionHandler.releaseExternalResources(); isOn = false; } public ChannelHandler getChannelExecutionHandler() { return channelExecutionHandler; } public ChannelGroup getChannelGroup() { return channelGroup; } public ExecutorService getExecutorService() { return executorService; } }