Skip to content

Latest commit

 

History

History
423 lines (374 loc) · 11.2 KB

08-IcyMetaClient.md

File metadata and controls

423 lines (374 loc) · 11.2 KB

#Ti.IcyMetaClient

The sound file format embeds ID3Tags to deliver meta details about the trace. These data are in the tail of file. In field of live streaming is no end. Therefore the meta data will send on start of streaming and during streaming.

If we start the streaming we send a special http header and then the server sends these meta data between the mp3/aac data in fixe distances. More you can find here

Now we have two ways t read this meta:

  1. usage of special streaming client like Trevor's one. It realize both: the sound data and meta data
  2. or we debundle the problem and uses a special client only for meta.

An inspiration for us is this snippet The code improvements:

  1. the client should run as async task
  2. the regex to parse the answer is bad (sometimes the value contains ';')
package de.appwerft.icymetaclient;

@Kroll.proxy(creatableInModule = IcymetaclientModule.class)
public class IcyMetaClientProxy extends KrollProxy implements OnLifecycleEvent {
	private static final String LCAT = "ICYMETA";
	private URL url = null;
	private int pullInterval = 0; // sec
	private boolean autoStart = false;
	private String charset = "UTF-8";
	boolean isForeGround = false;

	IcyStreamMeta metaClient = null;
	KrollFunction loadCallback = null;
	KrollFunction errorCallback = null;

In constructor of KrollProxy we create an instance of the IcyStreamMeta class:

	public IcyMetaClientProxy() {
		super();
		metaClient = new IcyStreamMeta();
	}

This is the try to add this 'OnLifecycleEventListener', but it doesn't work ;-(

    @Override
    public void initActivity(Activity activity) {
        super.initActivity(activity);
        ((TiBaseActivity) getActivity()).addOnLifecycleEventListener(this);

    }

Here we import all options from javascript layer:

