Developer Console

Matter Casting Integration

To enable Matter Casting in your apps update the following:

  • Client: This is your user-facing phone app for Android or iOS. By making your phone app a Matter client, the user can discover casting targets such as a Fire TV. They can also cast content, and control casting sessions.
  • Content app: This is your user-facing app on a Fire TV. By making your TV app a Matter content app, clients are able to control it. For example, they can start playback of specific content.

Communication between the client and player follows the Matter protocol. As a result, you can develop the client against a Fire TV device or use one of the sample players included in the Matter open source repository. This repository includes an Android player which can run on Android TV platforms. Similarly, the communication between the player and the content app is done using Matter-specific Android Intents, so that development can be done with a Fire TV device or with the Android player on a generic Android TV platform. The following will explain how to integrate the Matter Casting SDK into your Android or iOS client app and how to interact with it.

Step 1: Integrate the Matter Casting SDK into your client app

Here are the steps required to integrate and initialize the Matter SDK into your mobile Android or iOS app.

Build and setup

To consume any of the APIs described in this document, your client must include the Matter Casting library built the the desired platform. This can be an Android or an iOS app.

Caching

Clients maintain an on-device cache with the information about the players it previously connected to. This cached information allows the client to connect with these players faster, and using fewer resources, by potentially skipping the longer commissioning process, and simply re-establishing the CASE session. This cache can be cleared by calling the ClearCache method on the client. Clearing the cache can be useful when the user signs out of the client, for instance.

Integration on Android

Download the Android SDK from the CSA Github repository and add it to your project. Once it has been added to your project, initialize the library with the following steps (full listing).

1. Define the rotating device identifier

This is a unique per-device identifier, consisting of a randomly-generated 128-bit or longer octet string. Review the Matter specifications for details on how to generate the Rotating Device Identifier. Instantiate a DataProvider object as described below to provide this identifier.

Copied to clipboard.

private static final DataProvider<byte[]> rotatingDeviceIdUniqueIdProvider =
new DataProvider<byte[]>() {
    private static final String ROTATING_DEVICE_ID_UNIQUE_ID =
        "EXAMPLE_IDENTIFIER"; // dummy value for demonstration only

    @Override
    public byte[] get() {
    return ROTATING_DEVICE_ID_UNIQUE_ID.getBytes();
    }
};

2. Initialize commissioning data

The commissioningDataProvider object contains the passcode and other artifacts which identify the client and are provided to the player during the commissioning process. Refer to Matter specification’s Onboarding Payload section for details on commissioning data.

Copied to clipboard.

public static class CommissionableDataProvider implements DataProvider<CommissionableData> {
CommissionableData commissionableData =
    // Dummy values for commissioning demonstration only. These are hard coded in the example tv-app:
    // connectedhomeip/examples/tv-app/tv-common/src/AppTv.cpp
    private static final long DUMMY_SETUP_PASSCODE = 20202021;
    private static final int DUMMY_DISCRIMINATOR = 3874;

    new CommissionableData(DUMMY_SETUP_PASSCODE, DUMMY_DISCRIMINATOR);

    @Override
    public CommissionableData get() {
    return commissionableData;
    }

    // If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature:
    public void updateCommissionableDataSetupPasscode(long setupPasscode, int discriminator) {
        commissionableData.setSetupPasscode(setupPasscode);
        commissionableData.setDiscriminator(discriminator);
    }
};

3. Add Device Attestation Credentials

The Device Attestation Credentials (DAC) object provides the certificates required to sign messages as part of the Device Attestation process during commissioning. The Matter specification provides background information on how this works. For development purposes, you can use the DAC provided in the sample code.

Define dacProvider to provide the client's Device Attestation Credentials, by implementing com.matter.casting.support.DACProvider.

Copied to clipboard.

private static final DACProvider dacProvider = new DACProviderStub();
private static final DataProvider<DeviceAttestationCredentials> dacProvider = new DataProvider<DeviceAttestationCredentials>() {
    private static final String kDevelopmentDAC_Cert_FFF1_8001 = "MIIB5z...<snipped>...CXE1M=";     // dummy values for demonstration only
    private static final String kDevelopmentDAC_PrivateKey_FFF1_8001 = "qrYAror...<snipped>...StE+/8=";
    private static final String KPAI_FFF1_8000_Cert_Array = "MIIByzC...<snipped>...pwP4kQ==";

    @Override
    public DeviceAttestationCredentials get() {
        DeviceAttestationCredentials deviceAttestationCredentials = new DeviceAttestationCredentials() {
        @Override
        public byte[] SignWithDeviceAttestationKey(byte[] message) {
            try {
            byte[] privateKeyBytes = Base64.decode(kDevelopmentDAC_PrivateKey_FFF1_8001, Base64.DEFAULT);
            AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance("EC");
            algorithmParameters.init(new ECGenParameterSpec("secp256r1"));
            ECParameterSpec parameterSpec = algorithmParameters.getParameterSpec(ECParameterSpec.class);
            ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(new BigInteger(1, privateKeyBytes), parameterSpec);

            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            PrivateKey privateKey = keyFactory.generatePrivate(ecPrivateKeySpec);

            Signature signature = Signature.getInstance("SHA256withECDSA");
            signature.initSign(privateKey);
            signature.update(message);

            return signature.sign();
            } catch (Exception e) {
            return null;
            }
        }
        };

        deviceAttestationCredentials.setDeviceAttestationCert(
        Base64.decode(kDevelopmentDAC_Cert_FFF1_8001, Base64.DEFAULT));
        deviceAttestationCredentials.setProductAttestationIntermediateCert(
        Base64.decode(KPAI_FFF1_8000_Cert_Array, Base64.DEFAULT));
        return deviceAttestationCredentials;
    }
};

4. Initialize the client

Once the objects are created, initialize the client as listed below. The client may be initialized only once before a Matter Casting session is started.

Copied to clipboard.

