Build Periscope in 10 Minutes

November 30th, 2015 by Chris Allen

Create_header_2

With live streaming becoming increasingly prevalent in 2015, developers are focused on creating applications to address the public’s fascination with streaming media. Periscope is the prime example of such an application and the sheer size of Periscope’s user base and class-leading engagement metrics validate its dominance in the space.

But what does it take to build a live streaming and communication platform such as Periscope, with the capability to broadcast to one hundred thousand or even one million subscribers? What if I told you that you could build a live streaming application with Periscope-like functionality and scalability in just 10 minutes?

Before we created Red5 Pro it took some serious effort to build this kind of server-side infrastructure and tackle the high level of complexity to build a native Android and iOS video encoder/decoder that works with the server. We saw this trend of a new kind of mobile app that connects people in real-time, and we saw these early adopters cobble together inefficient software wasting tons of time and energy. We couldn’t allow this to happen anymore, so we decided to make it easy for developers. With Red5 Pro, you truly have the ability to build the guts of the next live streaming phenomenon in a matter of minutes, and here’s how:

Let’s first start with all the pieces, and what you would need to build if you were to do this from scratch.

The Fundamentals

1. Publish from the mobile client:

  • Access the camera

  • Encode the video

  • Encode microphone data

  • Negotiate a connection with a media server

  • Implement a low-latency streaming protocol for transport

  • Stream the data to the server

2. Intercept with a media server

  • Intercept the stream

  • Relay to other clients

      and/or

  • Re-stream to a CDN (adds latency)

  • Record the stream (optional)

3. Implement client side subscribing:

  • HLS in WebView (even more latency)

and/or

  • Setup connection with media server

  • Implement streaming protocol

  • Mix the audio and video

  • Decode video/audio

  • Render video and play the audio

 

*Note-this is actually a simplified list of all the tasks involved. Try doing this on multiple threads and getting it to perform well; it is complicated! It’s truly a rabbit hole that most developers don’t want to venture down. Given the awesome tools and libraries that exist for us developers, we thought that it was ridiculous that an easy-to-use and extensible live streaming platform just didn’t exist. That’s why we built Red5 Pro.

 

Red5 Pro to the Rescue

Let’s uncomplicate this. The Red5 Pro Streaming SDKs provide what we think is an intuitive and flexible API to remove the complexity while retaining tremendous control if you need it. Let’s take a look at the classes our SDKs provide. (note that they are the same on Android and iOS).

Graph

Let’s step through an example using these classes, piece by piece.

The Publisher

R5Configuration:

Red5Pro_tools

The first step is to create an R5Configuration. This class holds the various data used by your streaming app. It contains things like the address of your server, the ports used, protocols, etc. In this example we are connecting to a server running at 192.168.0.1 on port 8554 via the RTSP protocol. This Red5 Pro server has an app called “live” running on it, and that is what we want to connect to based on the context name. And finally we are setting the buffer time to half a second.

iOS

 

Android

 

R5Connection:

Red5Pro_links

Next, you create an R5Connection object, passing in your configuration. This establishes a connection to the Red5 Pro media server.

 

iOS

 

Android

 

 

R5Stream:

Red5Pro_stream

Now you create a stream object passing in the connection object you just created. Note that the R5Stream is also used for incoming streams, which we will get to in a bit.

 

iOS

 

 

Android

 

R5Camera:

Red5Pro_camera

Next we create a camera object and attach it to the R5Stream as a video source.

 

iOS

 

 

Android

 

 

R5Microphone:

Red5Pro_microphone

Then we create a microphone object and attach it to the stream as an audio source.

 

iOS

 

 

Android

 

 

R5VideoView:

 

Red5Pro_view

While it’s not a requirement to publish a live stream, we find it useful to provide a preview for the user of their video being streamed from the camera. This is how you set that up.

 

iOS

 

 

Android

 

R5Stream.publish():

Red5Pro_publish

Finally the last step for the publisher is to publish the stream using a unique name that the subscriber can subscribe to.

 

iOS

 

 

Android

 

The record type parameter tells the server the recording mode to use on the server. In this example we are setting it to live, meaning it won’t record the stream on the server.

 

Here are your other choices.

R5RecordTypeLive – Stream but do not record

R5RecordTypeRecord – Stream and record the file name. Replace existing save.

