Commit 8890520c authored by Steve Johnson's avatar Steve Johnson

works - still issues with starting playback

parent abfad7b0
/*
* Copyright 2013 Sebastian Mauer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.maui.cheapcast;
import android.content.Context;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.UUID;
public class Installation {
private static String sID = null;
private static final String INSTALLATION = "INSTALLATION";
public synchronized static String id(Context context) {
if (sID == null) {
File installation = new File(context.getFilesDir(), INSTALLATION);
try {
if (!installation.exists())
writeInstallationFile(installation);
sID = readInstallationFile(installation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return sID;
}
private static String readInstallationFile(File installation) throws IOException {
RandomAccessFile f = new RandomAccessFile(installation, "r");
byte[] bytes = new byte[(int) f.length()];
f.readFully(bytes);
f.close();
return new String(bytes);
}
private static void writeInstallationFile(File installation) throws IOException {
FileOutputStream out = new FileOutputStream(installation);
String id = UUID.randomUUID().toString();
out.write(id.getBytes());
out.close();
}
}
\ No newline at end of file
/*
* Copyright 2013 Sebastian Mauer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.maui.cheapcast;
import android.content.Context;
import android.content.res.AssetManager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
public class Utils {
public static NetworkInterface getActiveNetworkInterface() {
Enumeration<NetworkInterface> interfaces = null;
try {
interfaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) {
return null;
}
while (interfaces.hasMoreElements()) {
NetworkInterface iface = interfaces.nextElement();
Enumeration<InetAddress> inetAddresses = iface.getInetAddresses();
/* Check if we have a non-local address. If so, this is the active
* interface.
*
* This isn't a perfect heuristic: I have devices which this will
* still detect the wrong interface on, but it will handle the
* common cases of wifi-only and Ethernet-only.
*/
while (inetAddresses.hasMoreElements()) {
InetAddress addr = inetAddresses.nextElement();
if (!(addr.isLoopbackAddress() || addr.isLinkLocalAddress())) {
return iface;
}
}
}
return null;
}
public static InetAddress getLocalV4Address(NetworkInterface netif)
{
Enumeration addrs = netif.getInetAddresses();
while (addrs.hasMoreElements())
{
InetAddress addr = (InetAddress) addrs.nextElement();
if (addr instanceof Inet4Address && !addr.isLoopbackAddress())
return addr;
}
return null;
}
public static String readerToString(BufferedReader reader) {
StringBuffer rawBody = new StringBuffer();
String line = null;
try {
while ((line = reader.readLine()) != null)
rawBody.append(line);
} catch (Exception e) { /*report an error*/ }
return rawBody.toString();
}
public static String inputStreamToString(InputStream is) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
is.close();
return sb.toString();
}
public static String readAsset(Context context, String assetPath) throws IOException {
String asset = null;
AssetManager am = context.getAssets();
try {
InputStream is = am.open(assetPath);
int length = is.available();
byte[] data = new byte[length];
is.read(data);
is.close();
asset = new String(data, "ASCII");
} catch (IOException e1) {
e1.printStackTrace();
}
return asset;
}
public static final int byteArrayToInt(byte[] arr, int offset) {
if (arr == null || arr.length - offset < 4)
return -1;
int r0 = (arr[offset] & 0xFF) << 24;
int r1 = (arr[offset + 1] & 0xFF) << 16;
int r2 = (arr[offset + 2] & 0xFF) << 8;
int r3 = arr[offset + 3] & 0xFF;
return r0 + r1 + r2 + r3;
}
}
\ No newline at end of file
/*
* Copyright 2013 Sebastian Mauer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.maui.cheapcast.ssdp;
import android.content.Context;
import android.util.Log;
import com.cablelabs.android.dial.MSEConfig;
import at.maui.cheapcast.Installation;
import at.maui.cheapcast.Utils;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//public class SSDP extends Thread {
public class SSDP implements Runnable {
/**
* Default IPv4 multicast address for SSDP messages
*/
public static final String ADDRESS = "239.255.255.250";
public static final String IPV6_LINK_LOCAL_ADDRESS = "FF02::C";
public static final String IPV6_SUBNET_ADDRESS = "FF03::C";
public static final String IPV6_ADMINISTRATIVE_ADDRESS = "FF04::C";
public static final String IPV6_SITE_LOCAL_ADDRESS = "FF05::C";
public static final String IPV6_GLOBAL_ADDRESS = "FF0E::C";
public static final String ST = "ST";
public static final String LOCATION = "LOCATION";
public static final String NT = "NT";
public static final String NTS = "NTS";
/* Definitions of start line */
public static final String SL_NOTIFY = "NOTIFY * HTTP/1.1";
public static final String SL_MSEARCH = "M-SEARCH * HTTP/1.1";
public static final String SL_OK = "HTTP/1.1 200 OK";
/* Definitions of notification sub type */
public static final String NTS_ALIVE = "ssdp:alive";
public static final String NTS_BYEBYE = "ssdp:byebye";
public static final String NTS_UPDATE = "ssdp:update";
public static final String LOG_TAG = "SSDP";
private SocketAddress mMulticastGroupAddress = new InetSocketAddress("239.255.255.250", 1900);
private MulticastSocket mMulticastSocket;
private DatagramSocket mUnicastSocket;
private NetworkInterface mNetIf;
private Context mContext;
private boolean mRunning = false;
public SSDP(Context ctx) throws IOException {
mContext = ctx;
mNetIf = Utils.getActiveNetworkInterface();
}
@Override
//public synchronized void start() {
// mRunning = true;
// super.start();
//}
//@Override
public void run() {
try {
mMulticastSocket = new MulticastSocket(1900);
mMulticastSocket.setLoopbackMode(true);
mMulticastSocket.joinGroup(mMulticastGroupAddress, mNetIf);
mUnicastSocket = new DatagramSocket(null);
mUnicastSocket.setReuseAddress(true);
mUnicastSocket.bind(new InetSocketAddress(Utils.getLocalV4Address(mNetIf),1900));
} catch (IOException e) {
Log.e(LOG_TAG, "Setup SSDP failed.", e);
}
mRunning = true;
while(mRunning) {
DatagramPacket dp = null;
try {
dp = receive();
String startLine = parseStartLine(dp);
if(startLine.equals(SL_MSEARCH)) {
String st = parseHeaderValue(dp, ST);
if(st.contains("dial-multiscreen-org:service:dial:1")) {
String responsePayload = "HTTP/1.1 200 OK\r\n" +
"ST: urn:dial-multiscreen-org:service:dial:1\r\n"+
"HOST: 239.255.255.250:1900\r\n"+
"EXT:\r\n"+
"CACHE-CONTROL: max-age=1800\r\n"+
"LOCATION: http://"+Utils.getLocalV4Address(mNetIf).getHostAddress()+":"+ MSEConfig.PORT+MSEConfig.DD_XML+"\r\n" +
"CONFIGID.UPNP.ORG: 7339\r\n" +
"BOOTID.UPNP.ORG: 7339\r\n" +
"USN: uuid:"+ Installation.id(mContext)+"\r\n";
DatagramPacket response = new DatagramPacket(responsePayload.getBytes(), responsePayload.length(), new InetSocketAddress(dp.getAddress(),dp.getPort()));
mUnicastSocket.send(response);
Log.d(LOG_TAG, "Responding to "+ dp.getAddress().getHostAddress());
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "SSDP fail.", e);
}
}
Log.e(LOG_TAG, "SSDP shutdown.");
}
public synchronized void shutdown() {
mRunning = false;
}
private DatagramPacket receive() throws IOException {
byte[] buf = new byte[1024];
DatagramPacket dp = new DatagramPacket(buf, buf.length);
mMulticastSocket.receive(dp);
return dp;
}
private String parseHeaderValue(String content, String headerName) {
Scanner s = new Scanner(content);
s.nextLine(); // Skip the start line
while (s.hasNextLine()) {
String line = s.nextLine();
int index = line.indexOf(':');
String header = line.substring(0, index);
if (headerName.equalsIgnoreCase(header.trim())) {
return line.substring(index + 1).trim();
}
}
return null;
}
private String parseHeaderValue(DatagramPacket dp, String headerName) {
return parseHeaderValue(new String(dp.getData()), headerName);
}
private String parseStartLine(String content) {
Scanner s = new Scanner(content);
return s.nextLine();
}
private String parseStartLine(DatagramPacket dp) {
return parseStartLine(new String(dp.getData()));
}
}
\ No newline at end of file
/*
* Copyright (C) 2013 ENTERTAILION, LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.cablelabs.android.dial;
import java.net.InetAddress;
/*
HTTP/1.1 200 OK
USN: uuid:d17c2986-4624-3f2c-93a5-fe3ef0a0ec9c::urn:dial-multiscreen-org:service:dial:1
LOCATION: http://192.168.0.51:47944/dd.xml
BOOTID.UPNP.ORG: 1287126024
ST: urn:dial-multiscreen-org:service:dial:1
CACHE-CONTROL: max-age=1800
EXT
*/
public final class BroadcastAdvertisement {
private final String location;
private final InetAddress ipAddress;
private final int port;
BroadcastAdvertisement(String location, InetAddress ipAddress, int port) {
this.location = location;
this.ipAddress = ipAddress;
this.port = port;
}
public String getLocation() {
return location;
}
public InetAddress getIpAddress() {
return ipAddress;
}
public int getPort() {
return port;
}
}
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cablelabs.android.dial;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* DIAL protocol client:
* http://www.dial-multiscreen.org/dial-protocol-specification
*/
public class BroadcastDiscoveryClient implements Runnable {
private static final String LOG_TAG = "BroadcastDiscoveryClient";
/**
* UDP port to send probe messages to.
*/
private static final int BROADCAST_SERVER_PORT = 1900;
/**
* Frequency of probe messages.
*/
private static final int PROBE_INTERVAL_MS = 6000;
private static final int PROBE_INTERVAL_MS_MAX = 60000;
private static final String SEARCH_TARGET = "urn:dial-multiscreen-org:service:dial:1";
private static final String M_SEARCH = "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 10\r\n" + "ST: "
+ SEARCH_TARGET + "\r\n\r\n";
private static final String HEADER_LOCATION = "LOCATION";
private static final String HEADER_ST = "ST";
private final InetAddress mBroadcastAddress;
private final Thread mBroadcastThread;
private int mBroadcastInterval;
private boolean mBroadcasting = true;
private boolean mReceiving = true;
/**
* Handle to main thread.
*/
private final Handler mHandler;
/**
* Send/receive socket.
*/
private final DatagramSocket mSocket;
/**
* Constructor
*
* @param broadcastAddress
* destination address for probes
* @param handler
* update Handler in main thread
*/
public BroadcastDiscoveryClient(InetAddress broadcastAddress, Handler handler) {
mReceiving = true;
mBroadcastAddress = broadcastAddress;
mHandler = handler;
try {
mSocket = new DatagramSocket(); // binds to random port
mSocket.setBroadcast(true);
} catch (SocketException e) {
Log.e(LOG_TAG, "Could not create broadcast client socket.", e);
throw new RuntimeException();
}
mBroadcastInterval = PROBE_INTERVAL_MS;
mBroadcastThread = new Thread(new Runnable() {
@Override
public void run() {
while (mBroadcasting) {
try {
BroadcastDiscoveryClient.this.sendProbe();
try {
Thread.sleep(mBroadcastInterval);
}
catch (InterruptedException e) {
}
/*
mBroadcastInterval = mBroadcastInterval * 2;
if (mBroadcastInterval > PROBE_INTERVAL_MS_MAX) {
mBroadcastInterval = PROBE_INTERVAL_MS_MAX;
mBroadcasting = false;
mReceiving = false;
}
*/
} catch (Throwable e) {
Log.e(LOG_TAG, "run", e);
}
}
}
});
Log.i(LOG_TAG, "Starting client on address " + mBroadcastAddress);
}
/** {@inheritDoc} */
public void run() {
Log.i(LOG_TAG, "Broadcast client thread starting.");
byte[] buffer = new byte[4096];
mBroadcastThread.start();
while (mReceiving) {
try {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
mSocket.receive(packet);
handleResponsePacket(packet);
} catch (InterruptedIOException e) {
// timeout
} catch (IOException e) {
// SocketException - stop() was called
break;
} catch (IllegalArgumentException e) {
break;
}
}
Log.i(LOG_TAG, "Exiting client loop.");
mBroadcasting = false;
mBroadcastThread.interrupt();
}
/**
* Sends a single broadcast discovery request.
*/
private void sendProbe() {
try {
DatagramPacket packet = makeRequestPacket(mSocket.getLocalPort());
mSocket.send(packet);
} catch (Throwable e) {
Log.e(LOG_TAG, "Exception sending broadcast probe", e);
return;
}
}
/**
* Immediately stops the receiver thread, and cancels the probe timer.
*/
public void stop() {
if (mSocket != null) {
mSocket.close();
}
}
/**
* Constructs a new probe packet.
*
* @param responsePort
* the udp port number for replies
* @return a new DatagramPacket
*/
private DatagramPacket makeRequestPacket(int responsePort) {
String message = M_SEARCH;
byte[] buf = message.getBytes();
DatagramPacket packet = new DatagramPacket(buf, buf.length, mBroadcastAddress, BROADCAST_SERVER_PORT);
return packet;
}
/**
* Parse a received packet, and notify the main thread if valid.
*
* @param packet
* The locally-received DatagramPacket
*/
private void handleResponsePacket(DatagramPacket packet) {
try {
String strPacket = new String(packet.getData(), 0, packet.getLength());
//Log.d(LOG_TAG, "response=" + strPacket);
String tokens[] = strPacket.trim().split("\\n");
String location = null;
boolean foundSt = false;
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i].trim();
if (token.startsWith(HEADER_LOCATION)) {
// LOCATION: http://192.168.0.51:47944/dd.xml
location = token.substring(10).trim();
} else if (token.startsWith(HEADER_ST)) {
// ST: urn:dial-multiscreen-org:service:dial:1
String st = token.substring(4).trim();
if (st.equals(SEARCH_TARGET)) {
foundSt = true;
}
}
}
if (!foundSt || location == null) {
Log.w(LOG_TAG, "Malformed response: " + strPacket);
return;
}
BroadcastAdvertisement advert;
try {
Uri uri = Uri.parse(location);
InetAddress address = InetAddress.getByName(uri.getHost());
advert = new BroadcastAdvertisement(location, address, uri.getPort());
} catch (Exception e) {
return;
}
Message message = mHandler.obtainMessage(DialDiscovery.BROADCAST_RESPONSE, advert);
mHandler.sendMessage(message);
} catch (Exception e) {
Log.e(LOG_TAG, "handleResponsePacket", e);
}