public static MatterError initAndStart(Context applicationContext) {
    // Create an AppParameters object to pass in global casting parameters to the SDK
    final AppParameters appParameters =
        new AppParameters(
            applicationContext,
            new DataProvider<ConfigurationManager>() {
            @Override
            public ConfigurationManager get() {
                return new PreferencesConfigurationManager(
                    applicationContext, "chip.platform.ConfigurationManager");
            }
            },
            rotatingDeviceIdUniqueIdProvider,
            commissionableDataProvider,
            dacProvider);

    // Initialize the SDK using the appParameters and check if it returns successfully
    MatterError err = CastingApp.getInstance().initialize(appParameters);
    if (err.hasError()) {
    Log.e(TAG, "Failed to initialize Matter CastingApp");
    return err;
    }

    // Start the CastingApp
    err = CastingApp.getInstance().start();
    if (err.hasError()) {
    Log.e(TAG, "Failed to start Matter Casting Client");
    return err;
    }
    return err;
}

Integration on iOS

A. Build the Matter framework

The open source Github repository provides a framework for handling low-level communication between your iOS client and the video casting player and content app. Build the framework using the following steps.

  • Git clone the Github repository to your development machine using git clone https://github.com/project-chip/connectedhomeip/tree/master
  • In a terminal, change the folder to the location of your repository and run source scripts/bootstrap.sh. This builds the libraries and associations required.
  • Open the example iOS app to build the framework locally.
  • Include the framework in your iOS app.

B. Integrating the Matter Casting framework into your app

1. Define the rotating device identifier

This is a unique per-device identifier that consists of a randomly-generated 128-bit or longer octet string. Review the Matter specifications for details on how to generate the Rotating Device Identifier. Instantiate a DataProvider object as described below to provide this identifier.

Copied to clipboard.

class MCAppParametersDataSource : NSObject, MCDataSource
{
    func castingAppDidReceiveRequestForRotatingDeviceIdUniqueId(_ sender: Any) -> Data {
        // dummy value, with at least 16 bytes (ConfigurationManager::kMinRotatingDeviceIDUniqueIDLength), for demonstration only
        return "0123456789ABCDEF".data(using: .utf8)!
    }
    ...
}

2. Initialize commissioning data

This object contains the passcode, discriminator, and more. which identifies the app and provides the CastingPlayer object during the commissioning process. A passcode must be included as a 27-bit unsigned integer. This serves as proof of possession during commissioning. A Discriminator must be included as a 12-bit unsigned integer, which must match the value which a device advertises during commissioning. See Chapter 5 Commissioning in the Matter Core specification for more details on the Matter specification’s Onboarding Payload section.

Add func commissioningDataProvider to the MCAppParametersDataSource class defined above, that provides the required values to MCCastingApp. If using the CastingPlayer commissioner-generated passcode UDC feature, the casting client must update the commissioningDataProvider during the VerifyOrEstablishConnection() API call (described later). In the example below, the update function updates the CommissionableData with a CastingPlayer generated passcode entered by the user on the casting client UX.

Copied to clipboard.

// Dummy values for demonstration only.
private var commissionableData: MCCommissionableData = MCCommissionableData(
    passcode: 20202021,
    discriminator: 3874,
    spake2pIterationCount: 1000,
    spake2pVerifier: nil,
    spake2pSalt: nil
)

func castingAppDidReceiveRequestForCommissionableData(_ sender: Any) -> MCCommissionableData {
    return commissionableData
}

// If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature:
func update(_ newCommissionableData: MCCommissionableData) {
    self.commissionableData = newCommissionableData
}

3. Add device attestation credentials

This object contains the DeviceAttestationCertificate, ProductAttestationIntermediateCertificate, and more, that implements a way to sign messages when called by the Matter TV Casting Library as part of the Matter Device Attestation process during commissioning.

Add functions castingAppDidReceiveRequestForDeviceAttestationCredentials and didReceiveRequestToSignCertificateRequest to the MCAppParametersDataSource class defined above, which returns MCDeviceAttestationCredentials and sign messages for the Casting client.

Copied to clipboard.

// dummy DAC values for demonstration only
let kDevelopmentDAC_Cert_FFF1_8001: Data = Data(base64Encoded: "MIIB..<snipped>..CXE1M=")!;
let kDevelopmentDAC_PrivateKey_FFF1_8001: Data = Data(base64Encoded: "qrYA<snipped>tE+/8=")!;
let kDevelopmentDAC_PublicKey_FFF1_8001: Data = Data(base64Encoded: "BEY6<snipped>I=")!;
let KPAI_FFF1_8000_Cert_Array: Data = Data(base64Encoded: "MIIB<snipped>4kQ==")!;
let kCertificationDeclaration: Data = Data(base64Encoded: "MII<snipped>fA==")!;

func castingAppDidReceiveRequestForDeviceAttestationCredentials(_ sender: Any) -> MCDeviceAttestationCredentials {
    return MCDeviceAttestationCredentials(
        certificationDeclaration: kCertificationDeclaration,
        firmwareInformation: Data(),
        deviceAttestationCert: kDevelopmentDAC_Cert_FFF1_8001,
        productAttestationIntermediateCert: KPAI_FFF1_8000_Cert_Array)
}

func castingApp(_ sender: Any, didReceiveRequestToSignCertificateRequest csrData: Data, outRawSignature: AutoreleasingUnsafeMutablePointer<NSData>) -> MatterError {
    Log.info("castingApp didReceiveRequestToSignCertificateRequest")

    // get the private SecKey
    var privateKeyData = Data()
    privateKeyData.append(kDevelopmentDAC_PublicKey_FFF1_8001);
    privateKeyData.append(kDevelopmentDAC_PrivateKey_FFF1_8001);
    let privateSecKey: SecKey = SecKeyCreateWithData(privateKeyData as NSData,
                                [
                                    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
                                    kSecAttrKeyClass: kSecAttrKeyClassPrivate,
                                    kSecAttrKeySizeInBits: 256
                                ] as NSDictionary, nil)!

    // sign csrData to get asn1SignatureData
    var error: Unmanaged<CFError>?
    let asn1SignatureData: CFData? = SecKeyCreateSignature(privateSecKey, .ecdsaSignatureMessageX962SHA256, csrData as CFData, &error)
    if(error != nil)
    {
        Log.error("Failed to sign message. Error: \(String(describing: error))")
        return MATTER_ERROR_INVALID_ARGUMENT
    }
    else if (asn1SignatureData == nil)
    {
        Log.error("Failed to sign message. asn1SignatureData is nil")
        return MATTER_ERROR_INVALID_ARGUMENT
    }

    // convert ASN.1 DER signature to SEC1 raw format
    return MCCryptoUtils.ecdsaAsn1SignatureToRaw(withFeLengthBytes: 32,
                                                asn1Signature: asn1SignatureData!,
                                                     outRawSignature: &outRawSignature.pointee)
}