R5RecordTypeAppend – Stream and append the recording to any existing save.

If you compiled and ran this app with it configured to point to a running Red5 Pro server, you would be able to see it running in your browser. Open a browser window and navigate to –> http://your_red5_pro_server_ip:5080//live/streams.jsp to see a list of active streams. Click on the flash version to subscribe to your stream.

 

LiveStreaming.png

The Subscriber

Now that we’ve built the publisher we have established a live stream being published to the server. Yes, we did see the stream in Flash, but in order to consume that stream on mobile we need to build the subscriber client. Let’s dig into that now.

 

R5Configuration:

Red5Pro_tools

Just as before, we setup a configuration object holding the details of our connection and protocols.

 

iOS

 

 

Android

 

 

R5Stream:

Red5Pro_stream

Then, like in the publisher, we set up a stream by passing in the configuration into the constructor.

 

iOS

 

 

Android

 

R5VideoView:

Red5Pro_view

This is the step where things deviate just a little from the publisher. We still set up an R5View, but this time we want to use it to display the incoming stream

 

iOS

 

 

Android

 

 

R5Stream.play():

Red5Pro_play

Finally, we tell the stream to play by using the play method and passing in the unique stream name that the publisher is using.

 

iOS

 

Android

 

Voila, you can now build your own one-to-many live streaming experience, all within minutes with the help of Red5 Pro. What do you think, are there ways we could make this even easier? We love hearing feedback, so let us know in the comments or email us directly. Happy Coding!

, , , , ,

WebRTC

May 15th, 2013 by Dominick Accattato

What is WebRTC

In the world of disruptive technologies, WebRTC has quickly caught the attention of the web development community. WebRTC at its core provides real-time communication between browsers. The following represents the group’s mission statement:

WebRTC is a free, open project that enables web browsers with Real-Time Communications (RTC) capabilities via simple Javascript APIs. The WebRTC components have been optimized to best serve this purpose.

Our mission: To enable rich, high quality, RTC applications to be developed in the browser via simple Javascript APIs and HTML5.

Why was WebRTC created

WebRTC has humble beginnings but grand expectations based on source code that Google decided to open source. The original code came from two companies: Global IP Sound (for voice) and On2 (for video). These two companies were acquired by Google for the codecs and security based protocols for peer to peer streaming technology. On2 was the source for the webm project and codec.

Why is it interesting

WebRTC is interesting for so many reasons, but for real-time streaming developers it brings full circle what we have been working on for over 10 years. Previous to WebRTC, people were streaming with either their own proprietary technology or through a browser plugin. The most ubiquitous plugin was, and still is the Flash Player. Flash still remains today the best option for web conferences that want to maintain backwards compatibility with older browsers and leverage a full stack of streaming technologies.

However, WebRTC is steadily gaining momentum and will eventually overcome the advantages that Flash currently has in this space. It will take a bit of time for the technology to increase adoption, but it will eventually happen and many of the leading browser vendors are behind the movement. Especially since the standards are supported by the W3C and IETF working groups.

What are the current challenges?

Currently WebRTC has some challenges. First, it still needs wider acceptance and adoption. At this moment, only Chrome (Stable) and Firefox (Nightly) have support. Internet Explorer has expressed interest, but Safari has not made any indication that they would provide support. That said, if all the other major vendors end up supporting the standard, Safari would most likely follow suit.

Also, since the technology is largely peer to peer, there isn’t a great solution for a media server yet. In addition, the technology requires implementors install either a STUN or TURN server. A STUN server basically facilitates “hole punching” which is what is needed for NAT traversal through firewalls. A TURN server is basically a STUN server with extensions that allow it to also act as a fallback media relay server. Regardless, it’s still difficult to choose the right STUN/TURN server to work with, but I’m sure this will become more clear as the standards and implementors start to roll out more products.

In addition, there is still much work to be done on the specifications. The standards boards are continuing their efforts on the creation of their working drafts. These will eventually be published standards and RFC’s.

What are the main API Interfaces?

