Life, the Universe and Everything
Back to mainpage

Writing Hangout Apps


Several weeks ago the Google+™ Hangouts API has been moved out of preview. After helping out in a Hangout API Dev Hangout and holding a Hangout API talk at a GTUG meeting I thought it would be nice to put all the basic information together in an article, to help others start with developing Hangout apps, which actually is quite easy.

Introduction

To quote from the official documentation: "The Google+ Hangouts API allows you to develop collaborative apps that run inside of a Google+ Hangout. Hangout apps behave much like normal web apps, but with the addition of the rich, real-time functionality provided by the Hangouts APIs."

The goal of this article is to get you started with a basic app from which you can develop your own apps.

Some notes

All the information in here has been assembled from the official documentation as well as from personal experiments and experience. While the official documentation does a good job in getting you started I thought it would be useful to get the information presented in a different way, showing the more practical aspects of getting you right into Hangout App development.


I'm gonna assume some basic javascript and HTML knowledge. For the debugging parts I'm also going to assume that your are using a Chrome browser.


You can find the full sources for the simple app we are going to build on GitHub and on Google Code.

1 - Defining your App

The first step in creating an app is to create a Gadget-XML file. The general structure of a Gagdet-XML file looks like this:


<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Name">Name of the Gadget/App
...configuration options
</ModulePrefs>
<Content type="html">
<![CDATA[
...HTML Body of the Gadget/App
]]>
</Content>
</Module>


For the special case of Hangout Apps your Gadget-XML will usually look like this:


<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hangout Demo">Name of your Hangout App
<Require feature="rpc"/>Necessary features to allow communication
<Require feature="views"/>between your App and the Hangout
</ModulePrefs>
<Content type="html">
<![CDATA[
<script src="//talkgadget.google.com/hangouts/api/hangout.js?v=1.0"></script>Hangout API
<link rel="stylesheet" href="<YOUR_PATH>/css/demo.css" />your CSS styling
<script src="<PATH1>/library1.js"></script>possibly links to external libraries like jQuery
<script src="<PATH2>/library2.js"></script>.
....
<h1>Hangout Demo</h1>HTML elements
<div id="container"></div>.
....
<script src="<YOUR_PATH>/scripts/demo.js"></script>your javascript file
]]>
</Content>
</Module>


For the simple demo app we are going to build we won't be needing any external libraries (except for the Hangout API obviously) and keep the HTML simple with a header and an empty container we are going to fill dynamically.

We are also linking to a css file for some basic styling. You can take this one or create an empty file instead.

All the work will be done in the linked demo.js file (see below).

You will have to replace <YOUR_PATH> with the location where you are planning to upload the files.


<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hangout Demo">
<Require feature="rpc"/>
<Require feature="views"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<script src="//talkgadget.google.com/hangouts/api/hangout.js?v=1.0"></script>
<link rel="stylesheet" href="<YOUR_PATH>/css/demo.css" />
<h1>Hangout Demo</h1>
<div id="container"></div>
<script src="<YOUR_PATH>/scripts/demo.js"></script>
]]>
</Content>
</Module>


For the demo.js for now we are only going to provide a base which doesn't really do anything yet except for writing an entry to the console so we know that something is happening at all. We will fill in the real functionality as we progress.


(function (window) {
"use strict";
function HangoutDemo() {
console.log("Starting...");
}
var hangoutDemo = new HangoutDemo();
}(window));

(If you are wondering about the (function (window) {}(window)) construct you might want to read this article which offers a short introduction to this topic. One of the main ideas is to keep all the variables we define in our scope without cluttering the global object.)

2 - Running your App

First you will need to upload your app to a web server and make sure that the paths in the xml file are set correctly.
Then it's time to visit the Google API Console.

Extensions vs. Full Apps

A full Hangout app would take up the whole area that would normally be used for the main video stream. The minimum dimensions you have to work with would be 460px x 700px.

Hangout App dimensions

An extension in contrast will always have a fixed width of 300px and be displayed on the left side of the main video. The minimum height you have to work with would be 240px in this case.

Hangout Extension dimensions

In general it's a good idea to allow for dynamic adjustments of your hangout app layout depending on the current available area.


An app and extension could be running at the same time, but there is no internal way to let them communicate with each other.

3 - Testing and debugging your App

General notes

Debugging

For debugging the Chrome JavaScript console (or equivalent tools in other browsers) is your best friend.

When you open the console (with CTRL-SHIFT-J) and switch to a full console you will see that the "Starting..." from our console.log in the demo.js was already called, so we know that everything's working correctly so far.

You can ignore most of the random errors that sometimes appear inside of hangouts as long as they don't come from demo.js of course.

Debugging 1

To continue debugging we first have to switch the console to the correct context. The context we are looking for is usually at the bottom of the list the __gadget_x (ifr).

Debugging 2