4. Initialize SDK

Once you have created the DataProvider objects above, you are ready to initialize the casting app as described below.

Call MCCastingApp.initialize with an object of MCAppParametersDataSource.

Copied to clipboard.

func initialize() -> MatterError {
    if let castingApp = MCCastingApp.getSharedInstance() {
        return castingApp.initialize(with: MCAppParametersDataSource())
    } else {
        return MATTER_ERROR_INCORRECT_STATE
    }
}

After initialization, call start and stop on the MCCastingApp shared instance when the app sends UIApplication.didBecomeActiveNotification and UIApplication.willResignActiveNotification.

Copied to clipboard.

struct TvCastingApp: App {
    let Log = Logger(subsystem: "com.matter.casting", category: "TvCastingApp")
    @State
    var firstAppActivation: Bool = true

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear(perform: {
                    let err: Error? = MCInitializationExample().initialize()
                    if err != nil
                    {
                        self.Log.error("MCCastingApp initialization failed \(err)")
                        return
                    }
                })
                .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
                    self.Log.info("TvCastingApp: UIApplication.didBecomeActiveNotification")
                    if let castingApp = MCCastingApp.getSharedInstance()
                    {
                        castingApp.start(completionBlock: { (err : Error?) -> () in
                            if err != nil
                            {
                                self.Log.error("MCCastingApp start failed \(err)")
                            }
                        })
                    }
                }
                .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
                    self.Log.info("TvCastingApp: UIApplication.willResignActiveNotification")
                    if let castingApp = MCCastingApp.getSharedInstance()
                    {
                        castingApp.stop(completionBlock: { (err : Error?) -> () in
                            if err != nil
                            {
                                self.Log.error("MCCastingApp stop failed \(err)")
                            }
                        })
                    }
                }
            }   // WindowGroup
    }   // body
}   // App

Step 2. Integrate Matter with your Fire TV app

After Integrate the Matter Casting SDK into your client app, your client is ready to discover players and content apps on the same Matter network. Before you can listen, send, and receive Matter commands from the content app on Fire TV you must integrate it with the Fire TV Matter Casting service. This step shows you how to use the Android TV example app to bootstrap a content app for Android TV that’s already Matter Casting-enabled.

1. Update your Android manifest

Enabling Matter Casting requires extending the information provided in your Android manifest. The player and the Matter Agent clientuse information provided in the Android manifest to understand which clients to allow casting to your content app and which Matter clusters the content app supports.

1. Product and vendor information

Update your app’s tag to include your product ID and vendor ID. See App Attestation for more information.

For development, you may use the Product ID of 65521, Vendor ID of 32769, and Vendor Name provided in the example below.

<meta-data android:name="com.matter.tv.application.api.vendor_id" android:value="65521" />
 <meta-data android:name="com.matter.tv.application.api.product_id" android:value="32769" />
 <meta-data android:name="com.matter.tv.app.api.vendor_name" android:value="MyName" />

2. Update permissions

To bind with the player’s Matter agent service, your content app must provide the following permissions in its manifest file.

<uses-permission android:name="com.matter.tv.app.api.permission.BIND_SERVICE_PERMISSION"/>
 <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission"/>

The permission string is defined within the common-api module as PERMISSION_MATTER_AGENT_BIND. AndroidManifest.xml can be used as a reference point.

3. Supported clusters

Next, to add the Clusters your content app supports, add the following to the Android manifest.

<meta-data android:name="com.matter.tv.application.api.clusters" android:resource="@raw/static_matter_clusters" />

Now create the JSON file static_matter_clusters that holds the list of clusters supported by your content app.

Copied to clipboard.

{
   "clusters": [
     // Account Login Cluster
     {
       "identifier": 1294
     },
     {
       "identifier": 1289,
       "features": [
         "NV",
         "LK",
         "NK"
       ]
     },
     {
       "identifier": 1290,
       "features": [
         "CS"
       ]
     },
     {
       "identifier": 1286,
       "features": [
         "AS"
       ]
     }
   ]
 }

2. Integrate with Matter AIDL files

Matter Casting uses the Android Interface Definition Language (AIDL) to define the programming interface to communicate between the player and your content app. Copy the following 3 AIDL files from the example app to your app’s repository: IMatterapplicationAgent.aidl, SetSupportedClustersRequest.aidl, and SupportedCluster.aidl. This will help you integrate with the following required methods setSupportedClusters and reportAttributeChange. These methods are necessary for the following:

  • setSupportedClusters
    • This method reports clusters dynamically to the Matter agent.
    • This is not incremental and on each call, report the full set of clusters. Any clusters that are omitted in the latest method call that were added previously will be removed.
    • The above behavior does not impact static clusters declared in app resources and they will not be removed.
    • Dynamic cluster can be used to override and hide a static cluster based on the cluster name.
  • reportAttributeChange
    • This method reports changes to attributes by the content app.
    • It also takes in the cluster ID and attribute ID for the reported attribute change.

Additional documentation can be found in the Matter tv app common-api.

3. Create a Matter agent client

Create a Matter Agent client as found in the example. The agent is used to connect to the Matter service and to use the methods provided in the AIDL step above.

Here’s an example of establishing a binder connection.

Copied to clipboard.

