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
- Step 2. Integrate Matter with your Fire TV app
- Step 3. Interact with players
- Related topics
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.
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.
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.
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.
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.
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.
// 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.
// 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
.
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
.
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.
{
"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.
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.
<!-- 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.
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_COMMAN
D 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
.
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
.
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.
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.
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.
// 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.
// 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.
@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
.
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.
// 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.
// 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:
- For a list of supported clusters, commands, and attributes: MCClusterObjects.h.
- For the IDs and request and response types to use with the commands: MCCommandObjects.h and MCCommandPayloads.h.
- For attribute read operations: Read Operations and subscriptions: MCAttributeObjects.h.
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
.
// 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
.
// 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))"
}
}
}
Related topics
Last updated: Jan 26, 2024