Using Presentation API to Cast to Secondary Screens

The Presentation API enables web applications to extend their user interface to secondary displays, such as connected monitors, smart TVs, or wireless casting devices like Chromecasts. By establishing a bridge between a controlling web page and a separate receiving web page, the API allows developers to deliver dual-screen experiences—such as slide presentations, video players with remote controls, or gaming dashboards. This article explains the core architecture of the Presentation API, how it discovers external displays, initiates connections, and facilitates real-time data exchange across screens.

Core Architecture: Controller and Receiver

The Presentation API operates on a dual-role architecture:

  1. The Controller: The primary web application running on the user's main device (such as a laptop or smartphone). It requests the presentation, selects the target screen, and sends control messages or state updates.
  2. The Receiver: A distinct HTML document loaded directly onto the secondary display. This page receives instructions from the controller and renders the presentation view, free from controller-specific UI like buttons or speaker notes.

Detecting Device Availability

Before offering cast functionality to the user, the controller page checks whether a compatible display is accessible on the network or via hardware cables.

Developers instantiate a PresentationRequest object, providing the URL of the receiver page:

const presentationRequest = new PresentationRequest(['https://example.com/receiver.html']);

Using the getAvailability() method, the application observes device availability:

presentationRequest.getAvailability()
  .then(availability => {
    // Show or hide the cast button based on availability
    castButton.style.display = availability.value ? 'inline-block' : 'none';

    availability.onchange = () => {
      castButton.style.display = availability.value ? 'inline-block' : 'none';
    };
  })
  .catch(() => {
    // Availability monitoring is not supported; show button by default
    castButton.style.display = 'inline-block';
  });

Initiating the Cast Session

When a user clicks the cast trigger, the application invokes presentationRequest.start(). Because casting involves hardware access and external networks, browsers require this call to be triggered directly by a user gesture, such as a button click.

Calling start() prompts the browser's native device-picker dialog, allowing the user to select the target display (e.g., an HDMI-connected monitor, AirPlay receiver, or Google Cast target). Once selected, the browser launches the receiver URL on that screen and returns a PresentationConnection object to the controller:

castButton.addEventListener('click', async () => {
  try {
    const connection = await presentationRequest.start();
    setupConnection(connection);
  } catch (error) {
    console.error('Connection failed or was cancelled by the user:', error);
  }
});

Two-Way Communication Between Screens

Once established, the PresentationConnection functions similarly to a WebSocket. Both the controlling page and the receiving page can send text strings, JSON payloads, or binary data (such as Blob or ArrayBuffer) back and forth.

On the Controller:

function setupConnection(connection) {
  // Send state or commands
  connection.send(JSON.stringify({ command: 'PLAY_MEDIA', timestamp: 120 }));

  // Listen for messages from the receiver
  connection.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('Status from secondary display:', data);
  };

  // Monitor connection states (connected, closed, terminated)
  connection.onterminate = () => {
    console.log('Presentation session ended.');
  };
}

On the Receiver:

The receiver page accesses the incoming connection through the navigator.presentation.receiver property:

if (navigator.presentation.receiver) {
  navigator.presentation.receiver.connectionList.then(list => {
    list.connections.forEach(connection => {
      listenToController(connection);
    });

    list.onconnectionavailable = (event) => {
      listenToController(event.connection);
    };
  });
}

function listenToController(connection) {
  connection.onmessage = (event) => {
    const message = JSON.parse(event.data);
    if (message.command === 'PLAY_MEDIA') {
      playVideoAt(message.timestamp);
    }
  };

  // Notify the controller that the receiver is ready
  connection.send(JSON.stringify({ status: 'READY' }));
}

Session Reconnection and Termination

The Presentation API supports session persistence. If a controller page is refreshed or accidentally closed, it can reconnect to an ongoing session using presentationRequest.reconnect(presentationId).

When the user chooses to stop casting, either party can close the message channel via connection.close() or completely shut down the receiver instance on the remote screen using connection.terminate().