private synchronized boolean bindService(Context context) {
    ServiceConnection serviceConnection = new MyServiceConnection();
    final Intent intent = new Intent(MatterIntentConstants.ACTION_MATTER_AGENT);
    if (intent.getComponent() == null) {
        final ResolveInfo resolveInfo =
        resolveBindIntent(
            context,
            intent,
            MatterIntentConstants.PERMISSION_MATTER_AGENT_BIND,
            MatterIntentConstants.PERMISSION_MATTER_AGENT);
        if (resolveInfo == null) {
            Log.e(TAG, "No Service available on device to bind for intent " + intent);
            return false;
        }
        final ComponentName component =
        new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
        intent.setComponent(component);
    }

    try {
        Log.d(TAG, "Binding to service");
        latch = new CountDownLatch(1);
        return context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    } catch (final Throwable e) {
        Log.e(TAG, "Exception binding to service", e);
    }
    return false;
}

4. Register the Matter command receiver

The content app registers itself as a receiver in order to handle incoming Matter commands. To receive commands sent from the client, the content app registers a receiver instance that listens for MATTER_COMMAND intents with permission.

Copied to clipboard.

<!-- Intent action for receiving an Matter directive-->
<receiver
        android:name=".receiver.MatterCommandReceiver"
        android:permission="com.matter.tv.app.api.permission.SEND_DATA"
        android:enabled="true"
        android:exported="true">
    <intent-filter>
        <action android:name="com.matter.tv.app.api.action.MATTER_COMMAND" />
    </intent-filter>
</receiver>

In order to send data and respond to commands, include the permission com.matter.tv.app.api.permission.SEND_DATA for the receiver as seen above. Once you receive the intent, you can read the Matter command in your content app.

long commandId = intent.getLongExtra(MatterIntentConstants.EXTRA_COMMAND_ID, -1);
 long clusterId = intent.getLongExtra(MatterIntentConstants.EXTRA_CLUSTER_ID, -1);

Use this class to map command and cluster IDs.

5. Execute commands

Once the content app is started, it binds to the Matter Agent Service to receive commands and registers all its dynamic endpoints and supported clusters through the reportClusters call.

Copied to clipboard.

executorService.execute(() -> matterAgentClient.reportClusters(supportedClustersRequest));

If the content app needs to report an attribute change, use reportAttributeChange on the matter agent to notify the SDK.

When receiving a command from the Matter SDK, an intent of type ACTION_MATTER_COMMAND will be received using BroadcastReceiver implemented by the content app. The intent includes the command ID, cluster ID, and corresponding payload data. Once the command handler is called, a response for these commands must sent. An example receiver is found in MatterCommandReceiver.java. All the internal fields within the intents are found under MatterIntentConstants provided through the common-api module.

6. Install your Fire TV app on-demand

The following assumes that the customer has your content app installed and they are casting to it. However, this might not be the case. If the content app is not installed, Fire TV can prompt them to install the app if its package name is provided in the ApplicationID information of the launch app cluster payload.

Step 3. Interact with players

In this step you learn how to discover, select, and connect to a player, issue commands, read attributes like playback state, and subscribe to playback events.

Interaction on Android

1. Discover casting players

The client discovers players such as Fire TV using Matter Commissioner Discovery over DNS-SD by listening for player events as they are discovered, updated, or lost from the network.

First implement CastingPlayerDiscovery.CastingPlayerChangeListener.

Copied to clipboard.

private static final CastingPlayerDiscovery.CastingPlayerChangeListener castingPlayerChangeListener =
    new CastingPlayerDiscovery.CastingPlayerChangeListener() {
        private final String TAG = CastingPlayerDiscovery.CastingPlayerChangeListener.class.getSimpleName();

        @Override
        public void onAdded(CastingPlayer castingPlayer) {
            Log.i(TAG, "onAdded() Discovered CastingPlayer deviceId: " + castingPlayer.getDeviceId());
            // Display CastingPlayer info on the screen
            new Handler(Looper.getMainLooper()).post(() -> {
                arrayAdapter.add(castingPlayer);
            });
        }

        @Override
        public void onChanged(CastingPlayer castingPlayer) {
            Log.i(TAG, "onChanged() Discovered changes to CastingPlayer with deviceId: " + castingPlayer.getDeviceId());
            // Update the CastingPlayer on the screen
            new Handler(Looper.getMainLooper()).post(() -> {
                final Optional<CastingPlayer> playerInList = castingPlayerList.stream().filter(node -> castingPlayer.equals(node)).findFirst();
                if (playerInList.isPresent()) {
                    Log.d(TAG, "onChanged() Updating existing CastingPlayer entry " + playerInList.get().getDeviceId() + " in castingPlayerList list");
                    arrayAdapter.remove(playerInList.get());
                }
                arrayAdapter.add(castingPlayer);
            });
        }

        @Override
        public void onRemoved(CastingPlayer castingPlayer) {
            Log.i(TAG, "onRemoved() Removed CastingPlayer with deviceId: " + castingPlayer.getDeviceId());
            // Remove CastingPlayer from the screen
            new Handler(Looper.getMainLooper()).post(() -> {
                final Optional<CastingPlayer> playerInList = castingPlayerList.stream().filter(node -> castingPlayer.equals(node)).findFirst();
                if (playerInList.isPresent()) {
                    Log.d(TAG, "onRemoved() Removing existing CastingPlayer entry " + playerInList.get().getDeviceId() + " in castingPlayerList list");
                    arrayAdapter.remove(playerInList.get());
                }
            });
        }
};

Then register these listeners and start discovery. Add castingPlayerChangeListener as a listener to MatterCastingPlayerDiscovery to listen to changes in the discovered players and call startDiscovery.

Copied to clipboard.

MatterError err = MatterCastingPlayerDiscovery.getInstance().addCastingPlayerChangeListener(castingPlayerChangeListener);
if (err.hasError()) {
    Log.e(TAG, "startDiscovery() addCastingPlayerChangeListener() called, err Add: " + err);
    return false;
}

// Start discovery
Log.i(TAG, "startDiscovery() calling CastingPlayerDiscovery.startDiscovery()");
err = MatterCastingPlayerDiscovery.getInstance().startDiscovery(DISCOVERY_TARGET_DEVICE_TYPE);
if (err.hasError()) {
    Log.e(TAG, "Error in startDiscovery(): " + err);
    return false;
}