You can visit the following site for a good description of the API’s (http://docs.webplatform.org/wiki/apis/webrtc)

What about Flash Streaming?

I’ve thought about how this will affect the current ecosystem of Flash Streaming which basically dominates the video streaming on the Internet today. As Flash has a large adoption rate, it will continue to thrive and will even remain as a great backward compatible solution. At this point, I still feel like the Actionscript API’s are easier to work with and the aggregated technology behind Flash Streaming appears to be easier to work with, but that is a biased statement since I’ve been working with Flash Streaming for over 10 years.

Many groups are also still very interested in how WebRTC will affect Red5. I can only say that at this time, the Red5 developers including myself, are excited about the potential of WebRTC, and we plan to modernize Red5 to accommodate this new plugin-less approach.

Conclusion

So I hope I’ve drawn some attention to this very new and exciting technology. We at Infrared5 hope to put this technology to use for our clients. If you’re interested in a project based on WebRTC, just drop us a message.

More Information

Project Website: http://www.webrtc.org/

Google Code Project: https://code.google.com/p/webrtc/

WebRTC Blog: http://www.webrtc.org/blog

W3C Editor’s Draft: http://dev.w3.org/2011/webrtc/editor/webrtc.html

WebRTC Example: https://apprtc.appspot.com/?r=65920333

, , ,

Red5 1.0.1 Released

January 15th, 2013 by Paul Gregoire

Red5 1.0.1 Released

Announcing the release of Red5 server version 1.0.1 Final.
This is primarily a bug-fix release, coming just a little over a month after the 1.0 release. The following 13 bugfixes are addressed in this release:

  • Admin application fixed
  • ClientBroadcastStream was modified to handle stream listeners more efficiently
  • Fixed RTMPS server freeze
  • Plugged memory leak in the RTMP protocol decoder
  • Fixed several minor issues with client streams
  • Addressed tight-loop issue bubbled up from Mina when file handles are exceeded
  • Fixed loading issue with persistent shared objects
  • Repaired some Maven related problems with our builds

A major change from 1.0 on the project side of things is our move from Ant and Ivy based builds to Maven.

The server documentation has also received some updates and has been regenerated in HTML
and PDF formats; granted there are still a lot of sections that need to be updated, but this is a start. The googlecode hosted wiki has also been started during this period since we lost faith in the older Trac system. The unfortunate thing about losing Trac is that we lost a lot of the content.

Red5 Binaries

Binaries have been created to fill most of our users needs; absent from the list is a dedicated OSX build. OSX and Linux users should grab the Tarball or build from source. The following links are now active: Windows Installer (Java 7) | Windows (Java 6) | ZIP | Tarball

How to add Red5 as a Maven dependency in your project

Add the red5 repository

Add a dependency entry for the server:

Also if you’re doing RTMP client work in your project, you’ll also have to add the client library entry:

For both the server and client libraries you must add the “-java6″ prefix to the “version” node if you are not using Java 7.

Building Red5 in Eclipse

A Maven plugin now comes pre-installed in some Eclipse releases, but if you are using an older version and need install instructions go here first: Maven plugin install

Last but not least, I created a quick screencast to show how to build the server in Eclipse now that we’ve switched to Maven.

, , ,

Red5 Authentication

May 7th, 2012 by Paul Gregoire

How to implement CRAM authentication in Red5

In this post we will setup a challenge-response authentication mechanism (CRAM) in a Red5 application using two different methods; the first one being very simple and the other utilizing the powerful Spring security libraries. A basic challenge-response process works like so:

  • Client requests a session
  • Server generates a unique, random ChallengeString (e.g. salt, guid) as well as a SessionID and sends both to client
  • Client gets UserID and Password from UI. Hashes the password once and call it PasswordHash. Then combines PasswordHash with the random string received from server in step 2, and hashes them together again, call this ResponseString
  • Client sends the server UserID, ResponseString and SessionID
  • Server looks up users stored PasswordHash based on UserID, and the original ChallengeString based on SessionID. Then computes the ResponseHash by hashing the PasswordHash and ChallengeString. If its equal to the ResponseString sent by user, then authentication succeeds.

Before we proceed further, it is assumed that you are somewhat familiar with Red5 applications and the Flex SDK. For those who would like a quick set of screencasts to get up-to-speed, we offer the following:

Implementation

Implementing a security mechanism is as simple as adding an application lifecycle listener to your application. Red5 supports a couple types of CRAM authentication via an available auth plugin. The first one implements the FMS authentication routine and the other one is a custom routine developed by the Red5 team. In this post we will use the Red5 routine. An ApplicationLifecycle class implementing the Red5 routine, may be found in the Red5 source repository; this code only validates against the password “test”. While this class would not be useful in production, it may certainly be used as a starting point for a real implementation. Red5AuthenticationHandler Source

To enable the usage of your Red5AuthenticationHandler class or any other ApplicationLifecycle type class for that matter, you must add it to the listeners in your Application’s appStart method.

The reason for putting it in the appStart method is to ensure that the handler is added when your application starts and before it starts accepting connections. There is no other code to add to your application adapter at this point since the lifecycle methods will fire in your handler. Putting the authentication code within a lifecycle handler serves to keep the adapter code cleaner and less confusing. The authentication handler is defined in the red5-web.xml like so:


At this point, your application would require authentication before a connection would be allowed to proceed beyond “connect”. Entering any user name and the password of “test” or “password” (depends on class used in demo) would allow a client to be connected. As stated previously, this first “simple” implementation is not meant for production but is offered as a means to understand the mechanism at work.

Adding Spring Security

Once we add security layers such as Spring security, the application and authentication features become much more robust. The first thing we must do is to add the Spring Security namespace to our applications red5-web.xml.

Replace this node:

With this node:


Add the authentication manager bean and configure it to use a plain text file. The users file contains our users, passwords, and their credentials.

To demonstrate how users are handled, we will create three users: 1 admin, 1 regular user, and 1 user without a role. The plain text users file follows this pattern: username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] A user can have more than one role specified; granted authority and role are synonymous. Below are the contents of our users file for this demo.


