Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify behavior when multiple controlling pages are connected to the session #19

Closed
mfoltzgoogle opened this issue Sep 3, 2014 · 18 comments

Comments

@mfoltzgoogle
Copy link
Contributor

An implication of reconnection is that multiple pages may connect to the same presentation. The behavior of the API should be defined for this scenario. Specifically,

  • Firing of onstatechange = "connected"/"disconnected" on both sides
  • Semantics of close() on both sides
@anssiko
Copy link
Member

anssiko commented Dec 18, 2014

Noted as an issue in the "Usage on Remote Screen" section in the spec: http://w3c.github.io/presentation-api/#usage-on-remote-screen

@mfoltzgoogle
Copy link
Contributor Author

Adding to this issue from #36 discussion of how messaging should be done between the presentation and multiple connected sessions.

In some scenarios the presented page may have active connections to multiple PresentationSessions on other pages. In a gaming scenario, each connected session would represent a game player. Some information (like dealing a card into the hand) should be sent to one player, while other information (playing a card face up) should be sent to all players.

We need to specify:

  1. How the presented page may communicate to a specific connected page
  2. How the presented page may broadcast - communicate to all specific connected pages

Note this thread discussing some concrete spec changes to facilitate this case.

@jakearchibald
Copy link

The SharedWorker model of a connect event that provides access to the connecting client may help here.

@anssiko
Copy link
Member

anssiko commented Feb 20, 2015

Agreed with @jakearchibald on that. Would you @mfoltzgoogle with help from @drott like to investigate this further?

@anssiko anssiko added the F2F label Apr 20, 2015
@tidoust
Copy link
Member

tidoust commented May 19, 2015

PROPOSED RESOLUTION: starting point for #19 is Promise NP.getSession(), PresentationSession[] NP.getSessions(), NP.onsessionavailable event when set of sessions changes (with possible naming changes)

See related discussion at F2F in Berlin:
http://www.w3.org/2015/05/19-webscreens-minutes.html#item12

@anssiko
Copy link
Member

anssiko commented May 20, 2015

ACTION: @mfoltzgoogle to look at renaming "sessions" for controlling and presenting sides. [recorded in http://www.w3.org/2015/05/19-webscreens-minutes.html#action12]

ACTION: @mfoltzgoogle to fix spec to refer to updated Presentation idiom [recorded in http://www.w3.org/2015/05/19-webscreens-minutes.html#action13]

@anssiko anssiko added the action label May 20, 2015
@anssiko
Copy link
Member

anssiko commented May 20, 2015

Also, should define the mechanism by which the page can create a new session that can access existing presentation.

@avayvod
Copy link
Contributor

avayvod commented Jun 5, 2015

How about the following spec change for the multiple connections? It's based on the assumption that only one controller can connect to the presentation page at a time. If the page is slow handling each controller, the UA can simply queue them and resolve the next call to requestController when needed.

partial interface NavigatorPresentation {
    Promise<PresentationSession> requestController();  // replaces navigator.presentation.session
}

Then a simple presentation page supporting one controller would need to write something like this:

navigator.presentation.requestController().then(handleController);

var handleController = function(controller) {
    controller.onstatechange = function(e) {
        if (e.state == "connected") {
            controller.onmessage = function(e) {
                showMessage("Received message " + e.msg);
            };
        } else if (e.state == "disconnected") {
            // wait for the controller to connect back
            navigator.presentation.requestController().then(handleController);
        } else if (e.state == "terminated") {
            // nothing to do, the page is being closed, for the sake of completeness
        }
    };
}); 

And code for the page supporting multiple controllers would look like this:

// controllers
var mControllers = [];
// maximum number of controllers to join the presentation
var MAX_CONTROLLERS = 5;

// announce that the page is ready to become a presentation and receive its first controller
// assume that only one controller can start the presentation or connect to it at the time
navigator.presentation.requestController().then(handleControllerAdded);

var handleControllerAdded = function(controller) {
    showMessage("Controller added: " + controller.id);
    // assert controller.state == "connecting";
    controller.onstatechange = function(e) {
        if (e.state == "connected") {
            handleControllerConnection(this)
        } else if (e.state == "disconnected" || e.state == "terminated") {
            handleControllerDisconnection(this);
        }
    };
};

var handleControllerConnection = function(controller) {
    if (mControllers.empty()) {
        setUp();
    }   
    mControllers.add(controller);
    controller.onmessage = function(e) {
        showMessage("Received message " + e.msg + " from " + this.id);
    };
    if (mControllers.length < MAX_CONTROLLERS) {
        navigator.presentation.requestController(handleController);
    }
};

var handleControllerDisconnection = function(controller) {
    mControllers.remove(controller);
    if (mControllers.empty()) {
        shutDown();
    } else if (mControllers.length < MAX_CONTROLLERS) {
        navigator.presentation.requestController(handleController);
    }
};

var setUp = function() {
    showMessage("The presentation is connected");
};

var shutDown = function() {
    showMessage("No controllers, please connect.");
    navigator.presentation.requestController(handleController);
};

The semantic of the method is as following:

  1. If there's no active connection and no pending promise returned by requestController() - the page is not ready to become a presentation yet (relevant to issue Allow page to turn itself into a presentation session #32).
  2. If there's no active connection but a promise returned by requestController() is pending, the page designates itself as a presentation page and is ready to accept controllers connecting (relevant to issue Allow page to turn itself into a presentation session #32 too).
  3. If there's an active connection but no pending promise returned by requestController() (meaning one or more requestController() promises have resolved before, at least one of the controllers is in the connected state, the page doesn't want more controllers), the presentation can't be connected to anymore and can be replaced when something else starts a presentation to the same screen (or starting the presentation can be rejected by the UA that tried to connect).
  4. If there's an active connection and a promise pending from a previous requestController(), the page expects more controllers to join and the promise will be resolved as soon as another controller joins the presentation.