Connect with a player to see the list of endpoints they support. Refer to the Connection section for details on how to discover available endpoints supported by a casting player.

2. Connect with a discovered player

Each player object created during Discovery contains information such as deviceName, vendorId, and productId, which helps the user pick the right player. A client can attempt to connect to selectedCastingPlayer using Matter User Directed Commissioning (UDC). The Matter TV Casting library locally caches information required to reconnect to a player object once the client has been commissioned by it. After that, the Casting client is able to skip the full UDC process by establishing CASE with the player directly. Once connected, the Player object contains the list of available endpoints on that player, The following arguments may also be passed in as options. commissioningWindowTimeoutSec indicates how long to keep the commissioning window open, if commissioning is required. DesiredEndpointFilter specifies the attributes, such as vendor ID and product ID, of the endpoint the client desires to interact with after connecting. This forces the Matter TV Casting library to go through the full UDC process in search of the desired endpoint, in cases where it’s not available in the client’s cache.

The client may call verifyOrEstablishConnection on the CastingPlayer object it wants to connect to.

Copied to clipboard.

private static final short MIN_CONNECTION_TIMEOUT_SEC = 3 * 60;
private static final Integer DESIRED_TARGET_APP_VENDOR_ID = 65521;

// Specify the TargetApp that the client wants to interact with after commissioning. If this value is passed in,
// VerifyOrEstablishConnection() will force UDC, in case the desired TargetApp is not found in the on-device
// CastingStore
IdentificationDeclarationOptions idOptions = new IdentificationDeclarationOptions();
TargetAppInfo targetAppInfo = new TargetAppInfo(DESIRED_TARGET_APP_VENDOR_ID);
idOptions.addTargetAppInfo(targetAppInfo);

// If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature.
// Set the IdentificationDeclaration CommissionerPasscode flag to instruct the CastingPlayer /
// Commissioner to use the Commissioner-generated Passcode for commissioning.
idOptions = new IdentificationDeclarationOptions(commissionerPasscode:true);
idOptions.addTargetAppInfo(targetAppInfo);

ConnectionCallbacks connectionCallbacks =
    new ConnectionCallbacks(
        new MatterCallback<Void>() {
        @Override
        public void handle(Void v) {
            Log.i(
                TAG,
                "Successfully connected to CastingPlayer with deviceId: "
                    + targetCastingPlayer.getDeviceId());
            getActivity()
                .runOnUiThread(
                    () -> {
                    connectionFragmentStatusTextView.setText(
                        "Successfully connected to Casting Player with device name: "
                            + targetCastingPlayer.getDeviceName()
                            + "\n\n");
                    connectionFragmentNextButton.setEnabled(true);
                    });
        }
        },
        new MatterCallback<MatterError>() {
        @Override
        public void handle(MatterError err) {
            Log.e(TAG, "CastingPlayer connection failed: " + err);
            getActivity()
                .runOnUiThread(
                    () -> {
                    connectionFragmentStatusTextView.setText(
                        "Casting Player connection failed due to: " + err + "\n\n");
                    });
        }
        },
        // If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature.
        // Define a callback to handle CastingPlayer’s CommissionerDeclaration messages.
        // This can be null if using Casting Client / Commissionee generated passcode commissioning.
        new MatterCallback<CommissionerDeclaration>() {
        @Override
        public void handle(CommissionerDeclaration cd) {
            getActivity()
                .runOnUiThread(
                    () -> {
                        connectionFragmentStatusTextView.setText(
                            "CommissionerDeclaration message received from Casting Player: \n\n");
                        if (cd.getCommissionerPasscode()) {

                        displayPasscodeInputDialog(getActivity());
                        ...

                        // Update the commissioning session's passcode with the user-entered Passcode
                        InitializationExample.commissionableDataProvider.updateCommissionableDataSetupPasscode(
                                        passcodeLongValue, DEFAULT_DISCRIMINATOR_FOR_CGP_FLOW);

                        // Call continueConnecting to complete commissioning.
                        MatterError err = targetCastingPlayer.continueConnecting();
                        if (err.hasError()) {
                            ...
                            Log.e(
                                TAG,
                                "displayPasscodeInputDialog() continueConnecting() failed, calling stopConnecting() due to: "
                                    + err);
                            // Since continueConnecting() failed, Attempt to cancel the connection attempt with
                            // the CastingPlayer/Commissioner by calling stopConnecting().
                            err = targetCastingPlayer.stopConnecting();
                            if (err.hasError()) {
                                Log.e(TAG, "displayPasscodeInputDialog() stopConnecting() failed due to: " + err);
                            }
                            }
                        }
                    });
            }
        }
    );

MatterError err = targetCastingPlayer.verifyOrEstablishConnection(
        connectionCallbacks, MIN_CONNECTION_TIMEOUT_SEC, idOptions);
if (err.hasError())
{
    getActivity()
        .runOnUiThread(
            () -> {
                connectionFragmentStatusTextView.setText(
                    "Casting Player connection failed due to: " + err + "\n\n");
            });
}

3. Select an endpoint on the player

On a successful connection with a player object, a casting client may select one of the endpoints to interact with based on its attributes, such as the vendor ID, product ID, or list of supported clusters. In the context of Matter Casting, endpoints refers to your content app.

Copied to clipboard.

private static final Integer SAMPLE_ENDPOINT_VID = 65521;

public static Endpoint selectFirstEndpointByVID(CastingPlayer selectedCastingPlayer) {
Endpoint endpoint = null;
if (selectedCastingPlayer != null) {
    List<Endpoint> endpoints = selectedCastingPlayer.getEndpoints();
    if (endpoints == null) {
    Log.e(TAG, "selectFirstEndpointByVID() No Endpoints found on CastingPlayer");
    } else {
    endpoint =
        endpoints
            .stream()
            .filter(e -> SAMPLE_ENDPOINT_VID.equals(e.getVendorId()))
            .findFirst()
            .orElse(null);
    }
}
return endpoint;
}

