<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>IONCANNON</title>
	<atom:link href="http://www.ioncannon.net/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.ioncannon.net</link>
	<description>Thoughts on Software Development and Engineering</description>
	<lastBuildDate>Mon, 21 Jan 2013 13:33:49 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	
	<atom:link rel='hub' href='http://www.ioncannon.net/?pushpress=hub'/>
		<item>
		<title>Bluetooth 4.0 LE on Raspberry Pi with Bluez 5.x</title>
		<link>http://www.ioncannon.net/linux/1570/bluetooth-4-0-le-on-raspberry-pi-with-bluez-5-x/</link>
		<comments>http://www.ioncannon.net/linux/1570/bluetooth-4-0-le-on-raspberry-pi-with-bluez-5-x/#comments</comments>
		<pubDate>Mon, 21 Jan 2013 13:33:49 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[hardware]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[programming]]></category>
		<category><![CDATA[bluethooth]]></category>
		<category><![CDATA[rpi]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1570</guid>
		<description><![CDATA[Over the holiday I had a little time to fiddle with the Raspberry Pi I got earlier in the summer and I started wondering how hard it would be to get a Bluetooth LE adapter working. It turned out not to be as hard to get working as I thought it might be thanks to [...]]]></description>
				<content:encoded><![CDATA[<p>Over the holiday I had a little time to fiddle with the Raspberry Pi I got earlier in the summer and I started wondering how hard it would be to get a <a href="http://en.wikipedia.org/wiki/Bluetooth_low_energy">Bluetooth LE</a> adapter working. It turned out not to be as hard to get working as I thought it might be thanks to recently added support in the Bluez 5.x Bluetooth stack. What follows is the information you need to get things going.</p>
<p>To start with I picked the <a href="http://www.iogear.com/product/GBU521/">IOGEAR Bluetooth 4.0 USB Micro Adapter (GBU521)</a> that can be found on <a href="http://www.amazon.com/IOGEAR-Bluetooth-Micro-Adapter-GBU521/dp/B007GFX0PY/">Amazon for just $13</a> since it looked like the chip it uses is decently supported with recent Linux kernels. The only issue I had is the size itself, if it didn&#039;t have a little nub on the end it would be too small to pull back out of the USB plug.</p>
<p><span id="more-1570"></span></p>
<p>The GBU521 seems to be recognized by older kernels but to get LE support go with the latest kernel you can find. For the RPi it is easy enough to get a very recently kernel using <a href="https://github.com/Hexxeh/rpi-update">rpi-update</a>. Currently it is 3.6.11 and that worked well for me. I also used a 3.5 kernel on a laptop that worked fine but you can&#039;t go any older than that.</p>
<p>Once you have the correct kernel in place you need to grab the latest version of <a href="http://www.bluez.org/">Bluez</a>. Bluez is the official Bluetooth stack for Linux and the 5.x series has introduced Bluetooth LE support. It is still a work in progress but it does work. Grab the <a href="http://www.bluez.org/download/">5.1 version</a> or later and uncompress/untar it somewhere on your RPi. Please note that Bluez 5.x <b>requires a 3.5+ kernel</b> to work correctly, this seems to be a sticking point that a lot of people hit.</p>
<p>I am using the <a href="http://www.raspbian.org/">Raspbian</a> distro found on <a href="http://www.raspberrypi.org/downloads">Raspberry Pi downloads</a>. Outside of the normal install needed to compile I had to install the following packages:</p>
<div class="codesnip-container" >
<div class="bash codesnip" style="font-family:monospace;"><span class="kw2">apt-get</span> <span class="kw2">install</span> libusb-dev libdbus-1-dev libglib2.0-dev <span class="kw2">automake</span> libudev-dev libical-dev libreadline-dev</div>
</div>
<p>With those packages installed configure and make (note that I&#039;ve disabled systemd support, it didn&#039;t seem to work for me and I didn&#039;t need it myself):</p>
<div class="codesnip-container" >
<div class="bash codesnip" style="font-family:monospace;">.<span class="sy0">/</span>configure <span class="re5">&#8211;disable-systemd</span><br />
<span class="kw2">make</span><br />
<span class="kw2">make</span> <span class="kw2">install</span></div>
</div>
<p>Now you will have a number of binaries installed that will get you rolling. I&#039;m not going to go into a lot of detail here about what all of the following commands do but I want to touch on each of them briefly so you know they are there. Assuming you have the Bluetooth adapter plugged in you should be able to run the following command and get details about it:</p>
<div class="codesnip-container" >
<div class="bash codesnip" style="font-family:monospace;">hciconfig</p>
<p>hci0: &nbsp; Type: BR<span class="sy0">/</span>EDR &nbsp;Bus: USB<br />
&nbsp; &nbsp; &nbsp; &nbsp; BD Address: 00:00:<span class="nu0">12</span>:<span class="nu0">34</span>:<span class="nu0">56</span>:<span class="nu0">78</span> &nbsp;ACL MTU: <span class="nu0">1021</span>:<span class="nu0">8</span> &nbsp;SCO MTU: <span class="nu0">64</span>:<span class="nu0">1</span><br />
&nbsp; &nbsp; &nbsp; &nbsp; DOWN <br />
&nbsp; &nbsp; &nbsp; &nbsp; RX bytes:<span class="nu0">467</span> acl:<span class="nu0">0</span> sco:<span class="nu0">0</span> events:<span class="nu0">18</span> errors:<span class="nu0">0</span><br />
&nbsp; &nbsp; &nbsp; &nbsp; TX bytes:<span class="nu0">317</span> acl:<span class="nu0">0</span> sco:<span class="nu0">0</span> commands:<span class="nu0">18</span> errors:<span class="nu0">0</span></div>
</div>
<p>This shows that the device is in a down state. To bring it up you can issue the following command:</p>
<div class="codesnip-container" >
<div class="bash codesnip" style="font-family:monospace;">hciconfig hci0 up</p>
<p>hciconfig</p>
<p>hci0: &nbsp; Type: BR<span class="sy0">/</span>EDR &nbsp;Bus: USB<br />
&nbsp; &nbsp; &nbsp; &nbsp; BD Address: 00:00:<span class="nu0">12</span>:<span class="nu0">34</span>:<span class="nu0">56</span>:<span class="nu0">78</span> &nbsp;ACL MTU: <span class="nu0">1021</span>:<span class="nu0">8</span> &nbsp;SCO MTU: <span class="nu0">64</span>:<span class="nu0">1</span><br />
&nbsp; &nbsp; &nbsp; &nbsp; UP RUNNING <br />
&nbsp; &nbsp; &nbsp; &nbsp; RX bytes:<span class="nu0">467</span> acl:<span class="nu0">0</span> sco:<span class="nu0">0</span> events:<span class="nu0">18</span> errors:<span class="nu0">0</span><br />
&nbsp; &nbsp; &nbsp; &nbsp; TX bytes:<span class="nu0">317</span> acl:<span class="nu0">0</span> sco:<span class="nu0">0</span> commands:<span class="nu0">18</span> errors:<span class="nu0">0</span></div>
</div>
<p>Now if you know you have a Bluetooth LE peripheral sitting around advertising you can run the following command and you should see an address for that device show up:</p>
<div class="codesnip-container" >
<div class="bash codesnip" style="font-family:monospace;">hcitool lescan</p>
<p>LE Scan &#8230;<br />
12:88:FF:FF:11:99 <span class="kw2">touch</span><br />
<span class="nu0">12</span>:<span class="nu0">88</span>:FF:FF:<span class="nu0">11</span>:<span class="nu0">99</span> <span class="br0">&#40;</span>unknown<span class="br0">&#41;</span></div>
</div>
<p>If you get to the point of wanting to let the RPi act as a peripheral you will need to know about the following command that will enable advertising on the adapter:</p>
<div class="codesnip-container" >
<div class="bash codesnip" style="font-family:monospace;">hciconfig hci0 leadv <span class="nu0">0</span></div>
</div>
<p>Bluez also provides a deamon named bluetoothd (installed in /usr/local/libexc/bluetooth) that runs to take care of communicating with devices for you. It uses DBus to communicate with applications but at this point LE support hasn&#039;t been added. The best way to integrate with it if you want to write your own code is to use a plugin. I won&#039;t go into that here but you can see an example in the source (I also plan on releasing an example of my own that will work with an iOS app). </p>
<p>Lastly remember you don&#039;t need to start off on the Raspberry Pi. Most of the above will work on a normal Linux system and it may be easier to start there to get it working and then move to the RPi once you are familiar with the setup.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/linux/1570/bluetooth-4-0-le-on-raspberry-pi-with-bluez-5-x/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Direct Browser Uploading &#8211; Amazon S3, CORS, FileAPI, XHR2 and Signed PUTs</title>
		<link>http://www.ioncannon.net/programming/1539/direct-browser-uploading-amazon-s3-cors-fileapi-xhr2-and-signed-puts/</link>
		<comments>http://www.ioncannon.net/programming/1539/direct-browser-uploading-amazon-s3-cors-fileapi-xhr2-and-signed-puts/#comments</comments>
		<pubDate>Sun, 02 Sep 2012 05:01:07 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[programming]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1539</guid>
		<description><![CDATA[I&#039;ve been hacking around with FileAPI and XHR2 in HTML5 recently (more on why hopefully in another month or so). So when Amazon announced S3 CORS support I figured I should create a demo of directly uploading a file to S3 from a browser. The first thing to understand is that while the upload happens [...]]]></description>
				<content:encoded><![CDATA[<p>I&#039;ve been hacking around with FileAPI and XHR2 in HTML5 recently (more on why hopefully in another month or so). So when Amazon <a href="http://aws.typepad.com/aws/2012/08/amazon-s3-cross-origin-resource-sharing.html">announced S3 CORS support</a> I figured I should create a demo of directly uploading a file to S3 from a browser.</p>
<p>The first thing to understand is that while the upload happens directly to S3 there still needs to be some server side code that signs the URL used by the PUT call. That bit of code is really simple and I&#039;m including an example at the end for both PHP and Ruby. If you want to skip to the fun part you can check out the <a href="https://github.com/carsonmcdonald/direct-browser-s3-upload-example">PHP and Ruby example code</a> on github (instructions there on deploying to Heroku as well).</p>
<p>Second there are a good number of technologies involved here so I&#039;ve compiled a list of helpful links in case you aren&#039;t already familiar with them and/or want a reference:</p>
<ul>
<li><a href="http://www.html5rocks.com/en/tutorials/file/dndfiles/">FileAPI</a></li>
<li><a href="http://www.html5rocks.com/en/tutorials/file/xhr2/">XHR2</a></li>
<li><a href="http://www.html5rocks.com/en/tutorials/cors/">CORS</a></li>
<li><a href="http://docs.amazonwebservices.com/AmazonS3/latest/dev/cors.html">S3 CORS support docs</a></li>
<li><a href="http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html">S3 REST Authentication</a></li>
<li><a href="http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html">S3 PUT requests</a></li>
</ul>
<p><span id="more-1539"></span></p>
<p>Setting up CORS support for an S3 bucket can be done using the console, see the S3 CORS support docs above for details. For everything in this demo I used the following CORS configuration:</p>
<pre class="brush: xml; title: ; notranslate">
&lt;CORSConfiguration&gt;
    &lt;CORSRule&gt;
        &lt;AllowedOrigin&gt;*&lt;/AllowedOrigin&gt;
        &lt;AllowedMethod&gt;PUT&lt;/AllowedMethod&gt;
        &lt;MaxAgeSeconds&gt;3000&lt;/MaxAgeSeconds&gt;
        &lt;AllowedHeader&gt;Content-Type&lt;/AllowedHeader&gt;
        &lt;AllowedHeader&gt;x-amz-acl&lt;/AllowedHeader&gt;
        &lt;AllowedHeader&gt;origin&lt;/AllowedHeader&gt;
    &lt;/CORSRule&gt;
&lt;/CORSConfiguration&gt;
</pre>
<p>That configuration allows any origin to issue PUTs and include the headers Content-Type, x-amz-acl and origin. You would probably want to restrict the origin more but for this demo I want to make sure it works for people who just cut and paste the above.</p>
<p>The following HTML sets up the file input tag and a progress bar to track the upload (index.html):</p>
<pre class="brush: xml; title: ; notranslate">
&lt;html&gt;
&lt;head&gt;
 &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;styles.css&quot; /&gt;
 &lt;script type=&quot;text/javascript&quot; src=&quot;app.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
  &lt;table&gt;
    &lt;tr&gt;
      &lt;td&gt;File selection:&lt;/td&gt;
      &lt;td&gt;&lt;input type=&quot;file&quot; id=&quot;files&quot; name=&quot;files[]&quot; multiple /&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Progress:&lt;/td&gt;
      &lt;td&gt;&lt;div id=&quot;progress_bar&quot;&gt;&lt;div class=&quot;percent&quot;&gt;0%&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Status:&lt;/td&gt;
      &lt;td&gt;&lt;span id=&quot;status&quot;&gt;&lt;/span&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/table&gt;

  &lt;script type=&quot;text/javascript&quot;&gt;
    document.getElementById('files').addEventListener('change', handleFileSelect, false);
    setProgress(0, 'Waiting for upload.');
  &lt;/script&gt;

&lt;/body&gt;

&lt;/html&gt;
</pre>
<p>CSS for the progress bar (styles.css):</p>
<pre class="brush: css; title: ; notranslate">
#progress_bar {
  width: 200px;
  margin: 10px 0;
  padding: 3px;
  border: 1px solid #000;
  font-size: 14px;
  clear: both;
  opacity: 0;
  -moz-transition: opacity 1s linear;
  -o-transition: opacity 1s linear;
  -webkit-transition: opacity 1s linear;
}
#progress_bar.loading {
  opacity: 1.0;
}
#progress_bar .percent {
  background-color: #99ccff;
  height: auto;
  width: 0;
}
</pre>
<p>The JavaScript that follows has a couple different parts.</p>
<ul>
<li><b>handleFileSelect</b> &#8211; This is where things get started when a file is selected for upload. It kicks off the upload process with each file that was selected.</li>
<li><b>uploadFile</b> &#8211; Called for each file in handleFileSelect and ties the signing process to the S3 PUT process.</li>
<li><b>executeOnSignedUrl</b> &#8211; Calls the server side signing process with the a filename and mime type. The server side signed URL is then passed on to a callback.</li>
<li><b>uploadToS3</b> &#8211; Uses a signed PUT URL to upload the given file to S3 using CORS enabled XHR2.</li>
<li><b>createCORSRequest</b> &#8211; Creates a CORS XHR2 request.</li>
<li><b>setProgress</b> &#8211; Sets the current progress of the upload.</li>
</ul>
<pre class="brush: jscript; title: ; notranslate">
function createCORSRequest(method, url) 
{
  var xhr = new XMLHttpRequest();
  if (&quot;withCredentials&quot; in xhr) 
  {
    xhr.open(method, url, true);
  } 
  else if (typeof XDomainRequest != &quot;undefined&quot;) 
  {
    xhr = new XDomainRequest();
    xhr.open(method, url);
  } 
  else 
  {
    xhr = null;
  }
  return xhr;
}

function handleFileSelect(evt) 
{
  setProgress(0, 'Upload started.');

  var files = evt.target.files; 

  var output = [];
  for (var i = 0, f; f = files[i]; i++) 
  {
    uploadFile(f);
  }
}

/**
 * Execute the given callback with the signed response.
 */
function executeOnSignedUrl(file, callback)
{
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'signput.php?name=' + file.name + '&amp;type=' + file.type, true);

  // Hack to pass bytes through unprocessed.
  xhr.overrideMimeType('text/plain; charset=x-user-defined');

  xhr.onreadystatechange = function(e) 
  {
    if (this.readyState == 4 &amp;&amp; this.status == 200) 
    {
      callback(decodeURIComponent(this.responseText));
    }
    else if(this.readyState == 4 &amp;&amp; this.status != 200)
    {
      setProgress(0, 'Could not contact signing script. Status = ' + this.status);
    }
  };

  xhr.send();
}

function uploadFile(file)
{
  executeOnSignedUrl(file, function(signedURL) 
  {
    uploadToS3(file, signedURL);
  });
}

/**
 * Use a CORS call to upload the given file to S3. Assumes the url
 * parameter has been signed and is accessable for upload.
 */
function uploadToS3(file, url)
{
  var xhr = createCORSRequest('PUT', url);
  if (!xhr) 
  {
    setProgress(0, 'CORS not supported');
  }
  else
  {
    xhr.onload = function() 
    {
      if(xhr.status == 200)
      {
        setProgress(100, 'Upload completed.');
      }
      else
      {
        setProgress(0, 'Upload error: ' + xhr.status);
      }
    };

    xhr.onerror = function() 
    {
      setProgress(0, 'XHR error.');
    };

    xhr.upload.onprogress = function(e) 
    {
      if (e.lengthComputable) 
      {
        var percentLoaded = Math.round((e.loaded / e.total) * 100);
        setProgress(percentLoaded, percentLoaded == 100 ? 'Finalizing.' : 'Uploading.');
      }
    };

    xhr.setRequestHeader('Content-Type', file.type);
    xhr.setRequestHeader('x-amz-acl', 'public-read');

    xhr.send(file);
  }
}

function setProgress(percent, statusLabel)
{
  var progress = document.querySelector('.percent');
  progress.style.width = percent + '%';
  progress.textContent = percent + '%';
  document.getElementById('progress_bar').className = 'loading';

  document.getElementById('status').innerText = statusLabel;
}
</pre>
<p>The above example calls the PHP version of the server side signing code. It can easily be changed to anything that can sign a request in the same way.</p>
<p>I have an <a href="http://www.ioncannon.net/programming/21/creating-s3-urls-that-expire-using-php/">old way of creating signed URLs</a> using PHP that hasn&#039;t been updated in forever. With the more recent versions of PHP there is built in support for the <a href="http://php.net/manual/en/function.hash-hmac.php">hash-hmac function</a> and a <a href="http://php.net/manual/en/function.base64-encode.php">base64 encode</a>. Here is the updated PHP script you need on the server side, to get it to work you would need to replace the S3_KEY, S3_SECRET and S3_BUCKET values with your own:</p>
<pre class="brush: php; title: ; notranslate">
//
// Change the following settings
//
$S3_KEY='S3 Key Here';
$S3_SECRET='S3 Secret Here';
$S3_BUCKET='/uploadtestbucket';

$EXPIRE_TIME=(60 * 5); // 5 minutes
$S3_URL='http://s3.amazonaws.com';

$objectName='/' . $_GET['name'];

$mimeType=$_GET['type'];
$expires = time() + $EXPIRE_TIME;
$amzHeaders= &quot;x-amz-acl:public-read&quot;;
$stringToSign = &quot;PUT\n\n$mimeType\n$expires\n$amzHeaders\n$S3_BUCKET$objectName&quot;;
$sig = urlencode(base64_encode(hash_hmac('sha1', $stringToSign, $S3_SECRET, true)));

$url = urlencode(&quot;$S3_URL$S3_BUCKET$objectName?AWSAccessKeyId=$S3_KEY&amp;Expires=$expires&amp;Signature=$sig&quot;);

echo $url;
</pre>
<p>With all of that in place you should now be able to upload directly to S3 using a browser that supports CORS, XHR2 and the FileAPI (pretty much everything but IE currently).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/programming/1539/direct-browser-uploading-amazon-s3-cors-fileapi-xhr2-and-signed-puts/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Embed Ruby in Your iOS Apps Using mruby</title>
		<link>http://www.ioncannon.net/programming/1524/embed-ruby-in-your-ios-apps-using-mruby/</link>
		<comments>http://www.ioncannon.net/programming/1524/embed-ruby-in-your-ios-apps-using-mruby/#comments</comments>
		<pubDate>Tue, 08 May 2012 15:20:59 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[embed]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[mruby]]></category>
		<category><![CDATA[objective-c]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1524</guid>
		<description><![CDATA[I&#039;ve been playing with mruby for the past week or so. If you haven&#039;t seen it yet it is an embeddable version of Ruby. The first thing I wonder about when I heard about mruby last year a RubyConf was embedding it in iOS apps. Now that the initial version has been released I figured [...]]]></description>
				<content:encoded><![CDATA[<p>I&#039;ve been playing with <a href="https://github.com/mruby/mruby">mruby</a> for the past week or so. If you haven&#039;t seen it yet it is an embeddable version of Ruby. The first thing I wonder about when I heard about mruby last year a RubyConf was embedding it in iOS apps. Now that the initial version has been released I figured I would give it a try.</p>
<p>There are a few things to take into account before diving into this. The first is that the mruby project is very new and there are a number of gaps in the language support right now but the goal is to support the ISO definition of Ruby at some point. The second thing to know is that I&#039;m talking about embedding Ruby here and not writing iOS apps using Ruby. I&#039;m more interested in the potential of Ruby as a scripting language for something like a game. If you want to look into writing iOS apps using Ruby check out <a href="http://www.rubymotion.com/">RubyMotion</a> or the <a href="http://mobiruby.org/">MobiRuby</a> project (MobiRuby is based on mruby).</p>
<p><span id="more-1524"></span></p>
<p>Step one on the path to embedding mruby in an iOS app is getting the library compiled as a framework for XCode. Although the earliest version of mruby had support for building an iOS version that support was removed and there seems to be no current push to add it back. Because of that I created a simple <a href="https://github.com/carsonmcdonald/ios-ruby-embedded">iOS mruby build setup</a> on github. The instructions for using it are included with the project. Once the framework has been created it is easy enough to include in your XCode project just like any other external framework using the &#034;Add Other&#034; option in the &#034;Build Phases&#034; -> &#034;Link Binary With Libraries&#034; section.</p>
<p>There are two ways of using mruby, you can run a script as source or you can pre-compile the source into bytecode first. I&#039;m going to focus on the pre-compiled way because I believe it is the less likely of the two to get an app rejected. This requires that you have the mruby tools available. If you use the build setup I created you can find the tools in the bin directory.</p>
<p>And finally I have created a simple <a href="https://github.com/carsonmcdonald/MRubyiOSExample">mruby iOS example</a> XCode project that you can use to get started. It includes a pre-compiled version of the framework created with the build setup mentioned before and examples of calling objective-c from Ruby and Ruby from objective-c. The following is a quick overview of the example:</p>
<ul>
<li><b>FooUtil.h/m</b> &#8211; This is where all the interaction with mruby happens. It coordinates different parts of the examples as well.</li>
<li><b>example.rb</b> &#8211; This is the Ruby code for the example and has to be compiled into example.mrb before changes will take place.</li>
<li><b>example.mrb</b> &#8211; This is the compiled version of example.rb.</li>
<li><b>FooData.h/m</b> &#8211; This is an example data class that is wrapped using a Ruby class named FooData.</li>
</ul>
<p>Everything is driven from the FooUtil singleton to make it easier to find each example. There are five methods to understand in FooUtil:</p>
<ul>
<li><b>setDebugBlock</b> &#8211; This static method takes a block that will be passed a debug string. This gives an easy way of changing the output of the example.</li>
<li><b>loadFromBundle</b> &#8211; This is an example of loading a pre-compiled script. It assumes it can find the given filename in the app bundle.</li>
<li><b>execute</b> &#8211; This is an example of executing a loaded script. It assumes loadFromBundle has been called. Calling this method will cause the Ruby to objective-c examples to execute.</li>
<li><b>updateBarLocation</b> &#8211; This is an example of executing Ruby code from objective-c.</li>
<li><b>cleanup</b> &#8211; This method will force a garbage collection. It was added to demonstrate the release of the wrapped FooData class.</li>
</ul>
<p>The example code doesn&#039;t as much error checking as you should have in a real app but it hopefully is enough to get you started. I plan on adding more examples to the github project as features get added to mruby and as I have time. As a reminder, you have to recompile the Ruby code if you make changes or they won&#039;t be seen.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/programming/1524/embed-ruby-in-your-ios-apps-using-mruby/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Segmenting WebM Video and the MediaSource API</title>
		<link>http://www.ioncannon.net/utilities/1515/segmenting-webm-video-and-the-mediasource-api/</link>
		<comments>http://www.ioncannon.net/utilities/1515/segmenting-webm-video-and-the-mediasource-api/#comments</comments>
		<pubDate>Tue, 03 Jan 2012 13:59:08 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[utilities]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1515</guid>
		<description><![CDATA[For a while now I&#039;ve seen people ask when support for Apple&#039;s Pantos HTTP live streaming would make it past Safari and iOS. The answer seems to have been that it wasn&#039;t clear that Pantos streaming was the best option and something else would come about eventually that would be more flexible. There have been [...]]]></description>
				<content:encoded><![CDATA[<p>For a while now I&#039;ve seen people ask when support for Apple&#039;s <a href="http://tools.ietf.org/html/draft-pantos-http-live-streaming-07">Pantos HTTP live streaming</a> would make it past Safari and iOS. The answer seems to have been that it wasn&#039;t clear that Pantos streaming was the best option and something else would come about eventually that would be more flexible. There have been other options but they involve either Flash or Silverlight and most people want something that works with html5 video. After a long wait it seems like the time is getting close now with the <a href="http://html5-mediasource-api.googlecode.com/svn/trunk/draft-spec/mediasource-draft-spec.html">MediaSource API</a>.</p>
<p>The MediaSource API has experimental support in Chrome and can be enabled by using the chrome://flags option. To see it in action you can go to the <a href="http://d28nuaxr58rcpu.cloudfront.net/MediaSourceAPIDemo/demo.html">MediaSource demo page</a>. You can also read a litte more about it <a href="http://updates.html5rocks.com/2011/11/Stream-video-using-the-MediaSource-API">here</a> although the spec linked to above is probably a better place to learn about it.</p>
<p>A while ago I created a tool for <a href="http://www.ioncannon.net/projects/http-live-video-stream-segmenter-and-distributor/">segmenting H264 video</a> in a Pantos compliant way. When I saw the MediaSource API I wondered how the same type of tool might fit in. The first thing to note is that the Pantos draft describes a complete technique for video streaming while the MediaSource API gives you the tools to stream video and leaves the technique. What follows is a simple technique for segmenting a WebM video in a way that allows standard streaming with the MediaSource API in the same fasion as the Pantos draft technique. While this example will not support variable rate streams it could be expanded to do so and would be the next logical step.</p>
<p><span id="more-1515"></span></p>
<p>The basics of how the Pantos technique works are simple. A video is broken down into chunks and each of those chunks is downloaded, buffered and played. In older drafts this ment splitting a video file into multiple files but in later drafts support was added for range requests allowing the large file to remain intact. With that understanding there are two major parts that need to be addressed:</p>
<ul>
<li>Segmenting a WebM video &#8211; This will require some understanding of the container format for WebM and the result will be something like a playlist for the video.</li>
<li>Javascript to play the video &#8211; This will use the playlist created by segmenting the WebM video to drive the MediaSource API.</li>
</ul>
<h2>Segmenting a WebM Video</h2>
<p>Before reading on take a second to read the <a href="http://www.webmproject.org/about/">about WebM</a> page. It describes that WebM specifies a video/audio codec and a container format. We are only interested in the format of the container. If you want all the details you can read the <a href="http://www.webmproject.org/code/specs/container/">WebM container</a> format specs. It is enough to know that the WebM container is a modified version of the <a href="http://matroska.org/">Matroska Container</a> format and both containers use <a href="http://ebml.sourceforge.net/specs/">EBML</a> to specify their structure.</p>
<p>EBML is basically a binary XML format. There are a number of tools available for readin EBML and for my perposes I found the Python <a href="https://github.com/jspiros/python-ebml">python-ebml</a> implementation to be the easiest to get working.</p>
<p>The MediaSource API wants to be fed chunks of video data in a specific way. First it wants header information and then it wants what are called <a href="http://www.webmproject.org/code/specs/container/#cluster">clusters</a> from the WebM container. Each cluster contains various information but the most likely reason for wanting a cluster as input is that each cluster starts with a keyframe. A keyframe is an important reference point in the video stream and one that gets built on until the next keyframe.</p>
<p>The following code will take as input a WebM file and output a playlist for that file in JSON format. The playlist will contain the offsets of each cluster in the given WebM video and we will use it later to fetch the header and each cluster as chunks and feed those to a video element:</p>
<pre class="brush: python; title: ; notranslate">
from ebml.schema import EBMLDocument, UnknownElement, CONTAINER, BINARY


def fill_video_info(element, offset, video_info):
  if element.name == 'Duration':
    video_info['duration'] = element.value

  if element.name == 'DisplayWidth':
    video_info['width'] = element.value

  if element.name == 'DisplayHeight':
    video_info['height'] = element.value

  if element.name == 'Cluster':
    video_info['clusters'].append({'offset': offset})

  if element.name == 'Timecode':
    video_info['clusters'][-1]['timecode'] = element.value

  if element.type == CONTAINER:
    for sub_el in element.value:
      fill_video_info(sub_el, offset + element.head_size, video_info)
      offset += sub_el.size


if __name__ == '__main__':
  import sys
  import json
  import os
  
  mod_name, _, cls_name = 'ebml.schema.matroska.MatroskaDocument'.rpartition('.')
  try:
    doc_mod = __import__(mod_name, fromlist=[cls_name])
    doc_cls = getattr(doc_mod, cls_name)
  except ImportError:
    parser.error('unable to import module %s' % mod_name)
  except AttributeError:
    parser.error('unable to import class %s from %s' % (cls_name, mod_name))
  
  video_info = {}
  video_info['filename'] = sys.argv[1]
  video_info['total_size'] = os.stat(sys.argv[1]).st_size
  video_info['clusters'] = []

  with open(sys.argv[1], 'rb') as stream:
    doc = doc_cls(stream)
    offset = 0
    for el in doc.roots:
      fill_video_info(el, offset, video_info)
      offset += el.size

  print json.dumps(video_info)
</pre>
<p>To run the script you will want to save the above script and install the module then do something like this:</p>
<pre class="brush: bash; title: ; notranslate">
git clone https://github.com/jspiros/python-ebml.git
PYTHONPATH=python-ebml python create_playlist.py test.webm
</pre>
<p>Running the script will result in JSON output that can be piped to a file and used as a playlist.</p>
<h2>Javascript to Play the WebM Segmented Playlist</h2>
<p>Feeding data to the MediaSource API is done with Javascript. The first thing to do is read in the JSON playlist file created by the Python script when the page is finished loading. To do that I&#039;m using the Snack JS framework, it is micro-framework that has some of the features of JQuery. After reading in the playlist chunks of the WebM file need to be read in with range requests.</p>
<p>A while ago I made created a post about <a href="http://www.ioncannon.net/programming/1506/range-requests-with-ajax/">range requests with ajax</a> and that comes in handy here. The main difference between that post and what we need here is that the MediaSource API expects a typed array as input so we need to use <a href="http://www.w3.org/TR/XMLHttpRequest2/">XHR2</a>.</p>
<p>The following Javascript code is bare bones. One of the most obvious limitations is that it doesn&#039;t have the ability to seek to a portion of the stream, it will only play the stream start to finish or if the player has the content buffered it should allow for seeking in the buffered portion. It demonstrates that the API works and should be a good starting point for experimenting more with multi-rate streaming which is my short term goal.</p>
<pre class="brush: jscript; title: ; notranslate">
  /**
   * This fetches a chunk of video using a range request.
   *   video_name - The relative filename of the video to fetch
   *   start_bytes - The start byte of the chunk to fetch
   *   end_bytes - The last byte of the chunk to fetch
   *   is_last - True if this is the last chunk to fetch
   */
  function fetch_chunk(video, video_name, start_bytes, end_bytes, is_last)
  {
    var range_req = 'bytes=' + start_bytes + '-' + end_bytes;

    var xhr = new XMLHttpRequest();
    xhr.open('GET', video_name, true);
    xhr.setRequestHeader(&quot;Range&quot;, range_req);
    xhr.responseType = 'arraybuffer';

    xhr.onload = function(e) 
    {
      video.webkitSourceAppend(new Uint8Array(this.response));

      if(is_last)
      {
        video.webkitSourceEndOfStream(HTMLMediaElement.EOS_NO_ERROR);
      }
    };

    xhr.send();
  }

  /**
   * Fetch the header portion of the video file.
   */
  function fetch_header(video, video_info)
  {
    fetch_chunk(video, video_info.filename, 0, video_info.clusters[0].offset-1, video_info.clusters.length == 1);
  }

  var current_cluster = 0;

  /**
   * Fetch the next cluster of the video file.
   */
  function fetch_next_cluster(video, video_info)
  {
    if(video_info.clusters.length == current_cluster+1)
    {
      fetch_chunk(video, video_info.filename, video_info.clusters[current_cluster].offset, video_info.total_size, true);
    }
    else
    {
      fetch_chunk(video, video_info.filename, video_info.clusters[current_cluster].offset, video_info.clusters[current_cluster+1].offset-1, false);
    }

    current_cluster++;
  }

  /**
   * Enable the video for MediaSource and attach event listeners to drive the fetching of video chunks.
   */
  function video_setup(video_info)
  {
    current_cluster = 0;

    video.src = video.webkitMediaSourceURL;

    video.addEventListener('webkitsourceopen', function(e) 
    {
      var video = this;

      fetch_header(video, video_info);
      fetch_next_cluster(video, video_info);
    }, false);

    video.addEventListener('progress', function(e)
    {
      var video = this;

      if( video.webkitSourceState != HTMLMediaElement.SOURCE_ENDED )
      {
        fetch_next_cluster(video, video_info);
      }
    });
  }

  /**
   * When the page is done loading read in the JSON playlist and get the video set up.
   */
  snack.ready(function()
  {
    if (!video.webkitMediaSourceURL) 
    {
      alert('webkitMediaSourceURL is not available');
      return;
    }

    snack.request({ method: 'get', url: 'test.json', }, function (err, res)
    {
      if (err) 
      {
        alert(err);
        return;
      }

      video_setup(snack.parseJSON(res));
    });
  });
</pre>
<p>Assuming you have enabled support in Chrome (currently the only browser to support the API) you can see everything in action by checking out my <a href="http://d28nuaxr58rcpu.cloudfront.net/MediaSourceAPIDemo/demo.html">MediaSource API demo</a> page.</p>
<p>I have glossed over a lot of details but the main take aways are that the MediaSource API gives you the interface to build a Pantos streaming system, segment WebM on clusters to feed to the MediaSource API and use XHR2 to fetch the WebM data with range requests. As one last point a lot of the best practices for encoding video for Pantos streaming will work here as well.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/utilities/1515/segmenting-webm-video-and-the-mediasource-api/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Range Requests with Ajax</title>
		<link>http://www.ioncannon.net/programming/1506/range-requests-with-ajax/</link>
		<comments>http://www.ioncannon.net/programming/1506/range-requests-with-ajax/#comments</comments>
		<pubDate>Tue, 22 Nov 2011 16:03:58 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[ajax]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[javascript]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1506</guid>
		<description><![CDATA[I ran across something the other day that made wonder about doing range requests using ajax. For some reason it wasn&#039;t obvious at first if this would be easy but as it turns out it is. If you aren&#039;t familiar with range requests head over to the HTTP RFC and check out the range header. [...]]]></description>
				<content:encoded><![CDATA[<p>I ran across something the other day that made wonder about doing range requests using ajax. For some reason it wasn&#039;t obvious at first if this would be easy but as it turns out it is.</p>
<p>If you aren&#039;t familiar with range requests head over to the HTTP RFC and check out the <a href="http://tools.ietf.org/html/rfc2616#section-14.35.2">range header</a>. Your web server needs to support range requests for this to be useful but most do so that shouldn&#039;t be a huge issue. As a bonus you will find that some CDNs support range request as well (Amazon S3 for example).</p>
<p><span id="more-1506"></span></p>
<p>To show how this works I&#039;ll start off by getting the offsets of a text file, then use curl to show the actual request and then put it into an ajax request. Even though I&#039;m using a text file the same thing could be done with a binary file as well. Here are the contents of the file I&#039;ve called range-test.txt:</p>
<pre class="brush: plain; title: ; notranslate">
First part of the file.

More text here.

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

Some text after Lorem Ipsum.

End of the file.
</pre>
<p>Using egrep we can get the offset of each line pretty easy like so:</p>
<pre class="brush: plain; title: ; notranslate">
&gt; egrep -b &quot;\r|\n&quot; range-test.txt 
0:First part of the file.
25:More text here.
42:Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
618:Some text after Lorem Ipsum.
660:End of the file.
</pre>
<p>Quickly verify that the range looks right in isolation using hexdump:</p>
<pre class="brush: plain; title: ; notranslate">
&gt; hexdump -c -s 618 -n 30 range-test.txt 
000026a   S   o   m   e       t   e   x   t       a   f   t   e   r    
000027a   L   o   r   e   m       I   p   s   u   m   .  \n  \n        
0000288
</pre>
<p>Now we have a sample range so put the text file on your web server and use curl to send the range request headers (note that the hexdump takes an offset and count of bytes while the range header takes an start and end offset):</p>
<pre class="brush: plain; title: ; notranslate">
&gt; curl -H 'Range:bytes=618-647' http://localhost/range-test.txt
Some text after Lorem Ipsum.

</pre>
<p>Now that it makes sense how this range is used to make a request for part of a file we can move on to doing it with an ajax request. I&#039;m going to use <a href="http://jquery.com/">jQuery</a>&#039;s <a href="http://api.jquery.com/jQuery.ajax/">ajax</a> function to make this really simple. It has a feature that lets you set headers really easily. The following is all you need:</p>
<pre class="brush: xml; title: ; notranslate">
&lt;html&gt;
  &lt;head&gt;
    &lt;script type=&quot;text/javascript&quot; src=&quot;jquery-1.7.min.js&quot;&gt;&lt;/script&gt;
    &lt;script type=&quot;text/javascript&quot;&gt;
    $(function() {
      $.ajax({
        url: 'range-test.txt',
        headers: {Range: &quot;bytes=618-647&quot;},
        success: function( data ) { $('#results').html( data ); }
      });
    });
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id=&quot;results&quot;&gt;&lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
</pre>
<p>And that is all there is to it. This could be useful for all kinds of things like displaying just a section of a large log file at a time. This might also be useful in a situation where you want to stream binary data back to the browser a chunk at a time.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/programming/1506/range-requests-with-ajax/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Using WebP to Reduce Native iOS App Size</title>
		<link>http://www.ioncannon.net/programming/1483/using-webp-to-reduce-native-ios-app-size/</link>
		<comments>http://www.ioncannon.net/programming/1483/using-webp-to-reduce-native-ios-app-size/#comments</comments>
		<pubDate>Wed, 29 Jun 2011 12:23:40 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[WebP]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1483</guid>
		<description><![CDATA[Last year Google released WebM as an alternative to h264 encoded video. They followed that up with the release of WebP as an alternative to JPG. Ever since the release I&#039;ve been thinking about giving it a try on iOS to see how well it might work to reduce application size. As a bonus to [...]]]></description>
				<content:encoded><![CDATA[<p>Last year Google released <a href="http://www.webmproject.org/">WebM</a> as an alternative to h264 encoded video. They followed that up with the release of <a href="http://code.google.com/speed/webp/">WebP</a> as an alternative to JPG. Ever since the release I&#039;ve been thinking about giving it a try on iOS to see how well it might work to reduce application size. As a bonus to reduced size, WebP also supports an alpha channel that JPG doesn&#039;t (there is more information available on the original <a href="http://blog.chromium.org/2010/09/webp-new-image-format-for-web.html">release</a> blog post).</p>
<p><span id="more-1483"></span></p>
<p>To follow along you will need to grab the source for WebP and compile it. The easiest way to do that is to follow the official <a href="http://code.google.com/speed/webp/docs/compiling.html">compiling WebP</a> instructions. After compiling the source you will be able to encode WebP images, later I describe how to build the same source for use with iOS. </p>
<p>Before trying to build libwebp for iOS I did some testing to see just how much of a reduction I could achieve over JPG. I used ImageMagick to convert the source images into JPG and pushed the quality of the JPG down as far as seemed reasonable to reduce the resulting size. I tried a number of combinations but ended up using the following command to convert the JPGs:</p>
<pre class="brush: bash; title: ; notranslate">
convert -quality 75 -resize 1024x680 inputfile.png outputfile.jpg
</pre>
<p>Here are a couple samples produced using that command:</p>
<p><a href="http://www.ioncannon.net/wp-content/uploads/2011/06/test2.jpg"><img src="http://www.ioncannon.net/wp-content/uploads/2011/06/test2-300x199.jpg" alt="Egg Carton" title="Egg Carton" width="300" height="199" class="alignnone size-medium wp-image-1485" /></a></p>
<p><a href="http://www.ioncannon.net/wp-content/uploads/2011/06/test9.jpg"><img src="http://www.ioncannon.net/wp-content/uploads/2011/06/test9-300x199.jpg" alt="Elephants" title="Elephants" width="300" height="199" class="alignnone size-medium wp-image-1485" /></a></p>
<p>For WebP I made the quality 90 and encoded the same images with the following command:</p>
<pre class="brush: bash; title: ; notranslate">
cwebp -q 90 -resize 1024 680 inputfile.png -o outputfile.webp
</pre>
<p>Here are a couple example images produced using that command (at the time of this post these will only be visible using Chrome):</p>
<p><a href="http://www.ioncannon.net/wp-content/uploads/2011/06/test2.webp"><img src="http://www.ioncannon.net/wp-content/uploads/2011/06/test2-300x199.webp" alt="Egg Carton" title="Egg Carton" width="300" height="199" class="alignnone size-medium wp-image-1485" /></a></p>
<p><a href="http://www.ioncannon.net/wp-content/uploads/2011/06/test9.webp"><img src="http://www.ioncannon.net/wp-content/uploads/2011/06/test9-300x199.webp" alt="Elephants" title="Elephants" width="300" height="199" class="alignnone size-medium wp-image-1485" /></a></p>
<p>Even with the advantage I was giving the JPG images by compressing them with a lower quality I was able to get a 25% size reduction in the WebP version. Generally the JPG images would have been usable but some ended up with artifacts that would have required them to be re-encoded at a higher quality. The WebP versions ended up looking better in every case.</p>
<p>This was just my quick attempt to verify that it would be worth the effort of using WebP. A more in depth overview of JPG vs WebP can be found in the <a href="http://code.google.com/speed/webp/gallery.html">example gallery</a>.</p>
<p>With that simple test out of the way I created a script that would compile the libwebp source into a framework compatible with the iOS simulators, iPad and iPhone:</p>
<pre class="brush: bash; title: ; notranslate">
SDK=4.3
PLATFORMS=&quot;iPhoneSimulator iPhoneOS-V6 iPhoneOS-V7&quot;
DEVELOPER=/Developer
TOPDIR=`pwd`
BUILDDIR=&quot;$TOPDIR/tmp&quot;
FINALDIR=&quot;$TOPDIR/WebP.framework&quot;
LIBLIST=''

mkdir -p $BUILDDIR
mkdir -p $FINALDIR
mkdir $FINALDIR/Headers/

for PLATFORM in ${PLATFORMS}
do
  if [ &quot;${PLATFORM}&quot; == &quot;iPhoneOS-V7&quot; ]
  then
    PLATFORM=&quot;iPhoneOS&quot;
    ARCH=&quot;armv7&quot;
  elif [ &quot;${PLATFORM}&quot; == &quot;iPhoneOS-V6&quot; ]
  then
    PLATFORM=&quot;iPhoneOS&quot;
    ARCH=&quot;armv6&quot;
  else
    ARCH=&quot;i386&quot;
  fi

  ROOTDIR=&quot;${BUILDDIR}/${PLATFORM}-${SDK}-${ARCH}&quot;
  rm -rf &quot;${ROOTDIR}&quot;
  mkdir -p &quot;${ROOTDIR}&quot;

  export DEVROOT=&quot;${DEVELOPER}/Platforms/${PLATFORM}.platform/Developer&quot;
  export SDKROOT=&quot;${DEVROOT}/SDKs/${PLATFORM}${SDK}.sdk&quot;
  export CC=${DEVROOT}/usr/bin/gcc
  export LD=${DEVROOT}/usr/bin/ld
  export CPP=${DEVROOT}/usr/bin/cpp
  export CXX=${DEVROOT}/usr/bin/g++
  export AR=${DEVROOT}/usr/bin/ar
  export AS=${DEVROOT}/usr/bin/as
  export NM=${DEVROOT}/usr/bin/nm
  export CXXCPP=$DEVROOT/usr/bin/cpp
  export RANLIB=$DEVROOT/usr/bin/ranlib
  export LDFLAGS=&quot;-arch ${ARCH} -pipe -no-cpp-precomp -isysroot ${SDKROOT} -L${ROOTDIR}/lib&quot;
  export CFLAGS=&quot;-arch ${ARCH} -pipe -no-cpp-precomp -isysroot ${SDKROOT} -I${ROOTDIR}/include&quot;
  export CXXFLAGS=&quot;-arch ${ARCH} -pipe -no-cpp-precomp -isysroot ${SDKROOT} -I${ROOTDIR}/include&quot;

  rm -rf libwebp-0.1.2
  tar xzf libwebp-0.1.2.tar.gz
  cd libwebp-0.1.2

  sh autogen.sh

  ./configure --host=${ARCH}-apple-darwin --prefix=${ROOTDIR} --disable-shared --enable-static
  make
  make install

  LIBLIST=&quot;${LIBLIST} ${ROOTDIR}/lib/libwebp.a&quot;
  cp -Rp ${ROOTDIR}/include/webp/* $FINALDIR/Headers/

  cd ..
done

${DEVROOT}/usr/bin/lipo -create $LIBLIST -output $FINALDIR/WebP

rm -rf libwebp-0.1.2
rm -rf ${BUILDDIR}
</pre>
<p>The script assumes that the source for libwebp is in the same directory as the script is executed from. If you skipped the first part of the post or want to make sure you are using the same version I used then use the following curl command:</p>
<pre class="brush: bash; title: ; notranslate">
curl http://webp.googlecode.com/files/libwebp-0.1.2.tar.gz &gt; libwebp-0.1.2.tar.gz
</pre>
<p>After you run the script you will have a WebP.framework directory that represents the library. You can use this as a framework in XCode.</p>
<p>Using the library is easy. The following snipit shows how to get the current WebP library version, get the width and height of an image and then read the image into a buffer:</p>
<pre class="brush: cpp; title: ; notranslate">
    // Get the current version of the WebP decoder
    int rc = WebPGetDecoderVersion();
    
    NSLog(@&quot;Version: %d&quot;, rc);

    // Get the width and height of the selected WebP image
    int width = 0;
    int height = 0;
    WebPGetInfo([myData bytes], [myData length], &amp;width, &amp;height);
    
    NSLog(@&quot;Image Width: %d Image Height: %d&quot;, width, height); 
    
    // Decode the WebP image data into a RGBA value array
    uint8_t *data = WebPDecodeRGBA([myData bytes], [myData length], &amp;width, &amp;height);
</pre>
<p>There is more documentation on the <a href="http://code.google.com/speed/webp/docs/api.html">WebP API</a> and I&#039;ve put together a complete example on Github. If you grab the <a href="https://github.com/carsonmcdonald/WebP-iOS-example">WebP iOS example</a> from Github you will find the build script as well as the full example. First build the library from a command line then open the example project in XCode. If you don&#039;t first build the library before opening the project you will see that the WebP.framework is missing.</p>
<p>At this point you are on your way to having smaller iOS applications. There is at least one drawback at this point and that is speed. Decoding is a little slow and I think this is because the library isn&#039;t constructed to take advantage of any hardware acceleration. The next step is to see if it can be sped up by using the accelerate framework.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/programming/1483/using-webp-to-reduce-native-ios-app-size/feed/</wfw:commentRss>
		<slash:comments>9</slash:comments>
		</item>
		<item>
		<title>Using the Google Closure Compiler in Java</title>
		<link>http://www.ioncannon.net/programming/1447/using-the-google-closure-compiler-in-java/</link>
		<comments>http://www.ioncannon.net/programming/1447/using-the-google-closure-compiler-in-java/#comments</comments>
		<pubDate>Tue, 08 Mar 2011 14:29:55 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[closure]]></category>
		<category><![CDATA[java]]></category>
		<category><![CDATA[javascript]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1447</guid>
		<description><![CDATA[I recently had a chance to try out Google&#039;s closure compiler. The closure compiler is similar to the YUI compressor except that along with minimizing it may rewrite the JavaScript. If you want to understand more about what it does start at the overview documentation and then go from there. What I needed was a [...]]]></description>
				<content:encoded><![CDATA[<p>I recently had a chance to try out <a href="http://code.google.com/closure/compiler/">Google&#039;s closure compiler</a>. The closure compiler is similar to the <a href="http://developer.yahoo.com/yui/compressor/">YUI compressor</a> except that along with minimizing it may rewrite the JavaScript. If you want to understand more about what it does start at the <a href="http://code.google.com/closure/compiler/docs/overview.html">overview documentation</a> and then go from there.</p>
<p>What I needed was a way to use the closure compiler in an Ant task. The Ant task that comes with the library is good but there wasn&#039;t a way for me to integrate it into an existing system that wasn&#039;t going to change. After looking around for some example code and not finding any I went into the library&#039;s Ant task and figured out how to wire it all up myself.</p>
<p><span id="more-1447"></span></p>
<p>It isn&#039;t that hard to use the compiler library in your own Java code but without a simple example it takes some work to figure out what is needed and what isn&#039;t. The following code is just about as bare bones as you can get. It takes a number of JavaScript files and compile them using the medium setting, more about the choice of settings after the code:</p>
<pre class="brush: java; title: ; notranslate">
import com.google.javascript.jscomp.*;

import java.io.FileWriter;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Level;

public class Test
{
  public static void main(String[] args) throws Exception
  {
    // These are external JavaScript files you reference but don't want changed
    String externalJavascriptResources[] = {
        &quot;jquery.js&quot;,
        &quot;jqueryui.js&quot;
    };
    // These are the files you want optimized
    String primaryJavascriptToCompile[] = { 
        &quot;somejavascript.js&quot;,
        &quot;otherjavascript.js&quot;
    };
    // This is where the optimized code will end up
    String outputFilename = &quot;combined.min.js&quot;;

    com.google.javascript.jscomp.Compiler.setLoggingLevel(Level.INFO);
    com.google.javascript.jscomp.Compiler compiler = new com.google.javascript.jscomp.Compiler();

    CompilerOptions options = new CompilerOptions();
    CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel(options);

    WarningLevel.VERBOSE.setOptionsForWarningLevel(options);

    List&lt;JSSourceFile&gt; externalJavascriptFiles = new ArrayList&lt;JSSourceFile&gt;();
    for (String filename : externalJavascriptResources)
    {
      externalJavascriptFiles.add(JSSourceFile.fromFile(filename));
    }

    List&lt;JSSourceFile&gt; primaryJavascriptFiles = new ArrayList&lt;JSSourceFile&gt;();
    for (String filename : primaryJavascriptToCompile)
    {
      primaryJavascriptFiles.add(JSSourceFile.fromFile(filename));
    }

    compiler.compile(externalJavascriptFiles, primaryJavascriptFiles, options);

    for (JSError message : compiler.getWarnings())
    {
      System.err.println(&quot;Warning message: &quot; + message.toString());
    }

    for (JSError message : compiler.getErrors())
    {
      System.err.println(&quot;Error message: &quot; + message.toString());
    }

    FileWriter outputFile = new FileWriter(outputFilename);
    outputFile.write(compiler.toSource());
    outputFile.close();
  }
}
</pre>
<p>The above code is doing a number of things:</p>
<ul>
<li>It takes both external resources you don&#039;t want optimized as well as resources you do want optimized that refer to those external resources. You may not need to list external resources depending on what level of optimization you use.</li>
<li>It combines all the input files that are not external into one output file.</li>
<li>It compiles the javascript code given to it using the SIMPLE_OPTIMIZATIONS setting.</li>
<li>It lists any warnings or errors it found while compiling the code.</li>
</ul>
<p>One of the nice side effects of using the closure compiler is that because it compiles the code it can alert you to errors or potential issues using warnings.</p>
<p>What is listed above is using the middle of the road optimizations. There is a level above SIMPLE_OPTIMIZATIONS that you can set by changing that line in the above code to:</p>
<pre class="brush: java; title: ; notranslate">
  CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
</pre>
<p>Be aware that the advanced optimizations will do things, renaming your variables and functions to name two, that can break your code. The compiler gives you a way of alerting it to things you don&#039;t want changed using externs and exports. Before using the advanced option you should read the <a href="http://code.google.com/closure/compiler/docs/api-tutorial3.html">advanced options tutorial</a>.</p>
<p>If you do not like seeing all the steps the compiler takes you can set the logging level to QUIET by changing the WarningLevel line to the following:</p>
<pre class="brush: java; title: ; notranslate">
  WarningLevel.QUIET.setOptionsForWarningLevel(options);
</pre>
<p>One last note about the compiler is that the <a href="http://code.google.com/closure/compiler/docs/inspector.html">closure inspector</a> is a Firebug plugin that will let you see what the original line of code looked like before it was compiled. This can help a lot when something goes wrong.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/programming/1447/using-the-google-closure-compiler-in-java/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Google Analytics Dashboard WordPress Plugin Version 2.0 Released</title>
		<link>http://www.ioncannon.net/utilities/1460/google-analytics-dashboard-wordpress-plugi-version-2-0-released/</link>
		<comments>http://www.ioncannon.net/utilities/1460/google-analytics-dashboard-wordpress-plugi-version-2-0-released/#comments</comments>
		<pubDate>Tue, 22 Feb 2011 14:13:22 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[utilities]]></category>
		<category><![CDATA[wordpress]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1460</guid>
		<description><![CDATA[It has taken me a while but I&#039;ve finally been able to release version 2 of the Google Analytics Dashboard WordPress Plugin. The primary enhancement of this version is that it no longer blocks the dashboard, posts or pages interfaces while loading. The next major change is an upgrade to using Google&#039;s OAuth login system [...]]]></description>
				<content:encoded><![CDATA[<p>It has taken me a while but I&#039;ve finally been able to release version 2 of the <a href="http://www.ioncannon.net/projects/google-analytics-dashboard-wordpress-widget/">Google Analytics Dashboard WordPress Plugin</a>. The primary enhancement of this version is that it no longer blocks the dashboard, posts or pages interfaces while loading. The next major change is an upgrade to using Google&#039;s OAuth login system (see my <a href="http://www.ioncannon.net/programming/1443/google-oauth-for-installed-apps-php-example/">Google OAuth example using PHP</a> for more information on how I did that if you are interested). The old login system is still available but the OAuth login is the one to use going forward and I may remove the old one at some point. The move to OAuth along with the refactoring I did to the code will allow support for other Google sites such as Feedburner. As a bonus I also moved the caching system to the newer WordPress transient storage interface. The use of the transient storage interface should fix one of the biggest issues people have seen in the past so no more worrying about finding a temp directory that is writable.</p>
<p>Here are a list of the major changes:</p>
<ul>
<li>The dashboard panel now loads asynchronously so the entire dashboard doesn&#039;t block while it is loading</li>
<li>Made the analytics column in posts and pages load asynchronously so that it doesn&#039;t block the loading of the page</li>
<li>Added support for Google OAuth logins</li>
<li>Use transient API support with wordpress version 2.8+</li>
<li>Added ability to support multiple analytics sources</li>
</ul>
<p>Some other minor changes:</p>
<ul>
<li>Stop unlink warnings when caching won&#039;t work</li>
<li>Refactored code so that major parts are split into classes</li>
<li>Refactored code to better seperate UI code</li>
<li>Fixed mime type not being sent correctly for admin area javascript file</li>
<li>Fix bug in wordpress version checking</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/utilities/1460/google-analytics-dashboard-wordpress-plugi-version-2-0-released/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Google OAuth for Installed Apps PHP Example</title>
		<link>http://www.ioncannon.net/programming/1443/google-oauth-for-installed-apps-php-example/</link>
		<comments>http://www.ioncannon.net/programming/1443/google-oauth-for-installed-apps-php-example/#comments</comments>
		<pubDate>Tue, 08 Feb 2011 16:53:20 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[programming]]></category>
		<category><![CDATA[google]]></category>
		<category><![CDATA[oauth]]></category>
		<category><![CDATA[php]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1443</guid>
		<description><![CDATA[I have been working on a long needed update to the Google analytics dashboard plugin for WordPress and one of the items I had on my TODO list was using Google&#039;s OAuth login instead of the old ClientLogin. Setting OAuth up for a WordPress plugin is complicated because it isn&#039;t a hosted application and as [...]]]></description>
				<content:encoded><![CDATA[<p>I have been working on a long needed update to the <a href="http://www.ioncannon.net/projects/google-analytics-dashboard-wordpress-widget/">Google analytics dashboard plugin for WordPress</a> and one of the items I had on my TODO list was using Google&#039;s OAuth login instead of the old ClientLogin. Setting OAuth up for a WordPress plugin is complicated because it isn&#039;t a hosted application and as such I can&#039;t register it to get OAuth keys. That is where a special way of doing OAuth comes in called <a href="http://code.google.com/apis/accounts/docs/OAuthForInstalledApps.html">OAuth for installed apps</a>.</p>
<p>There seems to be a lot of general documentation on how to do OAuth but there wasn&#039;t much about using it for installed apps so what follows is an example using PHP that is basically what went into the plugin update.</p>
<p><span id="more-1443"></span></p>
<p>It helps to first know the basic steps that will make this work. From the documentation those steps are:</p>
<ol>
<li>Your application gets an unauthorized request token from Google&#039;s authorization server.</li>
<li>Google asks the user to grant you access to the required data.</li>
<li>Your application gets an authorized request token from the authorization server.</li>
<li>You exchange the authorized request token for an access token.</li>
<li>You use the access token to request data from Google&#039;s service access servers.</li>
</ol>
<p>I will reference these steps in the code that follows.</p>
<p>Before trying this example out grab the <a href="http://oauth.googlecode.com/svn/code/php/">OAuth PHP library</a>. The only file that is needed to execute the examples is OAuth.php. </p>
<p>I have split the code into two files that I named phase1.php and phase2.php. In phase1.php the resulting URL would be used to grant access to the given scope (note that I&#039;ve marked parts of the code that would need to be customized with a &#034;Customize this&#034; comment):</p>
<pre class="brush: php; title: ; notranslate">
require_once(&quot;OAuth.php&quot;);

// This is the setup for step 1
// 1. Your application gets an unauthorized request token from Google's authorization server.
$signature_method = new OAuthSignatureMethod_HMAC_SHA1();

$params = array();

// The callback is where the user comes back after authentication
$params['oauth_callback'] = 'http://example.com/phase2.php'; // Customize this

// The scope is what you are asking for access to
$params['scope'] = 'https://www.google.com/analytics/feeds/'; // Customize this

// Setting xoauth_displayname will show the given name on the authentication screen
$params['xoauth_displayname'] = 'My Test App'; // Customize this

// Set up an anonymous oauth consumer
$consumer = new OAuthConsumer('anonymous', 'anonymous', NULL);

// Set up the request for a request token
$req_req = OAuthRequest::from_consumer_and_token($consumer, NULL, 'GET', 'https://www.google.com/accounts/OAuthGetRequestToken', $params);

// Sign the request
$req_req-&gt;sign_request($signature_method, $consumer, NULL);

// Set up curl and have it get the token to use for the authenication call
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $req_req-&gt;to_url());

// This tells curl to return the response as one string
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 

// Run curl and grab the output and the return code
// This is the execution of step 1
$return = curl_exec($ch); 
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if($http_code == 200)
{
  // If the call was good parse out the response parameters into an array
  $access_params = array();
  $param_pairs = explode('&amp;', $return);
  foreach($param_pairs as $param_pair) 
  {
    if (trim($param_pair) == '') { continue; }
    list($key, $value) = explode('=', $param_pair);
    $access_params[$key] = urldecode($value);
  }

  // Print out the authentication URL that needs to be opened in a browser
  echo &quot;Open this link: https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=&quot; . urlencode($access_params['oauth_token']) . &quot;\n&quot;;

  // This is the oauth token secret that is needed for the second part
  echo &quot;\nRemember the folowing value: &quot; . $access_params['oauth_token_secret'] . &quot;\n&quot;;

  // This is where the user would perform step 2
  // 2. Google asks the user to grant you access to the required data.
}
else
{
  echo &quot;Error: $http_code and $return\n&quot;;
}
</pre>
<p>The above code performs steps 1 and 2. There are a few notes that are good to know:</p>
<ul>
<li>The OAuth consumer is set to &#034;anonymous&#034; here because this is an installed app, that is used again in the second phase.</li>
<li>There can be multiple scope values, just seperate them with a space.</li>
<li>Make sure to save the oauth_token_secret value, it is required for the second phase.</li>
</ul>
<p>At this point the user goes off to authorize the application on Google&#039;s system. When they are done they are redirected back to your callback URL specified in phase 1 as oauth_callback. I put the following code in a file named phase2.php (again I&#039;ve noted places that would need to be customized):</p>
<pre class="brush: php; title: ; notranslate">
require_once(&quot;OAuth.php&quot;);

// This is where step 3 comes in, this would be in the callback and would be called
// after the user authenticates.
// 3. Your application gets an authorized request token from the authorization server.
$oauth_verifier = urldecode($_REQUEST['oauth_verifier']);
$oauth_token = urldecode($_REQUEST['oauth_token']);

// The token secret would need to be looked up from the previous step
$ouath_token_secret = ''; // Customize this

// This is where setup for step 4 starts
// 4. You exchange the authorized request token for an access token.
$signature_method = new OAuthSignatureMethod_HMAC_SHA1();

$params = array();

$params['oauth_verifier'] = $oauth_verifier;

// Set up the anonymous consumer again
$consumer = new OAuthConsumer('anonymous', 'anonymous', NULL);

// Use the oauth token and token secret to set up the consumer for the access token
$final_consumer = new OAuthConsumer($oauth_token, $ouath_token_secret);

// Set up the call to get the access token
$acc_req = OAuthRequest::from_consumer_and_token($consumer, $final_consumer, 'GET', 'https://www.google.com/accounts/OAuthGetAccessToken', $params);

$acc_req-&gt;sign_request($signature_method, $consumer, $final_consumer);

// Set up curl and have it get the final token and secret
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $acc_req-&gt;to_url());

// This tells curl to return the response as one string
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 

// Run curl and grab the output and the return code
// This is the execution of step 4
$return = curl_exec($ch); 
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if($http_code == 200)
{
  // If the call was good parse out the response parameters into an array
  $access_params = array();
  $param_pairs = explode('&amp;', $return);
  foreach($param_pairs as $param_pair) 
  {
    if (trim($param_pair) == '') { continue; }
    list($key, $value) = explode('=', $param_pair);
    $access_params[$key] = urldecode($value);
  }

  // Print out the final information needed for access to the service that was in &quot;scope&quot;
  echo &quot;Access info: &quot; . $access_params['oauth_token'] . &quot; and &quot; . $access_params['oauth_token_secret'] . &quot;\n&quot;;
}
else
{
  echo &quot;Error: $http_code and $return\n&quot;;
}
</pre>
<p>At this point you have a good oauth_token and oauth_token_secret that would need to be stored securely. These two bits of information allow you to make calls later to services provided under the given scope listed in the initial token request. Here is an example of making a request:</p>
<pre class="brush: php; title: ; notranslate">
require_once(&quot;OAuth.php&quot;);

// This is step 5
// 5. You use the access token to request data from Google's service access servers.

$url = 'https://www.google.com/analytics/feeds/accounts/default';

$signature_method = new OAuthSignatureMethod_HMAC_SHA1();

$params = array();

$consumer = new OAuthConsumer('anonymous', 'anonymous', NULL);

// Here the saved oauth_token and oauth_token_secret are used
$token = new OAuthConsumer($saved_oauth_token, $saved_oauth_token_secret);

$oauth_req = OAuthRequest::from_consumer_and_token($consumer, $token, 'GET', $url, $params);

$oauth_req-&gt;sign_request($signature_method, $consumer, $token);

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);

// Use the oauth request to create the authenication header
curl_setopt($ch, CURLOPT_HTTPHEADER, array($oauth_req-&gt;to_header())));

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

$return = curl_exec($ch);
$this-&gt;http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if($this-&gt;http_code == 200)
{
  echo $return;
}
else
{
  echo $return;
}

</pre>
<p>For a lot more detail check out <a href="http://code.google.com/apis/gdata/articles/oauth.html">Google&#039;s OAuth documentation</a>. I also found it helpful to use the <a href="http://googlecodesamples.com/oauth_playground/">Google&#039;s OAuth playground</a> to test things out.</p>
<p>I also found a handy list of <a href="http://code.google.com/apis/gdata/faq.html#AuthScopes">all the define Google OAuth scopes</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/programming/1443/google-oauth-for-installed-apps-php-example/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Java AirPlay Client</title>
		<link>http://www.ioncannon.net/utilities/1436/java-airplay-client/</link>
		<comments>http://www.ioncannon.net/utilities/1436/java-airplay-client/#comments</comments>
		<pubDate>Tue, 25 Jan 2011 14:36:04 +0000</pubDate>
		<dc:creator>carson</dc:creator>
				<category><![CDATA[utilities]]></category>
		<category><![CDATA[airplay]]></category>
		<category><![CDATA[java]]></category>

		<guid isPermaLink="false">http://www.ioncannon.net/?p=1436</guid>
		<description><![CDATA[Ever since getting one of the new AppleTV devices I have been wanting to fiddle with AirPlay. I finally got around to looking at a dump of the traffic between an iPad and the AppleTV over Christmas and was surprised at how simple it was. Soon after I noticed a blog post about AirFlick for [...]]]></description>
				<content:encoded><![CDATA[<p>Ever since getting one of the new AppleTV devices I have been wanting to fiddle with AirPlay. I finally got around to looking at a dump of the traffic between an iPad and the AppleTV over Christmas and was surprised at how simple it was. Soon after I noticed a blog post about <a href="http://ericasadun.com/ftp/AirPlay/">AirFlick</a> for the Mac. AirFlick was close to what I was wanting at the time but I really wanted something that would let me control AirPlay from Linux or Windows.</p>
<p>I decided to make something that could run anywhere so I created my own AirPlay client called <a href="http://www.ioncannon.net/projects/ap4j-player-java-airplay-player/">AP4J</a>. I used Java and a pure Java Bonjour implementation called <a href="http://jmdns.sourceforge.net/">JmDNS</a> so AP4J can run anywhere Java runs.</p>
<p>The current version only has the ability to control an AirPlay device. That means you have to supply a location that has a compatible video (h264 encoded) but once playing you will have control over the video just as you would using the iPad or iPhone. The next step will be to add the ability to directly serve videos instead of only being able to control the playback of videos. My goal will be the ability to run AP4J on my Windows Home Media server where I can have it stream videos to my AppleTV.</p>
<p>I have tested AP4J on Linux, Windows and Mac but only extensively on Linux. I have also tested a number of sites that have compatible videos available, a few of those are listed here:</p>
<ul>
<li><a href="http://blip.tv/">http://blip.tv/</a></li>
<li><a href="http://blip.tv/">http://www.archives.org/</a></li>
<li><a href="http://blip.tv/">http://confreaks.net/</a></li>
</ul>
<p>Now for a couple screen shots. This is what you see after starting the server and going to the web interface:</p>
<p><a href="http://www.ioncannon.net/wp-content/uploads/2011/01/ap4jmain.png"><img src="http://www.ioncannon.net/wp-content/uploads/2011/01/ap4jmain.png" alt="" title="Java AirPlay Application Main Menu" width="353" height="204" class="alignnone size-full wp-image-1431" /></a></p>
<p>This is what it looks like when a video is playing:</p>
<p><a href="http://www.ioncannon.net/wp-content/uploads/2011/01/ap4jpopup.png"><img src="http://www.ioncannon.net/wp-content/uploads/2011/01/ap4jpopup.png" alt="" title="Java AirPlay Application Play Popup" width="698" height="407" class="alignnone size-full wp-image-1432" /></a> <br/></p>
]]></content:encoded>
			<wfw:commentRss>http://www.ioncannon.net/utilities/1436/java-airplay-client/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
	</channel>
</rss>

<!-- Dynamic page generated in 0.432 seconds. -->
<!-- Cached page generated by WP-Super-Cache on 2013-06-18 18:25:31 -->
