Syncing Youtube with Websockets

A while ago I came across SynchTube, a rather neat service which allows you to synchronise the playback of a Youtube videos (as well as other services).

This creates a rather interesting viewing experience: one can discover and collaboratively critique videos they otherwise would never have seen before.

Synchtube

After playing around with Synchtube, I was curious as to how it worked. It turns out the embeddable youtube plugin allows you to query and set the time of a playing video. As long as you have some sort of central authority and a realtime messaging service, you can synchronise playback between multiple users.

In the case of Synchtube, it synchronises the video by sending commands over Websockets. For instance, if you play around with the javascript code, you’ll find simple JSON messages being processed from the server:

Handling add_user with data ["11b85c64",null,null,null,false,false,0].
Handling num_votes with data {"votes":0}.
Handling remove_user with data "0282e6d7".
Sending < with data "=D".
Handling < with data ["66716fea","=d"].
Handling < with data ["55c2f7fa","cats and boots and cats and boots and boots and cats and boots and cats"].
Handling add_user with data ["bbabb3dc",null,null,null,false,false,0].
Handling num_votes with data {"votes":0}.
Handling remove_user with data "bbabb3dc".

Onto the Project

Making youtube videos sync over the web sounded like a cool idea for a project, so I decided to make a simple simple synchronised playback system: play a video in one window, and it’s replicated in another via a simple Websocket protocol.

TestTube 1

Putting together Sinatra, ActiveRecord, and thin-websocket I made an initial prototype using a simple JSON messaging protocol with two key commands: “video” to set the video, and “video_time” to set the video time:

[client] {'t': 'subscribe', 'channel_id': 1}
[server] {'t': 'userjoined', 'user': {id: => 'anon_123', name: 'Anonymous', anon: true }}
[server] {'t': 'skip', 'count': 0}
[server] {'t': 'video', 'time': 0, 'force': true, 'url': 'EJ_wXOFQV3M', 'provider': 'youtube', 'title': 'STALLMANQUEST', 'duration' => 152.953, 'playlist' => false, 'position': 0, 'added_by': 'Anonymous'}

A designated leader simply polled the youtube control and sent updates to all the other clients.

Nice enough, but I decided to continue on by adding more functionality: the playlist, video skipping, chat and moderation. All of this functionality ended up being passed around as simple JSON messages through the Websocket connection.

All was going well until I bumped into a design crisis. Originally I wanted users to sign up in order to create and moderate channels, but this solution was becoming more and more undesirable. I wanted to try something different.

So I borrowed an idea from a certain anonymous image board: make everyone anonymous and use Tripcodes to identify users who want to be identified.

Why? well after using Synchtube for a while, I found the only thing I was interested in was the sharing and discovery of videos, not the excessive point scoring by the community. I also noticed a general hostility to users without user accounts, which to me detracted from the experience of watching cool videos. By making everyone anonymous by default I hoped to emphasise the viewership aspect.

While this required rewriting half the authentication mechanism, it was well worth it. Except for the administration interface I didn’t have to worry about implementing user account logic.

Another change I made was to use Backbone.js to help structure the front-end, as well as the administration interface. While this significantly slowed down development, I felt it really helped to keep the back-end service simple and lightweight.

Problems with WebSockets

Websockets are undeniably great as they solve a fundamental problem of how to push messages to clients. Unfortunately though they suffer from poor support in web servers. For instance, during development I was able to serve both the front-end and the Websocket communication through a single port, but I found replicating such a configuration in a production environment to be practically impossible.

Using nginx, I was not able to open a web socket through its HTTP 1.1 backend proxy. This led to the rather undesirable solution of having to serve Websockets directly from the app server on a separate port.

Another problem with Websockets is they don’t seem to work reliably with cookies. So if for example you want to tie a Websocket connection to a logged in user, you need to use another mechanism to authenticate the user such as generating a user token.

Usage of backbone

I have to admit, I hated Backbone.js. It always seemed like a rather arbitrary solution for synchronising models between a client and server. A lot of the examples I saw seemed needlessly complicated and abstracted.

Half-way through development I was getting a bit annoyed at using so many views for something as simple as a list of items, so I decided to refactor and use a different design pattern: use a single view and take advantage of element manipulation and event bubbling in JQuery. This greatly simplified my code in many places, for example:

// Instead of this:

var BanListRow = {
  tagName: 'div',
  className: 'ban_row',
  events: {
    "click a.edit": "edit"
  }
}
BanListRow.initialize = function() {
  this.model.bind("change", this.render, this);
}

...

var BanListPanel = {
  tagName: 'div',
  id: 'banedit',
  events: {
    "click a.add_ban": 'createBan'
  }
}
BanListPanel.initialize = function() {
  BanList.bind('add', this.addBan, this);
  BanList.bind('remove', this.removeBan, this);
  BanList.bind('reset', this.addBans, this);

  BanList.fetch();
}
...

// Consolidate everything together like this :

var BanListPanel = {
  tagName: 'div',
  id: 'banedit',
  events: {
    "click a.add_ban": 'createBan'
  }
}
BanListPanel.initialize = function() {
  BanList.bind('add', this.addBan, this);
  BanList.bind('remove', this.removeBan, this);
  BanList.bind('reset', this.addBans, this);
  BanList.bind('change', this.updateBan, this);
}

Generally speaking I found it best to keep objects to a minimum and take advantage of event bubbling. After realising this I felt a bit more comfortable with using Backbone.js.

To conclude


TestTube 2

In the end TestTube turned into an anonymous synchronised youtube playlist. For those interested, I put up an instance so you can check it out:

http://testtube.cuppadev.co.uk/r/1

In all I felt this was a really cool project. It goes to show if you have an idea, even if someone has already implemented it there is nothing stopping you from having a go at implementing it yourself.