4. Interacting with a casting endpoint

Once the Casting client has selected an endpoint, it is ready to issue commands to it, read the current playback state, and subscribe to playback events. The list of supported clusters, commands, and attributes on Android can be found in the example code.

Issuing commands

Given the endpoint from the previous step, you can send a LaunchURL command (part of the Content Launcher cluster) by calling the launchURL method on a ChipClusters.ContentLauncherCluster object.

Copied to clipboard.

// get ChipClusters.ContentLauncherCluster from the endpoint
ChipClusters.ContentLauncherCluster cluster =
    endpoint.getCluster(ChipClusters.ContentLauncherCluster.class);
if (cluster == null) {
    Log.e(TAG, "Could not get ContentLauncherCluster for endpoint with ID: " + endpoint.getId());
    return;
}

// call launchURL on the cluster object while passing in a
// ChipClusters.ContentLauncherCluster.LauncherResponseCallback and request parameters
cluster.launchURL(
    new ChipClusters.ContentLauncherCluster.LauncherResponseCallback() {
    @Override
    public void onSuccess(Integer status, Optional<String> data) {
        Log.d(TAG, "LaunchURL success. Status: " + status + ", Data: " + data);
        new Handler(Looper.getMainLooper())
            .post(
                () -> {
                TextView launcherResult = getView().findViewById(R.id.launcherResult);
                launcherResult.setText(
                    "LaunchURL result\nStatus: " + status + ", Data: " + data);
                });
    }

    @Override
    public void onError(Exception error) {
        Log.e(TAG, "LaunchURL failure " + error);
        new Handler(Looper.getMainLooper())
            .post(
                () -> {
                TextView launcherResult = getView().findViewById(R.id.launcherResult);
                launcherResult.setText("LaunchURL result\nError: " + error);
                });
    }
    },
    contentUrl,
    Optional.of(contentDisplayString),
    Optional.empty());

Read operations and subscriptions

The following demonstrates how to read data from your content app and update the client user interface to represent the current playback position.

In this example the customer subscribes by calling subscribeCurrentStateAttribute on a ChipClusters.MediaPlaybackCluster object.

Copied to clipboard.

// get ChipClusters.MediaPlaybackCluster from the endpoint
ChipClusters.MediaPlaybackCluster cluster =
    endpoint.getCluster(ChipClusters.MediaPlaybackCluster.class);
if (cluster == null) {
Log.e(TAG,
    "Could not get ApplicationBasicCluster for endpoint with ID: "
        + endpoint.getId());
return;
}

// call subscribeCurrentStateAttribute on the cluster object while passing in a
// ChipClusters.IntegerAttributeCallback and [0, 1] for min and max interval
// params
cluster.subscribeCurrentStateAttribute(
    new ChipClusters.IntegerAttributeCallback() {
    @Override
    public void onSuccess(int value) {
        Log.d(TAG,
            "Read success on subscription. Value: " + value + " @ "
                + new Date());
        new Handler(Looper.getMainLooper()).post(() -> {
        TextView currentStateResult =
            getView().findViewById(R.id.currentStateResult);
        currentStateResult.setText("Current State result\nValue: " + value);
        });
    }

    @Override
    public void onError(Exception error) {
        Log.e(TAG, "Read failure on subscription: " + error);
        new Handler(Looper.getMainLooper()).post(() -> {
        TextView currentStateResult =
            getView().findViewById(R.id.currentStateResult);
        currentStateResult.setText("Current State result\nError: " + error);
        });
    }
    },
    0, 1);

Interaction on iOS

1. Discover casting players

Now that you have integrated the Matter library into your client and in the content app, you can discover and communicate with video casting players and content apps on the same Matter fabric.

Implement func addDiscoveredCastingPlayers, func removeDiscoveredCastingPlayers and func updateDiscoveredCastingPlayers which listens to notifications as casting players are added, removed, or updated.

Copied to clipboard.

@objc
func didAddDiscoveredCastingPlayers(notification: Notification)
{
    Log.info("didAddDiscoveredCastingPlayers() called")
    guard let userInfo = notification.userInfo,
        let castingPlayer     = userInfo["castingPlayer"] as? MCCastingPlayer else {
        self.Log.error("didAddDiscoveredCastingPlayers called with no MCCastingPlayer")
        return
    }

    self.Log.info("didAddDiscoveredCastingPlayers notified of a MCCastingPlayer with ID: \(castingPlayer.identifier())")
    DispatchQueue.main.async
    {
        self.displayedCastingPlayers.append(castingPlayer)
    }
}

@objc
func didRemoveDiscoveredCastingPlayers(notification: Notification)
{
    Log.info("didRemoveDiscoveredCastingPlayers() called")
    guard let userInfo = notification.userInfo,
        let castingPlayer     = userInfo["castingPlayer"] as? MCCastingPlayer else {
        self.Log.error("didRemoveDiscoveredCastingPlayers called with no MCCastingPlayer")
        return
    }

    self.Log.info("didRemoveDiscoveredCastingPlayers notified of a MCCastingPlayer with ID: \(castingPlayer.identifier())")
    DispatchQueue.main.async
    {
        self.displayedCastingPlayers.removeAll(where: {$0 == castingPlayer})
    }
}

@objc
func didUpdateDiscoveredCastingPlayers(notification: Notification)
{
    Log.info("didUpdateDiscoveredCastingPlayers() called")
    guard let userInfo = notification.userInfo,
        let castingPlayer     = userInfo["castingPlayer"] as? MCCastingPlayer else {
        self.Log.error("didUpdateDiscoveredCastingPlayers called with no MCCastingPlayer")
        return
    }

    self.Log.info("didUpdateDiscoveredCastingPlayers notified of a MCCastingPlayer with ID: \(castingPlayer.identifier())")
    if let index = displayedCastingPlayers.firstIndex(where: { castingPlayer.identifier() == $0.identifier() })
    {
        DispatchQueue.main.async
        {
            self.displayedCastingPlayers[index] = castingPlayer
        }
    }
}

