Android Open Source - peeler Air Player






From Project

Back to project page peeler.

License

The source code is released under:

MIT License

If you think the Android project peeler listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.example.peeler;
//w w w.  j a  v  a2s.co m
import android.util.Log;

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

import java.lang.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.math.BigInteger;

import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.security.spec.*;

class AirPlayer {
  static public void main(String[] args) throws FileNotFoundException {
    if (args.length == 0 || args.length > 2) {
      System.out.println("airplayer <file> [<volume>]");
      return;
    }

    AirPlayer player = new AirPlayer();
    List<RAOPDevice> devices = AirPlayer.findDevices();

    player.setActiveDevices(devices);
    if (args.length == 2) player.setVolume(Integer.valueOf(args[1]).intValue());
    player.start(args[0]);

    while (true) {
      try { Thread.sleep(0); }
      catch (InterruptedException e) { }
    }

    //player.closeAll();
  }

  static public int samplesPerSecond = 44100;
  static public int bytesPerMilliSec = (samplesPerSecond << 2) / 1000;

  static public int samplesPerPacket = 4096;
  static public int bytesPerPacket = samplesPerPacket << 2;
  static public int deviceBufferSize = (samplesPerSecond << 2) * 2; // size of buffer on remote device

  static final private String[] standardTypes = {
    "_airport._tcp.local",
    "_appletv._tcp.local"
  };