@louaybassbouss
Copy link
Contributor

+1. Just one comment about naming what about getNextSession or gerNextConnection instead of getNextController? We deceided to use the term controller for the opening page.

@avayvod
Copy link
Contributor

avayvod commented Jun 5, 2015

Session to me is an interface exposed to the controller page to communicate with a presentation.
It doesn't sound right when the presentation is using the same interface to do the opposite. Controller is more explicit that connection.

@mfoltzgoogle
Copy link
Contributor Author

@avayvod Regarding the API proposal:

During the F2F we proposed the following API, in part to ensure that the common use case is simple, and to follow the example of ServiceWorker getRegistration/getRegistrations [1]. Something like the following:

interface NavigatorPresentation : EventTarget {
  // Returns the PresentationSession that was created when the presentation was started.
  Promise<PresentationSession> getSession();
  // Returns all PresentationSessions that have been connected to this presentation.
  Promise<PresentationSession[]> getSessions();
  // Fired when a new controller has connected to this presentation.
  EventHandler onsessionavailable;
}

In your example I had the following questions:

  • What happens if the page calls requestController repeatedly? It seems like when the Promises are all resolved then the client has to deduplicate the values. The advantage of the proposal above is that each Promise should return the same value according to the current set of PresentationSessions.
  • What is the initial state of the PresentationSession when a Promise returned by requestController resolves? In the example it seems like the initial state value is not examined?
  • What does it mean for "a page not ready to become a presentation yet"? Does that mean it has not yet called requestController?
  • In your point Fixed a few typos #3, it sounds like the presentation can put itself into a mode where it rejects additional controllers, and this is controlled by the number of outstanding Promises for requestController. Perhaps this could be an explicit boolean property like acceptsSessions?

[1] https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#navigator-service-worker-getRegistration

@avayvod
Copy link
Contributor

avayvod commented Jun 8, 2015

I haven't found any examples of using getRegistration() and getRegistrations() on HTML5Rocks or MDN or in the API draft. From the algorithm description linked it seems that getRegistration() is used to get a registration for ServiceWorker with a known scope URL, while getRegistrations() is an enumeration method to query all registrations available. There's also no event to notify the page when a new registration is available. IMHO, it doesn't match what's needed by the Presentation API... (get the session for the initial controller page and get the rest of them as they connect).