Register these listeners to start discovery by calling addObserver in NotificationCenter with the appropriate selector, and then call start on the sharedInstance object of MCCastingPlayerDiscovery.

Copied to clipboard.

func startDiscovery() {
    NotificationCenter.default.addObserver(self, selector: #selector(self.didAddDiscoveredCastingPlayers), name: NSNotification.Name.didAddCastingPlayers, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.didRemoveDiscoveredCastingPlayers), name: NSNotification.Name.didRemoveCastingPlayers, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(self.didUpdateDiscoveredCastingPlayers), name: NSNotification.Name.didUpdateCastingPlayers, object: nil)

    MCCastingPlayerDiscovery.sharedInstance().start()
    ...
}

2. Connect to a casting player

Each CastingPlayer object created during discovery contains information such as deviceName, vendorId, and productId, which helps the user pick the right CastingPlayer. A Casting client can attempt to connect to selectedCastingPlayer using Matter User Directed Commissioning (UDC). The Matter TV Casting library locally caches information required to reconnect to a CastingPlayer object once the Casting client has been commissioned by it. After that, the Casting client can skip the full UDC process by establishing CASE with the CastingPlayer object directly. Once connected, the CastingPlayer object contains the list of available endpoints on that CastingPlayer object. commissioningWindowTimeoutSec indicates how long to keep the commissioning window open, if it’s required. DesiredEndpointFilter specifies the attributes, such as the vendor ID and product ID of the endpoint the Casting client wants to interact with after connecting. This forces the Matter TV Casting library to go through the full UDC process in search of the desired endpoint, in cases where it’s not available in the casting client’s cache.

The Casting client may call verifyOrEstablishConnection on the MCCastingPlayer object it wants to connect to and handle any errors through NSErrors that may happen in the process.

Copied to clipboard.

// VendorId of the MCEndpoint on the MCCastingPlayer that the MCCastingApp desires to interact with after connection
let kDesiredEndpointVendorId: UInt16 = 65521;

@Published var connectionSuccess: Bool?;

@Published var connectionStatus: String?;

func connect(selectedCastingPlayer: MCCastingPlayer?) {

    let connectionCompleteCallback: (Swift.Error?) -> Void = { err in
        self.Log.error("MCConnectionExampleViewModel connect() completed with: \(err)")
        DispatchQueue.main.async {
            if err == nil {
                self.connectionSuccess = true
                self.connectionStatus = "Successfully connected to Casting Player!"
            } else {
                self.connectionSuccess = false
                self.connectionStatus = "Connection to Casting Player failed with: \(String(describing: err))"
            }
        }
    }

    // If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature.
    // Define a callback to handle CastingPlayer’s CommissionerDeclaration messages.
    let commissionerDeclarationCallback: (MCCommissionerDeclaration) -> Void = { commissionerDeclarationMessage in
        DispatchQueue.main.async {
            self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, recived a message form the MCCastingPlayer:\n\(commissionerDeclarationMessage)")
            if commissionerDeclarationMessage.commissionerPasscode {
                if let topViewController = self.getTopMostViewController() {
                    self.displayPasscodeInputDialog(on: topViewController, continueConnecting: { userEnteredPasscode in
                        self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Continuing to connect with user entered MCCastingPlayer/Commissioner-Generated passcode: \(passcode)")

                        // Update the commissioning session's passcode with the user-entered Passcode
                        if let dataSource = initializationExample.getAppParametersDataSource() {
                            let newCommissionableData = MCCommissionableData(
                                passcode: UInt32(userEnteredPasscode) ?? 0,
                                discriminator: 0,
                                ...
                            )
                            dataSource.update(newCommissionableData)
                            ...
                        } else {
                            self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed, calling stopConnecting()")
                            self.connectionStatus = "Failed to update the MCAppParametersDataSource with the user entered passcode: \n\nRoute back and try again."
                            self.connectionSuccess = false
                            // Since we failed to update the passcode, attempt to cancel the connection attempt with
                            // the CastingPlayer/Commissioner.
                            let err = selectedCastingPlayer?.stopConnecting()
                            if err == nil {
                                self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed, then stopConnecting() succeeded")
                            } else {
                                self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed, then stopConnecting() failed due to: \(err)")
                            }
                            return
                        }

                        // Call continueConnecting to complete commissioning.
                        let errContinue = selectedCastingPlayer?.continueConnecting()
                        if errContinue == nil {
                            self.connectionStatus = "Continuing to connect with user entered passcode: \(userEnteredPasscode)"
                        } else {
                            self.connectionStatus = "Continue Connecting to Casting Player failed with: \(String(describing: errContinue)) \n\nRoute back and try again."
                            self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed due to: \(errContinue)")
                            // Since continueConnecting() failed, Attempt to cancel the connection attempt with
                            // the CastingPlayer/Commissioner by calling stopConnecting().
                            let err = selectedCastingPlayer?.stopConnecting()
                            if err == nil {
                                self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed, then stopConnecting() succeeded")
                            } else {
                                self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed, then stopConnecting() failed due to: \(err)")
                            }
                        }
                    }, cancelConnecting: {
                        self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Connection attempt cancelled by the user, calling MCCastingPlayer.stopConnecting()")
                        let err = selectedCastingPlayer?.stopConnecting()
                        ...
                    })
                }
            }
        }
    }

    let identificationDeclarationOptions: MCIdentificationDeclarationOptions
    let targetAppInfo: MCTargetAppInfo
    let connectionCallbacks: MCConnectionCallbacks

    // Specify the TargetApp that the client wants to interact with after commissioning. If this value is passed in,
    // VerifyOrEstablishConnection() will force UDC, in case the desired TargetApp is not found in the on-device
    // CastingStore
    identificationDeclarationOptions = MCIdentificationDeclarationOptions()
    targetAppInfo = MCTargetAppInfo(vendorId: kDesiredEndpointVendorId)
    connectionCallbacks = MCConnectionCallbacks(
        callbacks: connectionCompleteCallback,
        commissionerDeclarationCallback: nil
    )
    identificationDeclarationOptions.addTargetAppInfo(targetAppInfo)

    // If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature.
    // Set the IdentificationDeclaration CommissionerPasscode flag to instruct the CastingPlayer /
    // Commissioner to use the Commissioner-generated Passcode for commissioning. Set the
    // CommissionerDeclarationCallback in MCConnectionCallbacks.
    identificationDeclarationOptions = MCIdentificationDeclarationOptions(commissionerPasscodeOnly: true)
    targetAppInfo = MCTargetAppInfo(vendorId: kDesiredEndpointVendorId)
    connectionCallbacks = MCConnectionCallbacks(
        callbacks: connectionCompleteCallback,
        commissionerDeclarationCallback: commissionerDeclarationCallback
    )
    identificationDeclarationOptions.addTargetAppInfo(targetAppInfo)

    let err = selectedCastingPlayer?.verifyOrEstablishConnection(with: connectionCallbacks, identificationDeclarationOptions: identificationDeclarationOptions)
    if err != nil {
        self.Log.error("MCConnectionExampleViewModel connect(), MCCastingPlayer.verifyOrEstablishConnection() failed due to: \(err)")
    }
}

3. Select a casting player endpoint

Once successfully connected with a CastingPlayer object, a casting client may select one of the endpoints to interact with based on its attributes (such as vendor ID, product ID, or list of supported clusters). Select MCEndpoint as shown below.

Copied to clipboard.

// VendorId of the MCEndpoint on the MCCastingPlayer that the MCCastingApp desires to interact with after connection
let sampleEndpointVid: Int = 65521
...
// select the MCEndpoint on the MCCastingPlayer to invoke the command on
if let endpoint: MCEndpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid}).first
{
...
}

