Collaboration

The PureWeb SDK supports collaboration, where several users can interact simultaneously with the same service session from their own instance of the client application. For example, when collaboration is enabled, if one participant zooms in on a specific part of a 3D graphic, the view will automatically change in each participant's screen to show the same zoomed-in section. This works even if participants are using different clients of that service (a web client and a mobile client, for example).

Overview

The collaboration functionality is handled by the CollaborationManager interface, which is included with every PureWeb API. To take advantage of this feature, you simply initiate collaboration through the WebClient API by calling getSessionShareUrlAsync from your client; the service-side CollaborationManager will respond by generating a share URL and sending that URL to back to the client. To join a collaboration session, participants simply navigate to the share URL. From there, the collaborative session can be managed through the client and service APIs.

By default, collaboration access is anonymous. In other words, any user who receives an invite from another user and is provided with the share URL and a password can join a session. However it is possible to restrict access to authenticated users; in this case, collaboration participants must log into the application first before they can join the session; see Restricting Collaboration Access.

Out of the box, the Collaboration Manager provides shallow collaboration: there is no difference, from a functionality perspective, between the host and the participant, and all can interact with the same features at the same time. The host has no control over when and how participants interact with their client, and consequently control cannot be passed from one participant to another. There are no visual cues that other participants have joined, unless they interact with their client.

The advantage of this approach is that you are not required to use a rigid collaboration model by default. If the functionality of the Collaboration Manager is not sufficient for your application, you have the flexibility of adding to your service's logic the tighter controls that suit your particular requirements. The Asteroids sample application is an example of a PureWeb service where deeper collaboration has been implemented. This approach is described in the section Using Session IDs to Manage Collaboration, further down this page.

To supplement the Collaboration Manager, the SDK also provides acetate tools that you can use, for example, to display each participant's mouse cursor on all participants' screen, or to enable writing on an overlay on top of views.

Each client application that joins a collaboration session runs its own thread. The maximum number of participants that can join a given service session depends on the GPU capacity of the server node on which the service is running. This can be configured in the Display Properties.

Although rarely needed, system administrators have the ability to manually disconnect users from a collaboration session using the PureWeb server's administrative interface. See Status Page.

Generating a Share URL

To take advantage of the collaboration feature, you use the client-side getSessionShareUrlAsync function to request a share URL from the service-side CollaborationManager.

Syntax

The syntax for getSessionShareUrlAsync is as follows:

WebClient.getSessionShareUrlAsync (password, descriptor, timeout, criteria, callback);

The descriptor and criteria parameters are not used at this time and can be safely ignored. Below is a brief description of the other three parameters.

  • password: the password that the participants will need to enter when using the share URL to join a session; this password, which is distinct from the password associated with the user who launched the session, must be communicated to the receiving users
  • timeout: the length of time (in milliseconds) that the share URL will remain valid for new collaborators to join (collaborators already in the session are not affected when this timeout lapses)
  • callback: the function that is called when CollaborationManager returns the share URL; this function usually displays the share URL in a dialog box in the client

Workflow

Typically, the steps for implementing collaboration in a client are as follows:

  1. Create an interface element that users can interact with to enable collaboration, for example a Share button or menu item. You use your platform's native elements for this.
  2. Add to the interface element a function to generate a share URL, that will be triggered when the user interacts with this element.
  3. Program the function above to use getSessionShareUrlAsync, which generates the share URL. Your function would also typically include the logic for handling situations where the share URL is not valid or has expired.
  4. Program a callback function for getSessionShareUrlAsync that will display the share URL to the end user. The end user is then responsible for communicating this URL to participants who want to join in the session, using what ever mechanism is most appropriate such as email.

HTML5

In order to keep the example below short and agnostic of any third-party library, the dialog box that presents the share URL is using the prompt() synchronous JavaScript call, but this is not best practice since getSessionShareUrlAsync is asynchronous. For more information, see Presenting Modal Dialogs in HTML5.

The Scribble sample application's interface has a Share button which, when pressed, calls the generateShareURL function:

<button id="share" onclick="generateShareURL();">Share</button>

The generateShareURL function uses getSessionShareUrlAsync to get the share URL. Within getSessionShareUrlAsync, the callback parameter is the getUrl function, which displays the share URL in a prompt window :

var shareURL;

var webClient= pureweb.getClient();
if (shareUrl === undefined) || (shareUrl === null)) {
    webClient.getSessionShareUrlAsync('Scientific', '', 1800000, '', function(getUrl, exception)  {
	    if ((getURL !== null) && (typeof(getUrl) !== "undefined")) {
		    shareUrl = getUrl;
			window.prompt("Here is your share URL:", getUrl);
			document.getElementById('share').inner.HTML = 'Invalidate Share';
		} else {
			alert('An error occurred creating the share URL: ' + exception.description);
		}
	});
} else {
	webClient.invalidateSessionShareUrlAsync(shareUrl, function (exception) {
		if (( exception !== undefined) && (typeof(exception) !== "undefined")) {
			alert('An error occurred invalidating the share URL ' + exception);
		} else {
			shareUrl = null;
			document.getElementById('share');
		}
	});
}				