	// Handle creation options
	@Override
	public void handleCreationDict(KrollDict options) {
		Log.d(LCAT, "Start handleCreationDict");
		super.handleCreationDict(options);

It is a good coding style to validate the URL parameter:

		if (options.containsKey(TiC.PROPERTY_URL)) {
			try {
				url = new URL(options.getString(TiC.PROPERTY_URL));
				metaClient.setStreamUrl(url);

			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (options.containsKey("pullInterval")) {
			pullInterval = options.getInt("pullInterval");
		}
		if (options.containsKey("charset")) {
			charset = options.getString("charset");
		}
		if (options.containsKey("autoStart")) {
			this.autoStart = options.getBoolean("autoStart");
			Log.d(LCAT, "autoStart=" + this.autoStart);
			if (this.autoStart)
				metaClient.startTimer();
		}

For async callback communication we use two endpoints:

		if (options.containsKey(TiC.PROPERTY_ONLOAD)) {
			Object cb = options.get(TiC.PROPERTY_ONLOAD);
			if (cb instanceof KrollFunction) {
				loadCallback = (KrollFunction) cb;
			} else {
				Log.e(LCAT, "onload is not KrollFunction");
			}
			if (autoStart == true) {
				try {
					metaClient.refreshMeta();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		if (options.containsKey(TiC.PROPERTY_ONERROR)) {
			Object cb = options.get(TiC.PROPERTY_ONERROR);
			if (cb instanceof KrollFunction) {
				errorCallback = (KrollFunction) cb;
			} else {
				Log.e(LCAT, "onerroris not KrollFunction");
			}
		}
	}

Here we expose a set of methods to javascript:

	@Kroll.method
	public String getStreamURL() {
		return this.url.toString();
	}

	@Kroll.method
	public void start() {
		metaClient.startTimer();
	}

	@Kroll.method
	public void stop() {
		metaClient.stopTimer();
	}

	
	@Kroll.method
	public void setStreamUrl(String url) throws IOException {
		metaClient.setStreamUrl(new URL(url));
	}

	@Kroll.method
	public boolean isError() {
		return metaClient.isError();
	}

	@Kroll.method
	public void refreshMeta() throws IOException {
		metaClient.refreshMeta();
	}

These methods currently never fired. Target was to suppress net activities during sleeping.

	@Override
	public void onResume(Activity activity) {
		super.onResume(activity);
		Log.d(LCAT, "onResume >>>>>>>>>>");
		isForeGround = true;
	}

	@Override
	public void onPause(Activity activity) {
		isForeGround = false;
		Log.d(LCAT, "onPause <<<<<<<<<<<<<");
		super.onPause(activity);
	}

	public void onStart(Activity activity) {
		super.onStart(activity);
		Log.d(LCAT, "onStart >>>>>>>>>>");
		isForeGround = true;
	}

Here the kernel - our logic:

	private class IcyStreamMeta {
		private URL streamUrl;
		private Map<String, String> metadata;
		private boolean isError;
		private boolean isRunning;
		private Timer timer;
		private int oldHash = 0;

		public IcyStreamMeta() {
			// setStreamUrl(streamUrl);
			isError = false;
			timer = new Timer();
		}

The equivalent to javascript's setInterval() is these construct:

		public void startTimer() {
			retreiveMetadata();
			if (pullInterval != 0) {
				timer.scheduleAtFixedRate(new TimerTask() {
					@Override
					public void run() {
						retreiveMetadata();
					}
				}, 0, pullInterval * 1000);
				isRunning = true;
			}
		}

And so we can stop the game:

		public void stopTimer() {
			timer.cancel();
			isRunning = false;
		}

		@SuppressWarnings("unused")
		public URL getStreamUrl() {
			return streamUrl;
		}

		
		public void refreshMeta() throws IOException {
			retreiveMetadata();
		}

		private Map<String, String> getMetadata() throws IOException {
			if (metadata == null) {
				refreshMeta();
			}
			return metadata;
		}

		public boolean isError() {
			return isError;
		}

		public void setStreamUrl(URL streamUrl) {
			this.metadata = null;
			this.streamUrl = streamUrl;
			this.isError = false;
		}

		private void sendError(String msg) {
			KrollDict resultDict = new KrollDict();
			resultDict.put("error", msg);
			if (errorCallback != null)
				errorCallback.call(getKrollObject(), resultDict);
			else
				Log.e(LCAT, "errorCallback is null");

		}

The server give us a binary stream, we have to convert to string. The real data comes after metaDataOffset, therefore we have to jump into right position:

		private String Stream2String(InputStream stream, int metaDataOffset) {

			// http://www.smackfu.com/stuff/programming/shoutcast.html

			final int BLOCKSIZE = 16;
			final int EOSTREAM = -1;
			if (stream == null)
				return null;
			int b; // 0...255
			int count = 0;
			int metaDataLength = BLOCKSIZE * 255; // 4080 is the max length (16

			byte[] bytesOfMetaData = new byte[metaDataLength + 1];
			boolean inData = false;
			try {
				int bytecount = 0;
				while ((b = stream.read()) != EOSTREAM) {
					count++;
					// detector.handleData(stream, 0, b);
					if (count == metaDataOffset + 1) {
						metaDataLength = b * BLOCKSIZE;
					}
					if (count > metaDataOffset + 1
							&& count < (metaDataOffset + metaDataLength)) {
						inData = true;
					} else {
						inData = false;
					}
					if (inData) {
						bytesOfMetaData[bytecount++] = (byte) b;
					}
					if (count > (metaDataOffset + metaDataLength)) {
						break;
					}
				}
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			String detectedCharset = guessEncoding(bytesOfMetaData);
			String result = new String(bytesOfMetaData,
					Charset.forName(charset));
			int stringLength = result.lastIndexOf(";");
			try {
				stream.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			if (stringLength != -1)
				return result.substring(0, stringLength).replaceAll("\r", "");
			else
				return null;
		}

The protocol doesn't contain data about charset but it proposes UTF-8. A lot of streaming sender (Ö1, WDR) uses Latin-1. It wbecomes a problem if the text contains umlaute. The method 'guessEncoding' never detects a charset, don't no why.

		public String guessEncoding(byte[] bytes) {
			String DEFAULT_ENCODING = "UTF-8";
			org.mozilla.universalchardet.UniversalDetector detector = new org.mozilla.universalchardet.UniversalDetector(
					null);
			detector.handleData(bytes, 0, bytes.length);
			detector.dataEnd();
			String encoding = detector.getDetectedCharset();
			detector.reset();
			if (encoding == null) {
				encoding = DEFAULT_ENCODING;
			}
			return encoding;
		}

This is the part that speals with server. We start it as asnc task:

		private void retreiveMetadata() {
			// Log.d(LCAT, "isForeGround=" + isForeGround);
			AsyncTask<Void, Void, Void> doRequest = new AsyncTask<Void, Void, Void>() {
				protected Void doInBackground(Void[] dummy) {
					// http://www.javased.com/?api=java.net.URLConnection
					URLConnection con = null;

In this KrollDict we collect all stuff:

					KrollDict resultDict = new KrollDict();
					try {
						con = streamUrl.openConnection();
					} catch (IOException e) {
						sendError(e.getMessage());
						return null;
					}

Very important: this tells the server, we need more then audio:

					
					con.setRequestProperty("Icy-MetaData", "1");

But only the data, no audio

					
					con.setRequestProperty("Connection", "close");
					try {
						con.connect();
					} catch (IOException e) {
						sendError(e.getMessage());
						return null;
					}

Now we analye some headers:

					int metaDataOffset = 0;
					Map<String, List<String>> headers = con.getHeaderFields();
					if (headers.containsKey("icy-name")) {
						resultDict.put("icy-name",
								headers.get("icy-name").get(0));
					}
					InputStream stream = null;
					try {
						stream = con.getInputStream();
					} catch (IOException e) {
						sendError(e.getMessage());
						return null;
					}
					if (headers.containsKey("icy-metaint")) {
						metaDataOffset = Integer.parseInt(headers.get(
								"icy-metaint").get(0));
					}

					if (metaDataOffset == 0 || stream == null) {
						isError = true;
						Log.e(LCAT, "metaDataOffset=0");
						return null;
					}

and now we parse the body:

					String metaString = Stream2String(stream, metaDataOffset);
					if (metaString == null) {
						return null;
					}
					String[] metaParts = metaString.split("\';");
					for (int i = 0; i < metaParts.length; i++) {
						String line = metaParts[i];

						String[] keyval = line.split("=");
						if (keyval != null && keyval.length == 2) {
							String key = keyval[0];
							String val = keyval[1];
							String sanitizedValue = "";
							sanitizedValue = val.substring(1, val.length());
							resultDict.put(key, sanitizedValue);

						} else {
							Log.e(LCAT, "cannot split meta with =");

						}
					}

We remember the old state in a hash, because we want only send back to jaavscript layer after change of meta data

					if (resultDict.hashCode() != oldHash) {
						if (loadCallback != null)
							loadCallback.call(getKrollObject(), resultDict);
						else
							Log.e(LCAT, "loadCallback is null");
						oldHash = resultDict.hashCode();
					}
					return null;
				} // do in background
			};// async task
			doRequest.execute();
		} // retreiveMetadata
	}// private class

} // main class