  static final byte[] airportPubKeyModBytes = {
    (byte)0xe7, (byte)0xd7, (byte)0x44, (byte)0xf2, (byte)0xa2, (byte)0xe2, (byte)0x78, (byte)0x8b,
    (byte)0x6c, (byte)0x1f, (byte)0x55, (byte)0xa0, (byte)0x8e, (byte)0xb7, (byte)0x05, (byte)0x44,
    (byte)0xa8, (byte)0xfa, (byte)0x79, (byte)0x45, (byte)0xaa, (byte)0x8b, (byte)0xe6, (byte)0xc6,
    (byte)0x2c, (byte)0xe5, (byte)0xf5, (byte)0x1c, (byte)0xbd, (byte)0xd4, (byte)0xdc, (byte)0x68,
    (byte)0x42, (byte)0xfe, (byte)0x3d, (byte)0x10, (byte)0x83, (byte)0xdd, (byte)0x2e, (byte)0xde,
    (byte)0xc1, (byte)0xbf, (byte)0xd4, (byte)0x25, (byte)0x2d, (byte)0xc0, (byte)0x2e, (byte)0x6f,
    (byte)0x39, (byte)0x8b, (byte)0xdf, (byte)0x0e, (byte)0x61, (byte)0x48, (byte)0xea, (byte)0x84,
    (byte)0x85, (byte)0x5e, (byte)0x2e, (byte)0x44, (byte)0x2d, (byte)0xa6, (byte)0xd6, (byte)0x26,
    (byte)0x64, (byte)0xf6, (byte)0x74, (byte)0xa1, (byte)0xf3, (byte)0x04, (byte)0x92, (byte)0x9a,
    (byte)0xde, (byte)0x4f, (byte)0x68, (byte)0x93, (byte)0xef, (byte)0x2d, (byte)0xf6, (byte)0xe7,
    (byte)0x11, (byte)0xa8, (byte)0xc7, (byte)0x7a, (byte)0x0d, (byte)0x91, (byte)0xc9, (byte)0xd9,
    (byte)0x80, (byte)0x82, (byte)0x2e, (byte)0x50, (byte)0xd1, (byte)0x29, (byte)0x22, (byte)0xaf,
    (byte)0xea, (byte)0x40, (byte)0xea, (byte)0x9f, (byte)0x0e, (byte)0x14, (byte)0xc0, (byte)0xf7,
    (byte)0x69, (byte)0x38, (byte)0xc5, (byte)0xf3, (byte)0x88, (byte)0x2f, (byte)0xc0, (byte)0x32,
    (byte)0x3d, (byte)0xd9, (byte)0xfe, (byte)0x55, (byte)0x15, (byte)0x5f, (byte)0x51, (byte)0xbb,
    (byte)0x59, (byte)0x21, (byte)0xc2, (byte)0x01, (byte)0x62, (byte)0x9f, (byte)0xd7, (byte)0x33,
    (byte)0x52, (byte)0xd5, (byte)0xe2, (byte)0xef, (byte)0xaa, (byte)0xbf, (byte)0x9b, (byte)0xa0,
    (byte)0x48, (byte)0xd7, (byte)0xb8, (byte)0x13, (byte)0xa2, (byte)0xb6, (byte)0x76, (byte)0x7f,
    (byte)0x6c, (byte)0x3c, (byte)0xcf, (byte)0x1e, (byte)0xb4, (byte)0xce, (byte)0x67, (byte)0x3d,
    (byte)0x03, (byte)0x7b, (byte)0x0d, (byte)0x2e, (byte)0xa3, (byte)0x0c, (byte)0x5f, (byte)0xff,
    (byte)0xeb, (byte)0x06, (byte)0xf8, (byte)0xd0, (byte)0x8a, (byte)0xdd, (byte)0xe4, (byte)0x09,
    (byte)0x57, (byte)0x1a, (byte)0x9c, (byte)0x68, (byte)0x9f, (byte)0xef, (byte)0x10, (byte)0x72,
    (byte)0x88, (byte)0x55, (byte)0xdd, (byte)0x8c, (byte)0xfb, (byte)0x9a, (byte)0x8b, (byte)0xef,
    (byte)0x5c, (byte)0x89, (byte)0x43, (byte)0xef, (byte)0x3b, (byte)0x5f, (byte)0xaa, (byte)0x15,
    (byte)0xdd, (byte)0xe6, (byte)0x98, (byte)0xbe, (byte)0xdd, (byte)0xf3, (byte)0x59, (byte)0x96,
    (byte)0x03, (byte)0xeb, (byte)0x3e, (byte)0x6f, (byte)0x61, (byte)0x37, (byte)0x2b, (byte)0xb6,
    (byte)0x28, (byte)0xf6, (byte)0x55, (byte)0x9f, (byte)0x59, (byte)0x9a, (byte)0x78, (byte)0xbf,
    (byte)0x50, (byte)0x06, (byte)0x87, (byte)0xaa, (byte)0x7f, (byte)0x49, (byte)0x76, (byte)0xc0,
    (byte)0x56, (byte)0x2d, (byte)0x41, (byte)0x29, (byte)0x56, (byte)0xf8, (byte)0x98, (byte)0x9e,
    (byte)0x18, (byte)0xa6, (byte)0x35, (byte)0x5b, (byte)0xd8, (byte)0x15, (byte)0x97, (byte)0x82,
    (byte)0x5e, (byte)0x0f, (byte)0xc8, (byte)0x75, (byte)0x34, (byte)0x3e, (byte)0xc7, (byte)0x82,
    (byte)0x11, (byte)0x76, (byte)0x25, (byte)0xcd, (byte)0xbf, (byte)0x98, (byte)0x44, (byte)0x7b
  };
  static final byte[] airportPubKeyExpBytes = { 0x01, 0x00, 0x01 };
  static final BigInteger airportPubKeyMod = new BigInteger(1, airportPubKeyModBytes);
  static final BigInteger airportPubKeyExp = new BigInteger(1, airportPubKeyExpBytes);

  private List<RAOPDevice> devices = new ArrayList<RAOPDevice>();
  private Cipher cipher;
  private byte[] encAESKey;
  private int volume;
  private FileInputStream input = null;
  private long currentPos = 0;
  private int bytesBuffered = 0;
  private Thread streamer;
  private boolean streaming = false;