iOS

The Scribble sample application's interface has a Share button which, when pressed, calls the shareButtonPushed function:

UISegmentedControl *seg2 = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Share",nil]];
[seg2 addTarget:self action:@selector(shareButtonPushed:) forControlEvents:UIControlEventValueChanged];
seg2.frame = CGRectMake(95, 7, 50, 30);
seg2.segmentedControlStyle = UISegmentedControlStyleBar;
seg2.momentary = YES;
seg2.tintColor = [UIColor darkGrayColor];
[container addSubview:seg2];
self.shareButton = seg2;				

The shareButtonPushed function uses getSessionShareUrlAsync to get the share URL.

- (IBAction)shareButtonPressed:(UIBarButtonItem *)sender {

    //request a share url from the server
	[[PWFramework sharedInstance].client getSessionShareUrlAsyncWithPassword:@"Scientific"
	    shareDescriptor:@""
		shareTimeout:1800000
                    
	completion:^(NSURL *shareURL, NSError *error) {

		if (error) {
			PWLogError(@"share url created failed with error %@", error);
			return;
		}
	[self presentMailComposerWithShareURL:shareURL];
	}];
}					

Within getSessionShareUrlAsync (above), the callback parameter is the serviceRequestDidFinish function, which creates an email that contains the share URL, and also provides the logic for handling exceptions:

- (void)serviceRequestDidFinish:(PWServiceRequestCompletedEventArgs *)args
{
    if (args.request.status == PWServiceRequestStatusSuccess)
	{
		PWAppShare *appShare = (PWAppShare *)args.request;
			self.sharedURL = [appShare shareUrl];
			 
		MFMailComposeViewController *mailController = [[MFMailComposeViewController alloc] init];
		if (mailController != nil)
		{
			mailController.navigationBar.tintColor = [UIColor darkGrayColor];
			mailController.mailComposeDelegate = self;
			[mailController setSubject:@"Please join my shared PureWeb session."];
			[mailController setMessageBody:self.sharedURL isHTML:NO];
			[self presentModalViewController:mailController animated:YES];
			 
		}
	}
	else
	{
			[UIAlertView showAlert:@"There was an error while creating the application share"
				message:[args.request.error description]];
	}
}			

Android

The Asteroids sample application's menu is created using the onOptionsItemSelected method. This method contains if/else statement for each menu option. When the end user choose the Share option (R.id.share), the application generates the share URL using getSessionShareUrlAsync:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
	int itemId = item.getItemId();

	if (itemId == R.id.settings) {
		Intent settingsIntent = new Intent(getBaseContext(), SettingsActivity.class);
		settingsIntent.putExtra(SettingsActivity.PREFERENCE_RES_ID_KEY, R.xml.preferences);
		startActivityForResult(settingsIntent, PureWebActivity.SETTINGS_REQUEST_CODE);
		return true;
	} else if (itemId == R.id.share){
		framework.getWebClient().getSessionShareUrlAsync
			("Scientific", "", 1800000, "", new ShareRequestCompleted());
		return true;
	} else if (itemId == R.id.close){
		finish();
		return true;
	}
	else {
		return false;
	}...			

Within getSessionShareUrlAsync (above), the callback parameter is the ShareRequestCompleted function, which creates an email form that contains the share URL, and also provides the logic for handling exceptions:

protected class ShareRequestCompleted implements GetSessionShareUrlCallback {
    public ShareRequestCompleted() {}

