Showing all current visitors cursors live

957 Views Asked by At

What would be the best way of showing all the current visitors cursors live in each browser window?

Is this achievable at all, without too much lag?

This is my attempt with meteor js:

http://meteorpad.com/pad/rCSsroc4G4gYQGkAY/Leaderboard (mind the name, can't change it?)

http://allofthecursors.meteor.com/

cursors.js:

Cursors = new Mongo.Collection("Cursors");

if (Meteor.isClient) {
  Meteor.startup(function () {
      if(!Session.get('cursorId')) {
        // todo: not unique enough!
        var cursorId = new Date();

        Session.set('cursorId', cursorId);
        Meteor.call('initCursor', cursorId);
      }
  });

  Template.cursor.helpers({
    cursor: function() {
      return Cursors.findOne({ cursorId: Session.get('cursorId') });
    },
    cursors: function() {
      return Cursors.find({ cursorId: { $ne: Session.get('cursorId') } });
    }
  });

  Template.cursor.events({
    "mousemove": function(event) {
      var x = event.pageX,
          y = event.pageY,
          cursorId = Session.get('cursorId');

      Meteor.call('updateCursor', cursorId, x, y);
    }
  });
}

if (Meteor.isServer) {

  Meteor.methods({
    'initCursor': function(cursorId) {
        Cursors.insert({
          cursorId: cursorId,
          x: 0,
          y: 0
        });
    },
    'updateCursor': function(cursorId, x, y) {
      Cursors.update(
        { cursorId: cursorId }, 
        { $set: { x: x, y: y }}
      );
    }
  });
}

cursors.html

<head>
  <title>cursors</title>
</head>
<body>

    {{> cursor}}

</body>

<template name="cursor">
    <div class="cursors-area">
        {{#each cursors}}
            <div class="cursor" style="top: {{y}}px; left: {{x}}px;"></div>
        {{/each}}

        <div class="my-cursor">
            {{cursor.cursorId}}: {{cursor.x}}, {{cursor.y}}
        </div>
    </div>
</template>

cursors.css

html, body {
    margin: 0;
    padding: 0;
}

.cursors-area {
    height: 100vh;
}

.cursor {
    position: fixed;
    width: 11px;
    height: 17px;
    background-image: url(cursor.png);
}

Thanks for the response evrybady. Here's an updated version.

Added the streams package: $ meteor add lepozepo:streams

The client side collection is only used to access the reactive capabilities.

cursors_2.js

Stream = new Meteor.Stream('public');

if( Meteor.isClient ) {

  Cursors = new Mongo.Collection(null);

  Template.cursor.events({
    'mousemove': function(event) {

      if (Meteor.status().connected) {
        Stream.emit('p', { 
          _id: Meteor.connection._lastSessionId,
          x: event.pageX, 
          y: event.pageY 
        });
      }

    }
  });

  Stream.on('p', function(p) {

    // how can I change this? 

    if( Cursors.findOne(p._id) === undefined ) {

      Cursors.insert({
        _id: p._id,
        x: p.x,
        y: p.y
      });

    } else {

      Cursors.update(p._id, {
          $set: {
            x: p.x,
            y: p.y
          }
        }
      );
    }

  });

  function getCursors() {
    return Cursors.find();
  }
  var throttledGetCursors = _.throttle(getCursors, 50);

  Template.cursor.helpers({
    cursors: throttledGetCursors
  });
}
2

There are 2 best solutions below

3
On

There are essentially two ways to go about this.

  1. The first is to use the built-in APIs you have for client-server-client communication and record keeping, i.e. collections. The advantage of this is that it's very straightforward and the reactivity and communication are taken care of for you. If you go down this route, I think your current setup is overcomplicated - you're using methods to communicate changes in position from each client, and then using livedata to communicate it back again as the collection is synchronised. Why not just write to the collection from the client? I'd suggest something like this:

CLIENT

Template.body.events({
    'mousemove': function(event) {
        if (Meteor.status().connected) {
            Cursors.update(Meteor.connection._lastSessionId, {
                $set: {
                    x: event.pageX,
                    y: event.pageY
                }
            });
        }
    }
});

SERVER

Meteor.onConnection(function(connection) {
    Cursors.insert({
        _id: connection.id,
        x: 0,
        y: 0
    });
    connection.onClose(function() {
        Cursors.remove(connection.id);
    });
});

As well as obviating the need for session variables, that uses the session id to provide a unique cursor id for each client and removes it from the collection when they disconnect. This is also preferable from a security perspective as it allows you to prevent users from updating cursors which don't share their connection id (via allow/deny) in case that would ever be a concern.

  1. However, you still have the overhead of putting things into out out of the MongoDB, which will cause some latency. As @sbking points out, the lepozepo:streams package (docs here) allows you to communicate closer to the raw websocket and, whilst it will involve rather more plumbing, will result in less latency. There was some discussion of this at the London DevShop in Dec 2015, the videos from which should be available soon. It would be great if somebody could write a collection for pure server-client messaging (independent of collections), but I don't think that exists at present.

Also, agreed with @sbking that you should be debouncing updates with Underscore (not throttling).

0
On

Use WebRTC for this. Google for it.

Essentially you're going to use some JavaScript to connect your two browsers together, Meteor will send the handshake, then the two browsers can talk. From here, you can do all sorts of data intensive stuff without blowing up Meteor or your clients. Ez. Pz.