4. Interacting with a casting endpoint

Once the casting client has selected an endpoint, it is ready to issue commands to it, read current playback state, and subscribe to playback events. Refer to the following platform specific files to find the list of clusters, commands, and attributes with their request and response types available for use with the Matter TV casting library.

Refer to the following files:

5. Issuing commands

Given an endpoint (MCEndpoint), it can send a LaunchURL command, which is part of the Content Launcher cluster, by calling the invoke method on MCContentLauncherClusterLaunchURLCommand.

Copied to clipboard.

// validate that the selected endpoint supports the ContentLauncher cluster
if(!endpoint.hasCluster(MCEndpointClusterTypeContentLauncher))
{
    self.Log.error("No ContentLauncher cluster supporting endpoint found")
    DispatchQueue.main.async
    {
        self.status = "No ContentLauncher cluster supporting endpoint found"
    }
    return
}

// get ContentLauncher cluster from the endpoint
let contentLaunchercluster: MCContentLauncherCluster = endpoint.cluster(for: MCEndpointClusterTypeContentLauncher) as! MCContentLauncherCluster

// get the launchURLCommand from the contentLauncherCluster
let launchURLCommand: MCContentLauncherClusterLaunchURLCommand? = contentLaunchercluster.launchURLCommand()
if(launchURLCommand == nil)
{
    self.Log.error("LaunchURL not supported on cluster")
    DispatchQueue.main.async
    {
        self.status = "LaunchURL not supported on cluster"
    }
    return
}

// create the LaunchURL request
let request: MCContentLauncherClusterLaunchURLParams = MCContentLauncherClusterLaunchURLParams()
request.contentURL = contentUrl
request.displayString = displayString

// call invoke on launchURLCommand while passing in a completion block
launchURLCommand!.invoke(request, context: nil, completion: { context, err, response in
    DispatchQueue.main.async
    {
        if(err == nil)
        {
            self.Log.info("LaunchURLCommand invoke completion success with \(String(describing: response))")
            self.status = "Success. Response data: \(String(describing: response?.data))"
        }
        else
        {
            self.Log.error("LaunchURLCommand invoke completion failure with \(String(describing: err))")
            self.status = "Failure: \(String(describing: err))"
        }
    }
},
timedInvokeTimeoutMs: 5000) // time out after 5000ms

6. Read operations

CastingClient may read an attribute from the endpoint on the CastingPlayer object. It should ensure that the desired cluster and attribute are available for reading on the endpoint before trying to read it.

Within MCEndpoint, the VendorID can be read similarly, by calling the read method on MCApplicationBasicClusterVendorIDAttribute.

Copied to clipboard.

// validate that the selected endpoint supports the ApplicationBasic cluster
if(!endpoint.hasCluster(MCEndpointClusterTypeApplicationBasic))
{
    self.Log.error("No ApplicationBasic cluster supporting endpoint found")
    DispatchQueue.main.async
    {
        self.status = "No ApplicationBasic cluster supporting endpoint found"
    }
    return
}

// get ApplicationBasic cluster from the endpoint
let applicationBasiccluster: MCApplicationBasicCluster = endpoint.cluster(for: MCEndpointClusterTypeApplicationBasic) as! MCApplicationBasicCluster

// get the vendorIDAttribute from the applicationBasiccluster
let vendorIDAttribute: MCApplicationBasicClusterVendorIDAttribute? = applicationBasiccluster.vendorIDAttribute()
if(vendorIDAttribute == nil)
{
    self.Log.error("VendorID attribute not supported on cluster")
    DispatchQueue.main.async
    {
        self.status = "VendorID attribute not supported on cluster"
    }
    return
}

// call read on vendorIDAttribute and pass in a completion block
vendorIDAttribute!.read(nil) { context, before, after, err in
    DispatchQueue.main.async
    {
        if(err != nil)
        {
            self.Log.error("Error when reading VendorID value \(String(describing: err))")
            self.status = "Error when reading VendorID value \(String(describing: err))"
            return
        }

        if(before != nil)
        {
            self.Log.info("Read VendorID value: \(String(describing: after)) Before: \(String(describing: before))")
            self.status = "Read VendorID value: \(String(describing: after)) Before: \(String(describing: before))"
        }
        else
        {
            self.Log.info("Read VendorID value: \(String(describing: after))")
            self.status = "Read VendorID value: \(String(describing: after))"
        }
    }
}

Last updated: Jan 26, 2024