In addition to the authentication handler, a authentication manager must be added when using Spring security. The manager should be added to the appStart method prior to adding the handler, as shown below.

The Spring security version of the authentication handler will replace the simple version in your red5-web.xml like so:


Lastly, an aggregated user details service is used for storage and look ups of user details; this is essentially an interface to the internal in-memory datastore holding the user details or credentials. The user details may be configured to retrieve details from our local properties file, databases, ldap, or active directory. Our aggregated service is fairly simple as you can see below.

It should be noted that Spring security makes use of an additional Spring framework library that is not included in Red5; the transaction library provides DAO and transaction implementations which do not require an external database or related dependencies. All the libraries required for the demo are included in the project zip file.

Client code

Creation of an authentication enabled client will require a single library not included in Flex / Flash builder called as3crypto. The AS3 cryptography library will provide the hashing functions nessasary for authentication in our demo. Download the as3crypto.swc from: http://code.google.com/p/as3crypto/ and place it in the “libs” folder of our client project.

The following functions will be needed in your client support authentication:

The sendCreds method is called “later” from the sendCredentials method to prevent issues in the event thread.

These are the imports that need to be added before beginning.

In your connect function you will need to determine which authentication mode to employ. The following block will show how to set up the connect call based on the type selected.

You may notice that the type is simply a string in the url denoting which mode to use.

In your net status event handler, you will need to add handling for authentication routines. The following block demonstrates how to do so when an NetConnection.Connect.Rejected is received.

Once you are successfully connected, there are two methods in the demo code for testing access. The helloAdmin method will return a positive result if the authenticated user has the admin permission. In the helloUser method the routine is similar, but only the user permission is required. The included users file contains an admin user and two regular users, the second user is set up to have no permissions. The user without permissions may only connect application and call unprotected methods.

Protected methods in the application contain an isAuthorized check against preselected permissions.

If the user does not qualify, the call fails.

In a future post, I will explain how to add Java Authentication and Authorization Service (JAAS) to Red5.

Download
Project Source Client and server

, , , , , , , , , ,

Red5 Vector Support

August 3rd, 2011 by Paul Gregoire

It all started for me last year, I noticed Jean-Philippe Auclair blogged about reading AS3 Vector’s in Java and provided byte-level examples. Since the blogged examples were written in Java, I saw this as an invitation to implement them in Red5. I knew of Vector’s in Java but had no idea they had been implemented in actionscript and with what looked to me like generics. In AS3 the generic notation denotes the base type for each element in the Vector, which is also how it works in Java. The main differences between the implementations are that a Vector in AS3 is basically a faster version of Array and in Java it is a synchronized List-type collection. Similarly each element must be either “null” or an instance of the base type. Java is a lot more flexible with the instances in this case, because the element merely needs to implement or extend the base type. In AS3 handling is different, a Vector of DisplayObject type will not accept a Sprite instance.