  AirPlayer() {
    try {
      KeyGenerator keyGen = KeyGenerator.getInstance("AES");
      keyGen.init(128);
      SecretKey aesKey = keyGen.generateKey();
      cipher = Cipher.getInstance("AES/CBC/NoPadding");
      cipher.init(Cipher.ENCRYPT_MODE, aesKey);

      RSAPublicKeySpec rsaKeySpec = new RSAPublicKeySpec(airportPubKeyMod, airportPubKeyExp);
      Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
      rsaCipher.init(Cipher.ENCRYPT_MODE, KeyFactory.getInstance("RSA").generatePublic(rsaKeySpec));
      encAESKey = rsaCipher.doFinal(aesKey.getEncoded());
    }
    catch (NoSuchAlgorithmException e) { System.out.println(e); /* won't happen */ }
    catch (NoSuchPaddingException e) { System.out.println(e); /* won't happen */ }
    catch (BadPaddingException e) { System.out.println(e); /* won't happen */ }
    catch (InvalidKeySpecException e) { System.out.println(e); /* won't happen */ }
    catch (InvalidKeyException e) { System.out.println(e); /* won't happen */ }
    catch (IllegalBlockSizeException e) { System.out.println(e); /* won't happen */ }
  }

  static public List<RAOPDevice> findDevices() {
    List<MDNSRecord> services = MDNSSD.findServices(standardTypes);
    List<RAOPDevice> devices = new ArrayList<RAOPDevice>();

    for (MDNSRecord s : services) {
      String[] parts = s.name.split("\\.", 3);
      for (InetAddress a : s.addresses) {
        try {
          RAOPDevice d = new RAOPDevice(a, parts[0], parts[1]);
          devices.add(d);
          break;
        }
        catch (Exception e) { 
          Log.e("peeler", "RAOP Device error: " + e);
          continue;
        }
      }
    }

    return devices;
  }

  public void setActiveDevices(List<RAOPDevice> devices) {
    if (devices == null) return;

    this.devices = devices;
    for (RAOPDevice d : devices)
      d.setup(encAESKey, cipher.getIV());
  }

  /* add a new device to the list, potentially while streaming */
  public void addActiveDevice(RAOPDevice device) {
    try {
      device.setup(encAESKey, cipher.getIV());
      device.setVolume(volume);
    }
    catch (IOException e) { return; }
    catch (RTSPException e) { return; }

    if (streamer.isAlive()) {
      RAOPDevice lastDevice;
      for (RAOPDevice d : devices) {
        d.queue.lock();
        lastDevice = d;
      }

      for (ByteBuffer b : devices.get(devices.size() - 1).queue)
        device.queue.offer(b);
      device.queue.lock();
      devices.add(device);

      for (RAOPDevice d : devices)
        d.queue.unlock();
    } else
      devices.add(device);
  }

  public void setVolume(int volume) {
    this.volume = volume;
    for (RAOPDevice d : devices) {
      try { d.setVolume(volume); }
      catch (IOException e) { }
      catch (RTSPException e) { }
    }
  }

  public void setAudioInput(String path) throws FileNotFoundException {
    input = new FileInputStream(path);
    currentPos = 0;
  }

  public void start() throws NoAudioInput {
    if (input == null) throw new NoAudioInput();

    startStreaming();

    for (RAOPDevice d : devices) {
      try { d.start(); }
      catch (IOException e) { /* should remove from the list of active devices */ }
      catch (RTSPException e) { /* should remove from the list of active devices */ }
    }
  }

  public void start(String path) throws FileNotFoundException {
    setAudioInput(path);
    try { start(); } catch (NoAudioInput e) { /* won't happen */ }
  }
  
  private void startStreaming() {
    streamer = new Thread(new Runnable() {
      public void run() {
        streaming = true;

        while (streaming) {
          try {
            int totalBytes = 0;
            byte[] data = new byte[bytesPerPacket];

            while (totalBytes < bytesPerPacket) {
              int readBytes = input.read(data, totalBytes, bytesPerPacket - totalBytes);
              if (readBytes == -1) break;
              totalBytes += readBytes;
            }

            if (bytesBuffered == deviceBufferSize)
              currentPos += totalBytes;
            else {
              bytesBuffered += totalBytes;
              if (bytesBuffered > deviceBufferSize) bytesBuffered = deviceBufferSize;
            }

            data = ALAC.encode(data);

            try { cipher.doFinal(data, 0, data.length / 16 * 16, data); }
            catch (ShortBufferException e) { System.out.println(e); /* won't happen */ }
            catch (IllegalBlockSizeException e) { System.out.println(e); /* won't happen */ }
            catch (BadPaddingException e) { System.out.println(e); /* won't happen */ }

            ByteBuffer buf = ByteBuffer.wrap(data);
            for (RAOPDevice d : devices)
              d.queue.put(buf);
          } catch (Exception e) { streaming = false; }
        }
      }
    });
    streamer.start();
  }

