Project: AirReceiver
/*
 * This file is part of AirReceiver. 
 * 
 * AirReceiver 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 3 of the License, or 
 * (at your option) any later version. 
 
 * AirReceiver 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 AirReceiver.  If not, see <http://www.gnu.org/licenses/>. 
 */
 
package org.phlo.AirReceiver; 
 
import java.io.IOException; 
import java.io.InputStream; 
import java.net.*; 
import java.util.Arrays; 
import java.util.Collections; 
import java.util.Map; 
import java.util.List; 
import java.util.Properties; 
import java.util.concurrent.*; 
import java.util.logging.*; 
import java.awt.*; 
import java.awt.event.*; 
 
import javax.swing.*; 
 
import javax.jmdns.*; 
 
import org.jboss.netty.bootstrap.ServerBootstrap; 
import org.jboss.netty.channel.*; 
import org.jboss.netty.channel.group.*; 
import org.jboss.netty.channel.socket.nio.*; 
import org.jboss.netty.handler.execution.*; 
 
public class AirReceiver { 
 /* Load java.util.logging configuration */ 
 static { 
  final InputStream loggingPropertiesStream = 
   AirReceiver.class.getClassLoader().getResourceAsStream("logging.properties"); 
     try { 
   LogManager.getLogManager().readConfiguration(loggingPropertiesStream); 
  
     catch (final IOException e) { 
      throw new RuntimeException(e.getMessage(), e); 
  
 
  
 private static final Logger s_logger = Logger.getLogger(AirReceiver.class.getName()); 
 
 public static final String Version = getVersion(); 
  
 /**
  * The hardware (MAC) address of the emulated Airport Express 
  */
 
 public static final byte[] HardwareAddressBytes = getHardwareAddress(); 
 
 /**
  * The hardware (MAC) address as a hexadecimal string 
  */
 
 public static final String HardwareAddressString = toHexString(HardwareAddressBytes); 
 
 /**
  * The hostname of the emulated Airport Express 
  */
 
 public static final String HostName = getHostName(); 
 
 /**
  * The AirTunes/RAOP service type 
  */
 
 public static final String AirtunesServiceType = "_raop._tcp.local."
 
 /**
  * The AirTunes/RAOP RTSP port 
  */
 
 public static final short AirtunesServiceRTSPPort = 5000
 
 /**
  * The AirTunes/RAOP M-DNS service properties (TXT record) 
  */
 
 public static final Map<String, String> AirtunesServiceProperties = 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" 
 ); 
 
 /**
  * Global executor service. Used e.g. to initialize the various netty channel factories  
  */
 
 public static final ExecutorService ExecutorService = Executors.newCachedThreadPool(); 
 
 /**
  * Channel execution handler. Spreads channel message handling over multiple threads 
  */
 
 public static final ExecutionHandler ChannelExecutionHandler = new ExecutionHandler( 
  new OrderedMemoryAwareThreadPoolExecutor(400
 ); 
 
 /**
  * Message dispayed in the "About" dialog 
  */
 
 private static final String AboutMessage = 
  "   * AirReceiver " + Version + " *\n" + 
  "\n" + 
  "Copyright (c) 2011 Florian G. Pflug\n" + 
  "\n" + 
  "AirReceiver is free software: you can redistribute it and/or modify\n" + 
  "it under the terms of the GNU General Public License as published by\n" + 
  "the Free Software Foundation, either version 3 of the License, or\n" + 
  "(at your option) any later version.\n" + 
  "\n" + 
  "AirReceiver is distributed in the hope that it will be useful,\n" + 
  "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + 
  "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + 
  "GNU General Public License for more details.\n" + 
  "\n" + 
  "You should have received a copy of the GNU General Public License\n" + 
  "along with AirReceiver.  If not, see <http://www.gnu.org/licenses/>." + 
  "\n\n" + 
  "   * Java ALAC Decoder *\n" + 
  "\n" + 
  "Copyright (c) 2011 Peter McQuillan"
 
 /**
  * JmDNS instances (one per IP address). Used to unregister the mDNS services 
  * during shutdown. 
  */
 
 private static final List<JmDNS> s_jmDNSInstances = new java.util.LinkedList<JmDNS>(); 
 
 /**
  * All open RTSP channels. Used to close all open challens during shutdown. 
  */
 
 private static ChannelGroup s_allChannels = new DefaultChannelGroup(); 
 
 /**
  * Channel handle that registeres the channel to be closed on shutdown 
  */
 
 public static final ChannelHandler CloseChannelOnShutdownHandler = new SimpleChannelUpstreamHandler() { 
  @Override 
  public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception { 
   s_allChannels.add(e.getChannel()); 
   super.channelOpen(ctx, e); 
  
 }; 
   
 /**
  * Map factory. Creates a Map from a list of keys and values 
  *  
  * @param keys_values key1, value1, key2, value2, ... 
  * @return a map mapping key1 to value1, key2 to value2, ... 
  */
 
 private static Map<String, String> map(final String... keys_values) { 
  assert keys_values.length % 2 == 0; 
  final Map<String, String> map = new java.util.HashMap<String, String>(keys_values.length / 2); 
  for(int i=0; i < keys_values.length; i+=2) 
   map.put(keys_values[i], keys_values[i+1]); 
  return Collections.unmodifiableMap(map); 
 
 
 /**
  * Decides whether or nor a given MAC address is the address of some 
  * virtual interface, like e.g. VMware's host-only interface (server-side). 
  *  
  * @param addr a MAC address 
  * @return true if the MAC address is unsuitable as the device's hardware address 
  */
 
 public static boolean isBlockedHardwareAddress(final byte[] addr) { 
  if ((addr[0] & 0x02) != 0
   /* Locally administered */ 
   return true
  else if ((addr[0] == 0x00) && (addr[1] == 0x50) && (addr[2] == 0x56)) 
   /* VMware */ 
   return true
  else if ((addr[0] == 0x00) && (addr[1] == 0x1C) && (addr[2] == 0x42)) 
   /* Parallels */ 
   return true
  else if ((addr[0] == 0x00) && (addr[1] == 0x25) && (addr[2] == (byte)0xAE)) 
   /* Microsoft */ 
   return true
  else 
   return false
 
  
 /**
  * Reads the version from the version.properties file 
  * @return the version 
  */
 
 private static String getVersion() { 
  Properties versionProperties = new Properties(); 
  final InputStream versionPropertiesStream = 
   AirReceiver.class.getClassLoader().getResourceAsStream("version.properties"); 
     try { 
      versionProperties.load(versionPropertiesStream); 
  
     catch (final IOException e) { 
      throw new RuntimeException(e.getMessage(), e); 
  
     return versionProperties.getProperty("org.phlo.AirReceiver.version"); 
 
 
 /**
  * Returns a suitable hardware address. 
  *  
  * @return a MAC address 
  */
 
 private static byte[] getHardwareAddress() { 
  try { 
   /* Search network interfaces for an interface with a valid, non-blocked hardware address */ 
      for(final NetworkInterface iface: Collections.list(NetworkInterface.getNetworkInterfaces())) { 
       if (iface.isLoopback()) 
        continue
       if (iface.isPointToPoint()) 
        continue
 
       try { 
        final byte[] ifaceMacAddress = iface.getHardwareAddress(); 
        if ((ifaceMacAddress != null) && (ifaceMacAddress.length == 6) && !isBlockedHardwareAddress(ifaceMacAddress)) { 
         s_logger.info("Hardware address is " + toHexString(ifaceMacAddress) + " (" + iface.getDisplayName() + ")"); 
            return Arrays.copyOfRange(ifaceMacAddress, 06); 
        
       
       catch (final Throwable e) { 
        /* Ignore */ 
       
      
  
  catch (final Throwable e) { 
   /* Ignore */ 
  
 
  /* Fallback to the IP address padded to 6 bytes */ 
  try { 
   final byte[] hostAddress = Arrays.copyOfRange(InetAddress.getLocalHost().getAddress(), 06); 
   s_logger.info("Hardware address is " + toHexString(hostAddress) + " (IP address)"); 
   return hostAddress; 
  
  catch (final Throwable e) { 
   /* Ignore */ 
  
 
  /* Fallback to a constant */ 
  s_logger.info("Hardware address is 00DEADBEEF00 (last resort)"); 
  return new byte[] {(byte)0x00, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF, (byte)0x00}; 
 
 
 /**
  * Returns the machine's host name 
  *  
  * @return the host name 
  */
 
 private static String getHostName() { 
  try { 
   return InetAddress.getLocalHost().getHostName().split("\\.")[0]; 
  
  catch (final Throwable e) { 
   return "AirReceiver"
  
 
 
 /**
  * Converts an array of bytes to a hexadecimal string 
  *  
  * @param bytes array of bytes 
  * @return hexadecimal representation 
  */
 
 private static String toHexString(final byte[] bytes) { 
  final StringBuilder s = new StringBuilder(); 
  for(final byte b: bytes) { 
   final String h = Integer.toHexString(0x100 | b); 
   s.append(h.substring(h.length() - 2, h.length()).toUpperCase()); 
  
  return s.toString(); 
 
 
 /**
  * Shuts the AirReceiver down gracefully 
  */
 
 public static void onShutdown() { 
  /* Close channels */ 
  final ChannelGroupFuture allChannelsClosed = s_allChannels.close(); 
 
  /* Stop all mDNS responders */ 
  synchronized(s_jmDNSInstances) { 
   for(final JmDNS jmDNS: s_jmDNSInstances) { 
    try { 
     jmDNS.unregisterAllServices(); 
     s_logger.info("Unregistered all services on " + jmDNS.getInterface()); 
    
    catch (final IOException e) { 
     s_logger.info("Failed to unregister some services"); 
    
   
  
 
  /* Wait for all channels to finish closing */ 
  allChannelsClosed.awaitUninterruptibly(); 
   
  /* Stop the ExecutorService */ 
  ExecutorService.shutdown(); 
 
  /* Release the OrderedMemoryAwareThreadPoolExecutor */ 
  ChannelExecutionHandler.releaseExternalResources(); 
 
 
    public static void main(final String[] args) throws Exception { 
     /* Make sure AirReceiver shuts down gracefully */ 
     Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { 
   @Override 
   public void run() { 
    onShutdown(); 
   
     })); 
 
     /* Create about dialog */ 
     final Dialog aboutDialog = new Dialog((Dialog)null); 
     final GridBagLayout aboutLayout = new GridBagLayout(); 
     aboutDialog.setLayout(aboutLayout); 
     aboutDialog.setVisible(false); 
     aboutDialog.setTitle("About AirReceiver"); 
     aboutDialog.setResizable(false); 
     
      /* Message */ 
      final TextArea title = new TextArea(AboutMessage.split("\n").length + 164); 
      title.setText(AboutMessage); 
      title.setEditable(false); 
      final GridBagConstraints titleConstraints = new GridBagConstraints(); 
      titleConstraints.gridx = 1
      titleConstraints.gridy = 1
      titleConstraints.fill = GridBagConstraints.HORIZONTAL; 
      titleConstraints.insets = new Insets(0,0,0,0); 
      aboutLayout.setConstraints(title, titleConstraints); 
      aboutDialog.add(title); 
     
     
      /* Done button */ 
      final Button aboutDoneButton = new Button("Done"); 
      aboutDoneButton.addActionListener(new ActionListener() { 
    @Override public void actionPerformed(final ActionEvent evt) { 
     aboutDialog.setVisible(false); 
    
      }); 
      final GridBagConstraints aboutDoneConstraints = new GridBagConstraints(); 
      aboutDoneConstraints.gridx = 1
      aboutDoneConstraints.gridy = 2
      aboutDoneConstraints.anchor = GridBagConstraints.PAGE_END; 
      aboutDoneConstraints.fill = GridBagConstraints.NONE; 
      aboutDoneConstraints.insets = new Insets(0,0,0,0); 
      aboutLayout.setConstraints(aboutDoneButton, aboutDoneConstraints); 
      aboutDialog.add(aboutDoneButton); 
     
     aboutDialog.setVisible(false); 
     aboutDialog.setLocationByPlatform(true); 
     aboutDialog.pack(); 
 
     /* Create tray icon */ 
  final URL trayIconUrl = AirReceiver.class.getClassLoader().getResource("icon_32.png"); 
  final TrayIcon trayIcon = new TrayIcon((new ImageIcon(trayIconUrl, "AirReceiver").getImage())); 
  trayIcon.setToolTip("AirReceiver"); 
  trayIcon.setImageAutoSize(true); 
  final PopupMenu popupMenu = new PopupMenu(); 
  final MenuItem aboutMenuItem = new MenuItem("About"); 
  aboutMenuItem.addActionListener(new ActionListener() { 
   @Override 
   public void actionPerformed(final ActionEvent evt) { 
       aboutDialog.setLocationByPlatform(true); 
       aboutDialog.setVisible(true); 
   
  }); 
  popupMenu.add(aboutMenuItem); 
  final MenuItem exitMenuItem = new MenuItem("Quit"); 
  exitMenuItem.addActionListener(new ActionListener() { 
   @Override 
   public void actionPerformed(final ActionEvent evt) { 
    onShutdown(); 
    System.exit(0); 
   
  }); 
  popupMenu.add(exitMenuItem); 
  trayIcon.setPopupMenu(popupMenu); 
  SystemTray.getSystemTray().add(trayIcon); 
 
        /* 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); 
  s_allChannels.add(airTunesRtspBootstrap.bind(new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), AirtunesServiceRTSPPort))); 
        s_logger.info("Launched RTSP service on port " + AirtunesServiceRTSPPort); 
 
     /* Create mDNS responders. */ 
        synchronized(s_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"); 
         s_jmDNSInstances.add(jmDNS); 
 
            /* Publish RAOP service */ 
            final ServiceInfo airTunesServiceInfo = ServiceInfo.create( 
          AirtunesServiceType, 
          HardwareAddressString + "@" + HostName + " (" + iface.getName() + ")"
          AirtunesServiceRTSPPort, 
          0 /* weight */0 /* priority */
          AirtunesServiceProperties 
         ); 
            jmDNS.registerService(airTunesServiceInfo); 
      s_logger.info("Registered AirTunes service '" + airTunesServiceInfo.getName() + "' on " + addr); 
     
     catch (final Throwable e) { 
      s_logger.log(Level.SEVERE, "Failed to publish service on " + addr, e); 
     
       
      
        } 
    } 
}