ClarkRasmussen.com

Welcome

Welcome to the personal home page of web developer Clark Rasmussen. I'm trying to blog more than I used to but in addition to that, this is where you can find my portfolio (now part of the blog), resumé, and a handful of other things. Look up in the header for ways to find me on social media.

My Blog

Retro Code Sample: TSC Interact! Hangout Video Controller

I Tweeted on Friday about futzing with Google Hangouts, something I hadn’t had to deal with in years.  The link I sent out went to a blog post about lessons learned from that experience but I realized that I never bothered to actually write up the code I wrote in that project and figured it might help some people.

The issue I was trying to solve was that I was working on a team that was trying to incorporate a remote employee and a handful of people that sometimes worked from home.  In the office, the team was split across two spaces that were next to each other.  Each space was given a TV, webcam, and microphone.  A problem immediately became apparent when the two local stations started causing feedback between each other.  Someone solved this by turning off the speakers and microphone on one of the stations, which left that room unable to easily communicate with people in the hangout.

To solve this, I took advantage of the fact that the two stations were logged in as the same user and wrote a Hangouts app to run on those machines.  The app looks for another person logged in as the same user and mutes them, eliminating feedback.  It also blocks that station from appearing on video if another user is available to be seen.

As per my usual, I’ll start with the big block of code, then break it down in small chunks.

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
  <ModulePrefs title="TSC Interact! Hangout Video Controller">
    <Require feature="rpc"/>
  </ModulePrefs>
  <Content type="html"><![CDATA[
<script src="//plus.google.com/hangouts/_/api/v1/hangout.js"></script>
<script>
  var my_id = 0;
  var my_hangout_id = 0;

  function init() {
    // When API is ready...
    gapi.hangout.onApiReady.add(function (eventObj) {
      if (eventObj.isApiReady) {
        my_id = gapi.hangout.getLocalParticipant().person.id;
        my_hangout_id = gapi.hangout.getLocalParticipant().id;

        update_video_options();
        gapi.hangout.onParticipantsChanged.add(function () {
          update_video_options();
        });
      }
    });
  }

  function update_video_options () {
    var participants = gapi.hangout.getParticipants();

    for (var n = 0; n < participants.length; n++) {
      if (participants[n].person.id == my_id) {
        gapi.hangout.av.setParticipantAudible(participants[n].id, false);
        if ((participants.length != 2) && (participants[n].id != my_hangout_id)) {
          gapi.hangout.av.setParticipantVisible(participants[n].id, false);
          gapi.hangout.av.setAvatar(participants[n].id, 'http://fakeurl.com/logo.png');
        }
      }

      if (participants.length == 2) {
        gapi.hangout.av.setParticipantVisible(participants[n].id, true);
      }
    }

    var feed = gapi.hangout.layout.getDefaultVideoFeed();
    if ((gapi.hangout.getParticipantById(feed.getDisplayedParticipant()).person.id == my_id) && (participants.length == 3)) {
      // just in case we get stuck on a blocked user's feed, force switch to the other person
      for (var n = 0; n < participants.length; n++) {
        if (participants[n].person.id != my_id) {
          feed.setDisplayedParticipant(participants[n].id);
          n = participants.length;
        }
      }
    } else {
      feed.clearDisplayedParticipant();
    }
  }

  gadgets.util.registerOnLoadHandler(init);
</script>
]]>
  </Content>
</Module>

The whole thing is JavaScript wrapped in an XML container.  I’ll ignore the XML because it’s pretty standard, aside from the title attribute of the ModulePrefs element being “TSC Interact! Hangout Video Controller” (TSC being the company abbreviation, Interact! being the internal team name).

<script src="//plus.google.com/hangouts/_/api/v1/hangout.js"></script>
<script>
  var my_id = 0;
  var my_hangout_id = 0;

We start by pulling in the Hangouts API JS from Google, then we open our own script block. The first thing we do is define my_id as 0 and my_hangout_id as 0. We’ll store the user’s ID and the Hangout ID (which is the user/machine’s unique connection to a Hangout) in these spots later.

function init() {
  // When API is ready...
  gapi.hangout.onApiReady.add(function (eventObj) {
    if (eventObj.isApiReady) {
      my_id = gapi.hangout.getLocalParticipant().person.id;
      my_hangout_id = gapi.hangout.getLocalParticipant().id;

      update_video_options();
      gapi.hangout.onParticipantsChanged.add(function () {
        update_video_options();
      });
    }
  });
}