Once in the correct context we can have a look at the calls the Hangout API allows by looking at the gapi.hangout object. As you can see there are quite a few functions available in there, and we will be looking at some of them below.

Debugging 3

4 - Is the API ready for us?

Before we can start using the API we have to make sure everything is loaded completely. The API allows us to add a callback for this gapi.hangout.onApiReady event.

The callback will get a event Object with a property isApiReady which will be true if everything's working correctly.


(function (window) {
"use strict";
function HangoutDemo() {
console.log("Starting...");
gapi.hangout.onApiReady.add(this.onApiReady.bind(this));// Add callback
}
HangoutDemo.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
console.log("API Ready");
// we can start doing stuff here
}
};
var hangoutDemo = new HangoutDemo();
}(window));

(If you are wondering about the .bind(this) you might want to read this article which explains the concept in detail. The simplest explanation would be that by binding the function to our object we make sure that calling this inside of the function will refer to our object, when the function is triggered by the event.)


After uploading the new demo.js and reloading the app you should be seeing this in the console now:

Debugging 4

5 - Accessing participants

The first thing we will look at is how to see the participants of the current hangout.

For this you can call gapi.hangout.getParticipants() in the console which will give you an array of all participant objects (two in the screenshot below).

Debugging 5

The important thing to note is that the person Object which includes basic Google profile information is (for now) only available when the participant has actually started the app.


Here are some of the more interesting properties. (full documentation)

hasAppEnabled: trueThe participant has the app enabled
id: "hangout123_ephemeral.id.google.com^xyz"Internal participant ID
can be used to access data via other API functions
person:Basic profile information
only available when participant is running the app
displayName: "John Smith"
id: "123456789012345678901"Google ID, can be used to request data via other Google APIs
image:
url: "(...)/photo.jpg"


You can also use gapi.hangout.getEnabledParticipants() which will return an array of only those participants who are actually running the app.


What we want to do now is to display a list of current participants in our Hangout app, for this we will extend our demo.js a little bit.


(function (window) {
"use strict";
function HangoutDemo() {
console.log("Starting...");
gapi.hangout.onApiReady.add(this.onApiReady.bind(this));
}
HangoutDemo.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
console.log("API Ready");
this.displayParticipants();
}
};
HangoutDemo.prototype.displayParticipants = function () {
var div, participants, ul, li, i, l;
participants = gapi.hangout.getParticipants();// Get array of participants from API
ul = document.createElement("ul");
l = participants.length;
for (i = 0; i < l; i++) {
li = document.createElement("li");
if (participants[i].person) {
li.innerHTML = participants[i].person.displayName;// Add name to list if available
} else {
li.innerHTML = "unknown";
}
ul.appendChild(li);
}
div = document.getElementById("container");
div.appendChild(ul);
};
var hangoutDemo = new HangoutDemo();
}(window));

After uploading and reloading the app you should now see a list of participants of the hangout, with names for the people who are using the app, and "unknown" for the others.

Participants

The problem we are having now is that this list is only displayed once when the app is started, but people are coming and leaving in Hangouts all the time, and some of the participants might start using the app. Luckily the API offers the gapi.hangout.onParticipantsChanged event which we can use to refresh out list whenever necessary with only a few lines of extra code.


(function (window) {
"use strict";
function HangoutDemo() {
console.log("Starting...");
gapi.hangout.onApiReady.add(this.onApiReady.bind(this));
}
HangoutDemo.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
console.log("API Ready");
gapi.hangout.onParticipantsChanged.add(// add callback for event
this.onParticipantsChanged.bind(this)
);
this.displayParticipants();
}
};
HangoutDemo.prototype.onParticipantsChanged = function (event) {
var div = document.getElementById("container");
div.innerHTML = "";// make sure our container is empty before displaying the list
this.displayParticipants();
};
HangoutDemo.prototype.displayParticipants = function () {
var div, participants, ul, li, i, l;
participants = gapi.hangout.getParticipants();
ul = document.createElement("ul");
l = participants.length;
for (i = 0; i < l; i++) {
li = document.createElement("li");
if (participants[i].person) {
li.innerHTML = participants[i].person.displayName;
} else {
li.innerHTML = "unknown";
}
ul.appendChild(li);
}
div = document.getElementById("container");
div.appendChild(ul);
};
var hangoutDemo = new HangoutDemo();
}(window));

6 - The Shared State

For communication between the different client-side sessions of a hangout app you can use the shared state which can be accessed via the API. You can save only string values inside of the shared state, so you will have to make sure to explicitely convert any non-string values.

You can play around with the shared state in the JavaScript console with the following functions.

Shared State

As you can see it's very easy to alter the shared state from the JavaScript console which could potentially be abused by hangout participants, so that's something you have to consider when writing apps that have important information inside of the shared state.