Before I get down to the byte-level stuff, I want to note that this information may not be exactly correct in terms of what or how these items are constructed by the flash player; they are however the result of reverse engineering the data. Anyone with more information or a correction is welcome to send it my way.

The following examples show how the Vector is constructed in AS3 and then its representation in bytes. The byte arrays are encoded as big endian and are not compressed.

Vector with numbers and a string

A Vector containing two Number objects and a simple repeating string:

Trace:

Raw bytes:

Vector inside a Vector with other objects

A Vector containing the previous examples Vector in addition to another Vector of int type:

Trace:

Raw bytes:

Mixed Vector with class instance member

A Vector containing a simple String, a “null”, and the instance of a custom class:

Trace:

Raw bytes:

The “Foo3″ classes

Actionscript

Java

The original post does not contain details for writing back to Flash Player so I simply reversed the process once I had read working in the server. To test round-trip (deserialization/serialization), I used this method in my Red5 application:

The method accepts a Vector containing any type of object and returns it back to the client as a response. On the client you’ll need a pair of methods to both hit the server and to get the response.

Lastly, to use Vector’s with Red5 you will need the latest trunk with a revision of 4264 or newer.

For your reference, the AS3 doc page for Vector may be found here: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/Vector.html
Note: Vector’s are available to Flash Player starting with the version 10 beta.

, , , ,

Clustering with Red5

July 28th, 2011 by Paul Gregoire

Once your service out grows its original space, you will most likely look at adding clustering capabilities. While clustering an application may seem daunting, it can be done in a fairly simple manner at first. Whilst at Infrared5, I have created several clustered applications using the methods which will be supplied below and now may be added to your skillset.

For this example you will need the following software (IDE’s not listed):

Herein we will go over clustering a simple chat application; both the client and server code in this example consist of only two to three source files respectively. Code blocks may not contain all of the code or comments to keep the sections short and focused.

Server-side (Java)

The first piece of code we need is the Red5 application adapter, it is used by the server to control the lifecycle of your application. In many cases, the base classes may be configured via Spring so that you don’t even need to write your own for simple applications; as an example, the SOSample demo that comes with the server doesn’t have any code for an application adapter. For this application we will override two lifecycle methods to handle the starting and stopping of the application. When the application starts, we need to start our cluster client class and join the cluster. On stop we disconnect from the cluster and perform any clean up necessary.

We must also add any methods which will be called by the client, which in this case would be one that accepts a sender name and some text.

Pretty easy looking isn’t it? Being a standard Java method, it could accept these values from anywhere that has access to the method such as a servlet. Next up, on the server-side is the code to send and receive messages on the cluster. This “ChatRegistry” extends the ReceiverAdapter provided by JGroups and is implemented in the most minimal way I could think of.

You may have noticed we’ll be using UDP for this example, I have found that this is the easiest way to setup JGroups and keep all the nodes communicating on a local network.

Last but not least for the server application we have our model class to hold a creation time, sender name, and the chat text itself.

The Actionscript twin for this model will be shown in the next section.

Client-side (ActionScript)

I used FlashBuilder to whip-up a quick chat client, so don’t bash my code too hard. For the example clients I created a Flex Project with MXML and one AS3 class file. In the MXML script block besides the connection handling, we need a method for sending and another to receive.

Be aware that the NetConnection.call in the send function must match the signature on the server’s application adapter.
The final class needed on the client is our model, which in Actionscript is a lot less “wordy” than in Java.

I have not included the configuration files and some other things in this post to prevent confusion, but rest assured they are in the source archives at the end.

Running

The quickest way to get up-and-running is to grab the server zip and deploy it to two servers or vmware virtual machines. Everything you need to run the client and server is in the zip.  After Red5 has started up, look at your console and you should see a line indicating that your application is in the cluster.

Please note that If you are using OSX you may need to add “-Djava.net.preferIPv4Stack=true” to your start-up script due to a bug in JGroups IPv6 handling.

Open browsers on each instance and navigate to http://localhost:5080/chatr/chatr.html

Once loaded, enter a name and click connect. After you have two servers running and clients connected to them, your output should look similar to this when chatting.

So now you know how easy this can be, go forth and create some cool stuff!!