We define an init() function that we’ll fire off later. In it, we use gapi.hangout.onApiReady to attach this code to the event of the API being loaded and ready for us to use. If eventObj.isApiReady is true (and it should be, because this should only be fired if the API is ready, but I went off some sample code that included this), we can do some stuff.

“Some stuff” is setting my_id to gapi.hangout.getLocalParticipant().person.id and my_hangout_id to gapi.hangout.getLocalParticipant().id. Then we fire off the update_video_options() function and set a listener on gapi.hangout.onParticipantsChanged to run that function again each time someone enters or leaves the Hangout.

function update_video_options () {
  var participants = gapi.hangout.getParticipants();

  for (var n = 0; n < participants.length; n++) {
    if (participants[n].person.id == my_id) {
      gapi.hangout.av.setParticipantAudible(participants[n].id, false);
      if ((participants.length != 2) && (participants[n].id != my_hangout_id)) {
        gapi.hangout.av.setParticipantVisible(participants[n].id, false);
        gapi.hangout.av.setAvatar(participants[n].id, 'http://fakeurl.com/logo.png');
      }
    }

    if (participants.length == 2) {
      gapi.hangout.av.setParticipantVisible(participants[n].id, true);
    }
  }

Here’s the bread and butter, the update_video_options() function. We start by using gapi.hangout.getParticipants() to get the list of Hangout participants, then we loop through it to find users with the same ID as the machine this is running on.

If the user is the same, we use gapi.hangout.av.setParticipantAudible() to mute them by passing in the user ID and false. If there are not only two users in the Hangout (which means someone other than the two local machines is logged in) and the user we’re looping through is not the machine this is running on, we also hide the user and reset their avatar. Hiding the user, much like muting them, is done by calling gapi.hangout.av.setParticipantVisible() and passing in the user ID and false. To change the avatar we call gapi.hangout.av.setAvatar() and pass in the user ID and a URL to the new avatar.

If there are only two participants in the Hangout (meaning the two local stations are the only thing logged in), we may as well show the other participant. We do the opposite of what we did to hide them, calling gapi.hangout.av.setParticipantVisible() and passing in the user ID and true.

  var feed = gapi.hangout.layout.getDefaultVideoFeed();
  if ((gapi.hangout.getParticipantById(feed.getDisplayedParticipant()).person.id == my_id) && (participants.length == 3)) {
    // just in case we get stuck on a blocked user's feed, force switch to the other person
    for (var n = 0; n < participants.length; n++) {
      if (participants[n].person.id != my_id) {
        feed.setDisplayedParticipant(participants[n].id);
        n = participants.length;
      }
    }
  } else {
    feed.clearDisplayedParticipant();
  }
}

We wrap up the update_video_options() function by making sure we don’t somehow get stuck on the video feed for the other local station if someone else is available to see. We use gapi.hangout.layout.getDefaultVideoFeed() to get some data about the displayed video feed. If the displayed participant (getDisplayedParticipant()) is the same user as the machine this is running on and there are three participants to choose from (the two local stations plus someone remote), we want to make a switch. We loop through the participants to find one that isn’t the same user, then we use setDisplayedParticipant() to set the video feed to that user. We re-check the number of participants before looping again because of some API weirdness I can’t explain.

Lastly, if we’re not displaying the other local station or there are multiple remote users to choose from, we clear the displayed participant with clearDisplayedParticipant() and let the Hangout decide who should be shown. Because the other local machine won’t be shown at this point and is muted, it allows whichever remote user is talking to appear.

gadgets.util.registerOnLoadHandler(init);

The last thing we do is use gadgets.util.registerOnLoadHandler() to set the init() function to run when the utility is fully loaded.

One thing I think I’d do differently if this were still in use is update it to handle a variable number of local stations. There’s no reason not to account for three or four or whatever, aside from the fact that there were only two when I wrote it.

My Projects

DetroitHockey.Net

DetroitHockey.Net is my long-running tribute to the Detroit Red Wings, where I do a lot of writing about the team as well as post photos and stats.

Me on Twitter

Mickey Mouse Clubhouse, where the lesson learned is, if a ladder isn't the right tool because it's too dangerous, try a monkey. #parenting

7/2/2015 - 7:57 PM

RT @rmillerwebster: "self-organizing" teams is a lie. The team is forced into a structure that empowers the individuals within it. It requires TRAINED leaders

7/2/2015 - 2:51 PM