  public int currentPosition() {
    return (int)(currentPos / bytesPerMilliSec);
  }

  public void play() {
    for (RAOPDevice d : devices)
      d.play();
  }
  
  private void stopStreaming() {
    streamer.interrupt();
    try { streamer.join(); } catch (InterruptedException e) { }
  }

  public void pause() {
    stopStreaming();

    for (RAOPDevice d : devices) {
      try { d.pause(); }
      catch (IOException e) { /* should remove from the list of active devices */ }
      catch (RTSPException e) { /* should remove from the list of active devices */ }
    }

    currentPos -= bytesBuffered;
    try { input.getChannel().position(currentPos); }
    catch (IOException e) {
      /* couldn't rewind. too bad. just accept the loss of <bytesBuffered> worth of audio and continue */
      Log.e("peeler", "Couldn't rewind audio stream on pause. " + (bytesBuffered >> 2) + " samples lost");
      currentPos += bytesBuffered;
    }
    bytesBuffered = 0;

    streamer.start(); // startStreaming();
  }

  public boolean seek(int newPos) throws IOException {
    stopStreaming();

    long seekTo = newPos * bytesPerMilliSec;
    try {
      input.getChannel().position(seekTo);

      for (RAOPDevice d : devices)
        d.pause();
    } catch (IOException e) {
      /* couldn't seek. too bad really... */
      Log.e("peeler", "Couldn't seek in audio stream");
      return false;
    } catch (RTSPException e) {
      Log.e("peeler", "Couldn't seek in audio stream");
      return false;
    } finally { streamer.start(); }

    for (RAOPDevice d : devices)
      d.play();
    
    return true;
  }

  public void stop() {
    /* XXX implement */
  }

  public void close() {
    stopStreaming();
    for (RAOPDevice d : devices)
      d.close();
    input = null;
  }
}

class NoAudioInput extends Exception {
  NoAudioInput() {
    super("The audio input has not been set");
  }
}

class ALAC {
  static private byte[] alacHeader = {
    0x20, // bits 5-7: num of channels (0 mono, 1 stereo); rest: unknown
    0x00, // unknown
    0x02  // bit 1: 'not compressed' flag; bit 4: 'size included' flag; rest unknown
  };

  static public byte[] encode(byte[] data) {
    int padLen = (4 - (data.length % 4)) % 4;
    byte[] alacData = new byte[3 + data.length + padLen];
    System.arraycopy(alacHeader, 0, alacData, 0, 3);

    // byte-swap raw samples (little-endian to big-endian) and shift everything left by one bit
    for (int i = 0; i < data.length; i += 2) {
      byte highByte = (i + 1 < data.length ? data[i + 1] : 0);
      alacData[i + 2] |= (highByte & 0xff) >> 7;
      alacData[i + 3] = (byte)((highByte & 0xff) << 1);
      alacData[i + 3] |= (data[i] & 0xff) >> 7;
      alacData[i + 4] = (byte)((data[i] & 0xff) << 1);
    }

    return alacData;
  }
}




Java Source Code List

com.example.peeler.AirPlayer.java
com.example.peeler.AlbumListView.java
com.example.peeler.ArtistListView.java
com.example.peeler.Common.java
com.example.peeler.CurrentSongView.java
com.example.peeler.DeviceView.java
com.example.peeler.GenreListView.java
com.example.peeler.MDNS-SD.java
com.example.peeler.Peeler.java
com.example.peeler.PlaylistListView.java
com.example.peeler.RAOPDevice.java
com.example.peeler.RTSP.java
com.example.peeler.SongListView.java
com.example.peeler.Streamer.java