IOS Airplay -- Implementation of Airtunes Music Play on Android Box and Mobile Phone (Part 1)

Posted by Adarmo on Mon, 17 Jun 2019 21:56:34 +0200

I. Preface

There are two main ways to play streaming media in LAN, Airplay and DLNA. For iOS systems, Airplay is a natural product, but unfortunately Apple has a consistent style, and Airplay is a closed source protocol. Fortunately, God reversed the content of the agreement, so that we can build services to complete this work. The complexity of this agreement is still slightly higher, which makes it impossible for me to write a complete article in one breath. Let's talk about it one by one.

2. Let iOS discover Android devices via AirTurns

Airplay is a LAN-based service. Under the same wifi intranet, Apple devices will search for devices that support Airplay services.
We can send a MultiCast broadcast to the LAN through the mDNS service, so that iOS devices can find you (Android devices) in the Intranet. Android can use the JmDNS library to build related code.

- Code implementation

  • 1. Get the local network device interface that meets the requirements. Remove devices that are not running, filter out feedbacks, peer-to-peer and virtual interfaces, and remove device interfaces that do not support MultiCast.
for (final NetworkInterface networkInterface : workInterfaces) {
            //If the network device interface is a loopback interface-point-to-point interface-no running-virtual port, then the execution is skipped
            if (networkInterface.isLoopback() || networkInterface.isPointToPoint() || !networkInterface.isUp()
                    || networkInterface.isVirtual()) {
                continue;
            }
            // Multicast skip is not supported
            if (!networkInterface.supportsMulticast()) {
                continue;
            }
  • 2. For the equipment that meets the requirements, choose the ports of ipv4 and ipv6.
for (final InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
                        //Ports are ipv4 or ipv6 ports
                        if (address instanceof Inet4Address || address instanceof Inet6Address) {
                            try {
                                final JmDNS jmDNS = JmDNS.create(address, hostName);
                                jmDNSList.add(jmDNS);
  • 3. Open the AirTunes/RAOP (Remote Audio Transfer Protocol) service for the port number that meets the requirements.

Protocol Constant Definition

/**
     * 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 = NetworkUtils.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");

    /**
     * The AirTunes/RAOP RTSP port
     */
    private int rtspPort = 5000; //default value

Logic code

final ServiceInfo airTunesServiceInfo = ServiceInfo.create(
                                        AIR_TUNES_SERVICE_TYPE,
                                        hardwareAddressString + "@" + hostName,
                                        getRstpPort(),
                                        0,
                                        0,
                                        AIRTUNES_SERVICE_PROPERTIES
                                );
                                jmDNS.registerService(airTunesServiceInfo);
  • 4 The complete workthread code is as follows
package ss.serven.rduwan.airtunesandroid;

import android.util.Log;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;

/**
 * Created by rduwan on 17/6/29.
 */

public class AirTunesRunnable implements Runnable {

    /**
     * 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 = NetworkUtils.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");

    /**
     * The AirTunes/RAOP RTSP port
     */
    private int rtspPort = 5000; //default value

    protected List<JmDNS> jmDNSList;

    private static AirTunesRunnable instance = null;

    private final static String TAG = "AirTunesRunnable";

    private AirTunesRunnable() {
        jmDNSList = new java.util.LinkedList<JmDNS>();
    }

    public synchronized static AirTunesRunnable getInstance() {
        if (instance == null) {
            instance = new AirTunesRunnable();
        }
        return instance;
    }


    @Override
    public void run() {
        startAirTunesService();
    }

    private void startAirTunesService() {
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                onAppShutDown();
            }
        }));
        sendMulitCastToiOS();
    }

    /**
     * Send multicast to iOS in LAN through DNS service, so that iOS devices can find you
     * java Using the JmDNS Library
     */
    private void sendMulitCastToiOS() {
        //get Network details
        NetworkUtils networkUtils = NetworkUtils.getInstance();
        String hostName = "Rduwan-AirTunes";
        networkUtils.setHostName(hostName);
        String hardwareAddressString = networkUtils.getHardwareAddressString();
        try {
            synchronized (jmDNSList) {
                List<NetworkInterface> workInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
                for (final NetworkInterface networkInterface : workInterfaces) {
                    //If several ports of a network device are backup interfaces-point-to-point interfaces-no running-virtual ports, then skip execution
                    if (networkInterface.isLoopback() || networkInterface.isPointToPoint() || !networkInterface.isUp()
                            || networkInterface.isVirtual()) {
                        continue;
                    }
                    // Multicast skip is not supported
                    if (!networkInterface.supportsMulticast()) {
                        continue;
                    }

                    for (final InetAddress address : Collections.list(networkInterface.getInetAddresses())) {
                        //Ports are ipv4 or ipv6 ports
                        if (address instanceof Inet4Address || address instanceof Inet6Address) {
                            try {
                                final JmDNS jmDNS = JmDNS.create(address, hostName);
                                jmDNSList.add(jmDNS);

                                //Building AirTunes/RAOP (Remote Audio Transfer Protocol) Service
                                final ServiceInfo airTunesServiceInfo = ServiceInfo.create(
                                        AIR_TUNES_SERVICE_TYPE,
                                        hardwareAddressString + "@" + hostName,
                                        getRstpPort(),
                                        0,
                                        0,
                                        AIRTUNES_SERVICE_PROPERTIES
                                );
                                jmDNS.registerService(airTunesServiceInfo);
                                Log.d(TAG, "Success to publish service on " + address + ", port: " + getRstpPort());
                            } catch (final Throwable e) {
                                Log.e(TAG, "Failed to publish service on " + address, e);

                            }

                        }
                    }
                }
            }
        } catch (Exception e) {

        }
    }

    public int getRstpPort() {
        return rtspPort;
    }

    private void onAppShutDown() {
        /* Stop all mDNS responders */
        synchronized(jmDNSList) {
            for(final JmDNS jmDNS: jmDNSList) {
                try {
                    jmDNS.unregisterAllServices();
                    Log.i(TAG, "Unregistered all services ");
                }
                catch (final Exception e) {
                    Log.i(TAG, "Failed to unregister some services");

                }
            }
        }
    }
}
  • Define a Service and start the thread. Run this program on an Android device. On iOS devices with the same wifi, you can see the IrTurns service called RDuwan-AirTunes.
    The following figure


  • Part 1 Engineering code see Github link

Topics: Java iOS Android network