Downloads

Full application with Red5 server (ready-to-run): http://bit.ly/pIoVBi

Client source: http://dl.dropbox.com/u/7316897/red5/chatrclient-src.zip

Server source: http://dl.dropbox.com/u/7316897/red5/chatr-src.zip

, , , , ,

The Evolution of Infrared5

June 21st, 2011 by Keith Peters

I joined Infrared5 back in November 2007. Those were very different times. We were a hard core Flash shop, focusing on Red5 Server based applications and Papervision3D. The iPhone had been out for less than six months and only Apple could write apps for it. The iPod Touch was just a few weeks old. Nobody had heard of Android. Tablets were just a failed venture by Microsoft that most people had forgotten about a few years before. Nobody was particularly excited about HTML (5 or otherwise) or JavaScript. If there was any perceived threat to Flash at the time, it might have been Silverlight, but nobody was particularly worried about that.

Now, the landscape is very different. I’m not going to say Flash is dead. I don’t think it is. I don’t even think that it is dying, per se. What is happening though, is that there are so many other cool and interesting things out there now, that Flash has lost its place in the spotlight for many developers. Also, I think that Flash initially had a very low learning curve and very little barrier to entry. A lot of Flash developers grew up as Flash did, learned real programming, object orientation, design patterns, best practices, etc., and were then able to branch out to other languages and platforms.

I have to say, that Infrared5 has not only rolled with the changes very well, but has completely embraced the change. I think virtually all of our front end developers are now seasoned iOS developers. Several have embraced Android development as well. We have Windows Phone 7 knowledge (mostly me), and our 3D platform has moved from Papervision to Unity. We’re doing HTML5 stuff as well as Flash and Flex sites, iPad apps, kiosk applications. Many of our projects even span multiple platforms – a Flex 4 app with an HTML5 public facing site, Flash or Unity 3D games with a companion iPhone app via Brass Monkey.

The company’s tag line is “Yeah, we can build that.” I’d say we’ve lived up to that.

In closing, I ran across this quote the other day that I really loved. It comes from a free on line book, “Learn Python the Hard Way”, by Zed A. Shaw, which you can find here: http://learnpythonthehardway.org/ . In the last section called “Advice From An Old Programmer”, he says:

“What I discovered after this journey of learning is that the languages did not matter, it’s what you do with them. Actually, I always knew that, but I’d get distracted by the languages and forget it periodically. Now I never forget it, and neither should you.

Which programming language you learn and use does not matter. Do not get sucked into the religion surrounding programming languages as that will only blind you to their true purpose of being your tool for doing interesting things.

Programming as an intellectual activity is the only art form that allows you to create interactive art. You can create projects that other people can play with, and you can talk to them indirectly. No other art form is quite this interactive. Movies flow to the audience in one direction. Paintings do not move. Code goes both ways.”

The full quote is here: http://learnpythonthehardway.org/book/advice.html

, , , , , , ,

Red5 – Past, Present & Future

August 19th, 2010 by Dominick Accattato

Red5 is not Infrared5. Many have confused the two or assume it is one entity. Infrared5 was started by some of the original Red5 team, and does focus many services on Red5 development. However, that is where it ends.

Red5 is an open source media server that delivers live video/audio/data to a client application. In most cases that client happens to be the Flash Player, however there isn’t anything stopping a keen developer from streaming to other endpoints, i.e. Java, Silverlight, HTML5. Since the server is licensed under the LGPL, companies have the liberty to use Red5 in proprietary products. The main restriction placed on Red5 is that any modification to the original source code must be donated back to the project. This ensures that the project continues to thrive with patches and helps us deliver a more stable product to the community.
Read the rest of this entry »

, , , , ,

Screaming with Ozzy

July 7th, 2010 by Mike Oldham

We recently collaborated with Sony Music to develop an engaging and unique experience to promote Ozzy Osbourne’s latest album Scream. The application challenges users to belt out there best scream using their webcam. The front-end was built using Flash and the Flex framework, and relied on Red5 for recording videos from a webcam and submitting the videos via the YouTube API. The most popular videos on the YouTube channel are prominently featured in a 3D wall on the website. This was yet another interesting project to build and test in the office. You’d think a team of engineers would be a little more introverted, guess again.
Read the rest of this entry »

, , , , , ,