    public void invoke(String shareUrl, final Throwable exception) {
        if (shareUrl != null){
            Intent emailIntent = new Intent(Intent.ACTION_SEND);
            String[] recipients = new String[]{"recipient@your-company.com"};
        emailIntent.putExtra(Intent.EXTRA_EMAIL, recipients);
        String appName = getResources().getString(R.string.app_name);
        emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Please join my " + appName + " session");
        emailIntent.putExtra(Intent.EXTRA_TEXT, shareUrl);
        emailIntent.setType("text/plain");
        startActivity(Intent.createChooser(emailIntent, "Send mail..."));
        } else{
            UiDispatcherUtil.beginInvoke(new Runnable(){
                public void run() {
                    AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(PureWebActivity.this);
                    errorDialogBuilder.setIcon(pureweb.samples.R.drawable.ic_pureweb);
                    errorDialogBuilder.setTitle("An error occurred creating the Share URL");
                    errorDialogBuilder.setMessage("Unexpected exception: " + exception.getMessage());
                    errorDialogBuilder.setCancelable(false);
                    errorDialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener(){
                        public void onClick(DialogInterface dialog, int id){
                            dialog.dismiss();
                         }...

Flex

The Scribble sample application's interface has a Share button which, when pressed, calls the createShare function:

<mx:HBox width="100%">
    <mx:Button id="shareBn" label="Share" click="viewModel.createShare()" />
	<mx:Label text="Share Url" />
	<mx:TextInput id="textOutput" width="100%" text="{viewModel.shareUrl}"/>
</mx:HBox>				

When the user clicks this Share button, the createShare function creates an AppShare service request and queues it for execution by the PureWeb server. The AppShare service request creates a collaboration session and returns the share URL. It also creates an event listener to monitor the share request.

public function createShare():void
{
	var appShare:AppShare = new AppShare("Scientific", "Collable", 600000);
	appShare.addEventListener(Event.COMPLETE, onShareRequestComplete);
	Framework.instance.client.queueRequest(appShare);
}

public function onShareRequestComplete(event:Event):void
{
	var appShare:AppShare = AppShare(event.target);
	shareUrl = appShare.shareUrl;
}				

If the client application that first requests the share URL disconnects when other users have joined the session, the next participant who has joined automatically becomes the "host". However, the Collaboration Manager by default makes no distinction in functionality between the host and the participants, therefore this is not relevant to the end users.

Note that when displaying a share URL, it is important to use the server's full IP address and avoid shared cookies. For more information, see Common Issues with Share URLs.

Using Session IDs to Manage Participants

The service-side CollaborationManager interface distinguishes between host and participant in name only. By default, service applications do not behave differently when used by hosts or participants, nor do they adapt the client's functionality based on the number of participants. In a PureWeb application, to provide this deeper collaboration functionality, you must add it directly to your service's application logic.

A common way to do this is to make use of the individual client session ID that is assigned to each participant, and to use these IDs to manage the functionality in a multi-user session.

The Asteroids sample application provides a good example of this: if only one client is logged in, the game works in single player mode. If a second player uses a share URL to join an existing game, Asteroids switches to two-player mode, where both ships appear on each participant's screen, in different colors.

Below is the Java code snippet that shows how this is done. This code uses commands.

private class SessionConnectedHandler implements EventHandler<SessionEventArgs>
{
    public void invoke(Object source, SessionEventArgs args)
    {
        UUID sessionId = args.getSessionId();
        String playerName = args.getCommand().getChildText("Name");
        if (ship1Controller.getSessionId().equals(UUIDUtil.EmptyUUID))
        {
            ship1Controller.setSessionId(sessionId);
            ship1Controller.setConnected(true);
            Ship ship1 = ship1Controller.getShip();
             ship1.setName(playerName);
             ship1.setDefaultPosition(new Point2D.Double(getWidth() / 2.0, getHeight() / 2.0));
             initializeGame();
        }else if (ship2Controller.getSessionId().equals(UUIDUtil.EmptyUUID))
        {
            Dimension size = getSize();
            ship2Controller.setSessionId(sessionId);
            ship2Controller.setConnected(true);
            Ship ship2 = ship2Controller.getShip();
            ship2.setColor(Color.GREEN);
            ship2.setName(playerName);
            ship2.setDefaultPosition(new Point2D.Double(size.width / 3.0, size.height / 2.0));
            ship2.initialize(MonotonicTimeUtil.currentTimeMillis());
            Ship ship1 = ship1Controller.getShip();
            ship1.setColor(Color.YELLOW);
            ship1.setDefaultPosition(new Point2D.Double((2.0 * size.width) / 3.0, size.height / 2.0));
            if (gameOver)
                ship1.initialize(MonotonicTimeUtil.currentTimeMillis());
            else
            {
                ship2.startGame();
                ship2.setVisible(false);
             }
         }
         else
            log.warn("ConnectSession with two players already connected, sessionId is " + sessionId.toString());
    }
}
Code Description

Each time a user connects on the Asteroids client, an event is triggered in both the client and the service application. On the service application, you use SessionManager.addSessionConnectedHandler (above) to listen to this event; on the client you would listen to the WebClient for the SessionStateChanged (or SESSION_STATE_CHANGED) event.

SessionManager sessionManager = stateManager.getSessionManager();
sessionManager.addSessionConnectedHandler(new SessionConnectedHandler());

On the service application, a registered command handler, SessionConnectedHandler, gets called whenever the SessionManager detects the arrival of a new client.

The handler responds to the event by getting the session ID for the new client and binding it to the first available ship, then performing various initialization actions.

If a second player joins, SessionConnectedHandler fires again and, the game is reconfigured to enter two-player mode.

Similarly, when one of the two players disconnects, an event handler is triggered, which responds by unbinding the ship corresponding to the session ID of the disconnected player, as shown in the Java code snippet below.

private class SessionDisconnectedHandler implements EventHandler<SessionEventArgs>
{
    public void invoke(Object source, SessionEventArgs args)
    {
        UUID sessionId = args.getSessionId();
         if (ship1Controller.getSessionId().equals(sessionId))
              ship1Controller.setConnected(false);
         else if (ship2Controller.getSessionId().equals(sessionId))
              ship2Controller.setConnected(false);
         else
             log.warn("DisconnectSession from unknown session, sessionId is " + sessionId.toString());
    }
}