To add some shared state functionality to our hangout we're first going to change our xml file to include a button which can be clicked to update the shared state and a place to display a current value of the shared state.

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hangout Demo">
<Require feature="rpc"/>
<Require feature="views"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<script src="//talkgadget.google.com/hangouts/api/hangout.js?v=1.0"></script>
<link rel="stylesheet" href="<YOUR_PATH>/css/demo.css" />
<h1>Hangout Demo</h1>
<div id="container"></div>
<button id="clickme">Click Me</button>
<span id="count">0</span>
<script src="<YOUR_PATH>/scripts/demo.js"></script>
]]>
</Content>
</Module>


In our demo.js we need to add some code to update the state when the button is clicked.


(function (window) {
"use strict";
function HangoutDemo() {
console.log("Starting...");
gapi.hangout.onApiReady.add(this.onApiReady.bind(this));
}
HangoutDemo.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
console.log("API Ready");
gapi.hangout.onParticipantsChanged.add(
this.onParticipantsChanged.bind(this)
);
document.getElementById("clickme").onclick = // callback for button-click
this.buttonClick.bind(this);
this.displayParticipants();
}
};
HangoutDemo.prototype.buttonClick = function () {
var value = gapi.hangout.data.getValue("count") || "0";// read current count from state
value = (parseInt(value, 10) + 1).toString();// increment count by one
gapi.hangout.data.setValue("count", value);// write new count into state
};
HangoutDemo.prototype.onParticipantsChanged = function (event) {
var div = document.getElementById("container");
div.innerHTML = "";
this.displayParticipants();
};
HangoutDemo.prototype.displayParticipants = function () {
var div, participants, ul, li, i, l;
participants = gapi.hangout.getParticipants();
ul = document.createElement("ul");
l = participants.length;
for (i = 0; i < l; i++) {
li = document.createElement("li");
if (participants[i].person) {
li.innerHTML = participants[i].person.displayName;
} else {
li.innerHTML = "unknown";
}
ul.appendChild(li);
}
div = document.getElementById("container");
div.appendChild(ul);
};
var hangoutDemo = new HangoutDemo();
}(window));

After uploading the new code and reloading the app you can press the button several times and then use gapi.hangout.data.getState() or gapi.hangout.data.getValue("count") to see if something has actually changed.


Shared State

What we need to add now is a callback for the gapi.hangout.data.onStateChanged event to update our display accordingly. And to display the current count to new participants right away we also call our display function once when the app starts.


(function (window) {
"use strict";
function HangoutDemo() {
console.log("Starting...");
gapi.hangout.onApiReady.add(this.onApiReady.bind(this));
}
HangoutDemo.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
console.log("API Ready");
gapi.hangout.onParticipantsChanged.add(
this.onParticipantsChanged.bind(this)
);
document.getElementById("clickme").onclick =
this.buttonClick.bind(this);
gapi.hangout.data.onStateChanged.add(// add callback for event
this.displayCount.bind(this)
);
this.displayCount();
this.displayParticipants();
}
};
HangoutDemo.prototype.displayCount = function () {
var value = gapi.hangout.data.getValue("count") || "0";// read current count from state
document.getElementById("count").innerHTML = value;// display current count
};
HangoutDemo.prototype.buttonClick = function () {
var value = gapi.hangout.data.getValue("count") || "0";
value = (parseInt(value, 10) + 1).toString();
gapi.hangout.data.setValue("count", value);
};
HangoutDemo.prototype.onParticipantsChanged = function (event) {
var div = document.getElementById("container");
div.innerHTML = "";
this.displayParticipants();
};
HangoutDemo.prototype.displayParticipants = function () {
var div, participants, ul, li, i, l;
participants = gapi.hangout.getParticipants();
ul = document.createElement("ul");
l = participants.length;
for (i = 0; i < l; i++) {
li = document.createElement("li");
if (participants[i].person) {
li.innerHTML = participants[i].person.displayName;
} else {
li.innerHTML = "unknown";
}
ul.appendChild(li);
}
div = document.getElementById("container");
div.appendChild(ul);
};
var hangoutDemo = new HangoutDemo();
}(window));

With these changes whenever someone clicks the button, the display will be updated for all participants using the app.

Shared State

Some additional notes about the shared state

Conclusion

This should give you a basic idea of how hangout apps work. There's a lot more you can do with the API, so make sure to have a look through the official documentation. Basically you can do everything you can do in a web-application with HTML, CSS and JavaScript inside of a hangout as well. This includes calls to external services if cross-domain policies allow it. So I hope you found this article interesting and will start building some amazing Hangout apps :)

References

Official Documentation: developers.google.com/+/hangouts/
Several code samples by myself: github.com/Scarygami/gplus-experiments or code.google.com/p/gplus-experiments/
Directory of hangout apps: hangoutapps.com



Google+ is a trademark of Google Inc. Use of this trademark is subject to Google Permissions.
This site is not affiliated with, sponsored by, or endorsed by Google Inc.


Back to mainpage

This work is licensed under a Creative Commons Attribution 3.0 Unported License.