Answers to the questions:

  1. the UA could reject the Promise returned by a consequently called method or queue the promises and resolve them one by one when getting new connections.
  2. in the example there's an // assert controller.state == "connecting"; meaning that the page waits for the state to become "connected" before appending the controller to its list
  3. yes (it's related to the issues Specify the presentation initialization algorithm #34 and Allow page to turn itself into a presentation session #32 )
  4. it seems to be redundant

The questions I have about the proposed resolution:

  • getSession().then(setSession) seems to be redundant for the simple use case since can be expressed as getSessions().then(function (sessions) { setSession(sessions[0]); }) - why is it needed then? it could be useful to distinguish between the first controller (e.g. game master) from the rest - then the page should call both getSession() and getSessions() and filter the result of the former from the results of the latter.
  • what does getSession() return if the initial controller has disconnected but there're some other controllers still connected?
  • the page will have to subscribe to the onsessionavailable after getSessions() is resolved; having a promise and an event seems to be uncommon; as discussed over availability a few times it's usually a Promise or a property (e.g. sessions and an event); we could consider an approach similar to availability in Rethinking availability monitoring #81
partial interface NavigatorPresentation {
  Promise<PresentationContext> presentationContext();
}

interface PresentationContext : EventTarget {
  readonly attribute PresentationSession[] sessions;
  attribute EventHandler onsessionavailable;
};
  • the API seems to unnecessarily push a concept of a uniform array of the controllers to the page; which may not be convenient

My example using the proposed resolution from F2F would look something like:

// controlling pages that successfully connected
var mSessions = [];
// maximum number of sessions to join the presentation
var MAX_SESSIONS = 5;

// announce that the page is ready to become a presentation and receive the controllers that are 
// already connected or connecting
// NOTE: if knowning the initial controller is important, the page must first call getSession() and execute 
// the call below in the promise resolution handler:
// var initialSession = null;
// navigator.presentation.getSession().then(function (firstSession) {
//     initialSession = firstSession;
navigator.presentation.getSessions().then(handleSessionsAdded);

var handleSessionsAdded = function(sessions) {
    // page could get more than the maximum supported number of sessions received in getSessions()
    for (var i = 0; i < min(sessions.length, MAX_SESSIONS); i++) 
        handleSessionAdded(sessions[i]);
    navigator.presentation.onsessionavailable = function(evt) {
        if (mSessions.length == MAX_SESSIONS) evt.preventDefault();
        handleSessionAdded(evt.session);
    };
};

var handleSessionAdded = function(session) {
    showMessage("Session added: " + session.id);
    // assert session.state == "connecting";
    session.onstatechange = function(e) {
        if (e.state == "connected") {
            handleSessionConnection(this)
        } else if (e.state == "disconnected" || e.state == "terminated") {
            handleSessionDisconnection(this);
        }
    };
};

var handleSessionConnection = function(session) {
    if (mControllers.empty()) {
        setUp();
    }   
    mSessions.add(sessions);
    session.onmessage = function(e) {
        showMessage("Received message " + e.msg + " from " + this.id);
    };
};

var handleSessionDisconnection = function(session) {
    mSessions.remove(session);
    if (mSessions.empty()) {
        shutDown();
    }
};

var setUp = function() {
    showMessage("The presentation is connected");
};

var shutDown = function() {
    showMessage("No controllers, please connect.");
};

@obeletski
Copy link

Why does getSessions() method have to return promise instead of simply returning an array? Do we expect that that call can start some time consuming activity?

  1. If we allow getSessions() to return empty array, then it is only about copying of all presentation from the set of the presentation known to the user agent into array and returning it. We do not need promise in that case.
  2. If getSessions() has to return array that has at least one connected session in it, then it is correct to return a promise.

I actually had in mind only presenting page that was started as a result of a call to startSession on a controller side and ignoring for a while discussion in #32.

@mfoltzgoogle
Copy link
Contributor Author

@avayvod

  • Regarding getSession() vs. getSessions(), the concern raised by JC Duford during the F2F was to make the common case (one controller) as simple as possible. However I agree that it adds some redundancy to the API.
  • getSession() could always return the initial controller (connected or disconnected), but that would prevent the underlying PresentationSession from ever being garbage collected; a minor but annoying inefficiency.
  • I think the transcription was botched from the F2F. Re-reading the notes, getSessions() was proposed to return just a PresentationSession[] (no Promise). We did not want to expose an Array property because of concerns about modification of the Array prototype.
  • I agree that an array is probably not the right semantic to convey the set of controllers. It makes it harder to garbage collect disconnected sessions and forces the Web developer to diff to find out if there are new controllers.

@obeletski

  • See my comment above; I believe the only Promise should be for getting the initial controller via getSession() and (under the original proposal) we would return an Array PresentationSession[] itself for getSessions().

Let me see if I can formulate another proposal that addresses the concerns raised here.

The other actions raised in the F2F to formulate new idioms and a new APIs for referring to the PresentationSession on either side of the presentation [1] [2] I will consider and propose separately.

[1] http://www.w3.org/2015/05/19-webscreens-minutes.html#action12
[2] http://www.w3.org/2015/05/19-webscreens-minutes.html#action13

@obeletski
Copy link

We might want to support the following functionality on the presenting page:

  1. Be able to distinguish the first session to the controlling page.
  2. Accept only certain number of controlling pages (think of the game that supports only 2 players)
  3. Stop accepting controlling pages after certain time (e.g. game or class has started)

From that perspective simple interface that was proposed by @avayvod could be a good solution.

partial interface NavigatorPresentation {
    Promise<PresentationSession> requestController(params);  // replaces navigator.presentation.session
}

Additionally, we will have to pass implementation specific parameters to getSession() or requestController() methods. For Google cast one has to provide cast message bus, for HbbTV app-endpoint #67 (comment), for Samsung's multiscreen - channelId. That means that UA is not able to initiate background task for connecting to the controlling pages without that extra information before page is loaded. That task has to be triggered by the script.

Another thing is that on the controlling page it would not be possible to differentiate that presentation page is not accepting connections anymore, see communication issues listed here: #67 (comment). Establishing of the connection (pairing) is working I the same way for Samsung's multiscreen and the same issues exist for that technology. In practice, requirements 2 and 3 would difficult to implement for controlling page. So presenting page would be better off establishing connection, sending message that no more participants are accepted and closing that immediately.

@mfoltzgoogle
Copy link
Contributor Author

@obeletski

  1. Definitely - yes. The assumption I have made is that when the presenting page is loaded the first thing it will want to do is establish connectivity to at least one controlling page.
  2. I believe this application logic can be handled by either:
    • The presenting page immediately closing any PresentationSessions from additional controllers, once the limit is reached;
    • The presenting page signaling to the user agent via a platform specific mechanism that no additional controllers should be connected.
      I am not sure yet there is a strong use case for adding a property to the API to deny further connections when there are alternatives available.
  3. Again, the presenting page can use setTimeout and then deny further connections using the mechanisms in Moving demo to its own repository. #2.
  4. I didn't quite follow the scenario you described. For HbbTV, it sounds like the app-endpoint could be passed with the presentation URL and it could be used to initiate one or more WebSocket connections from the presenting page. Or, the UA could capture the app-endpoint URL parameter and initiate the connection on the presenting page's behalf (wrapping the WebSocket in a PresentationSession).
  5. It sounds like you're in agreement that the first requirement is crucial, and it may be better support the second and third via script in the presenting page?

@obeletski
Copy link

Agree on 1, 2, 3, 5.

  1. I wanted to bring group's attention to the fact that we will have to make an extra effort on the implementation side to make sure that app-endpoint/channelId/CastMessageBus passed from controlling to presenting page if we want UA to take care of connecting the session with no extra parameters passed from the script.

@mounirlamouri
Copy link
Member

This seems to have been resolved. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants