Matter Casting APIs#
Matter Casting consists of three parts:
The mobile app: For most content providers, this would be your consumer-facing mobile app. By making your mobile app a Matter “Casting Client”, you enable the user to discover casting targets, cast content, and control casting sessions. The example Matter tv-casting-app for Android / iOS and Linux builds on top of the Matter SDK to demonstrate how a TV Casting mobile app works.
The TV content app: For most content providers, this would be your consumer-facing app on a Smart TV. By enhancing your TV app to act as a Matter “Content app”, you enable Matter Casting Clients to cast content. The example Matter content-app for Android builds on top of the Matter SDK to demonstrate how a TV Content app works.
The TV platform app: The TV platform app implements the Casting Video
Player device type and provides common capabilities around media playback on the TV such as play/pause, keypad navigation, input and output control, content search, and an implementation of an app platform as described in the media chapter of the device library specification. This is generally implemented by the TV manufacturer. The example Matter tv-app for Android builds on top of the Matter SDK to demonstrate how a TV platform app works.
This document describes how enable your Android and iOS apps to act as a Matter “Casting Client”. This documentation is also designed to work with the example example Matter tv-casting-app samples so you can see the experience end to end.
Introduction#
A Casting Client (e.g. a mobile phone app) is expected to be a Matter
Commissionable Node and a CastingPlayer
(i.e. a TV) is expected to be a Matter
Commissioner. In the context of the Matter Video Player architecture, a
CastingPlayer
would map to Casting “Video” Player. The CastingPlayer
is
expected to be hosting one or more Endpoints
(some of which can represent
Content Apps in the Matter Video Player architecture) that support one or more
Matter Media Clusters
.
The steps to start a casting session are:
Initialize the
CastingClient
using the Matter SDK.Discover
CastingPlayer
devices using Matter Commissioner discovery.Connect to the
CastingPlayer
to discover available endpoints. By connecting, theCastingClient
will send a User Directed Commissioning (UDC) request to theCastingPlayer
device in order to make a Matter commissioning request. TheCastingPlayer
will then obtain the appropriate user consent to allow a connection from thisCastingClient
and obtain the setup code needed to commission theCastingClient
. The setup code will typically come from a corresponding TV content app or be input by the user.Select an available
Endpoint
hosted by theCastingPlayer
.
Next, you’re ready to:
Issue commands to the
Endpoint
.Read endpoint attributes like playback state.
Subscribe to playback events.
In order to illustrate these steps, refer to the figure below
Build and Setup#
The Casting Client is expected to consume the Matter TV Casting library built
for its respective platform which implements the APIs described in this
document. Refer to the tv-casting-app READMEs for Linux,
Android and iOS to understand how to build and
consume each platform’s specific libraries. The libraries MUST be built with the
client’s specific values for CHIP_DEVICE_CONFIG_DEVICE_VENDOR_ID
and
CHIP_DEVICE_CONFIG_DEVICE_PRODUCT_ID
updated in the
CHIPProjectAppConfig.h file.
Other values like the CHIP_DEVICE_CONFIG_DEVICE_NAME
may be updated as well to
correspond to the client being built.
Initialize the Casting Client#
{Complete Initialization examples: Linux | Android | iOS}
A Casting Client must first initialize the Matter SDK and define the following
DataProvider
objects for the the Matter Casting library to use throughout the
client’s lifecycle:
Rotating Device Identifier - “This unique per-device identifier SHALL consist of a randomly-generated 128-bit or longer octet string.” Refer to the Matter specification for more details. Instantiate a
DataProvider
object as described below to provide this identifier.On Linux, define a
RotatingDeviceIdUniqueIdProvider
to provide the Casting Client’sRotatingDeviceIdUniqueId
, by implementing amatter:casting::support::MutableByteSpanDataProvider
:class RotatingDeviceIdUniqueIdProvider : public MutableByteSpanDataProvider { private: chip::MutableByteSpan rotatingDeviceIdUniqueIdSpan; uint8_t rotatingDeviceIdUniqueId[chip::DeviceLayer::ConfigurationManager::kRotatingDeviceIDUniqueIDLength]; public: RotatingDeviceIdUniqueIdProvider() { // generate a random Unique ID for this example app for demonstration for (size_t i = 0; i < sizeof(rotatingDeviceIdUniqueId); i++) { rotatingDeviceIdUniqueId[i] = chip::Crypto::GetRandU8(); } rotatingDeviceIdUniqueIdSpan = chip::MutableByteSpan(rotatingDeviceIdUniqueId); } chip::MutableByteSpan * Get() { return &rotatingDeviceIdUniqueIdSpan; } };
On Android, define a
rotatingDeviceIdUniqueIdProvider
to provide the Casting Client’sRotatingDeviceIdUniqueId
, by implementing acom.matter.casting.support.DataSource
: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(); } };
On iOS, define the
func castingAppDidReceiveRequestForRotatingDeviceIdUniqueId
in a class,MCAppParametersDataSource
, that implements theMCDataSource
: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)! } ... }
Commissioning Data - This object contains the passcode, discriminator, etc. which identify the app and are provided to the
CastingPlayer
during the commissioning process. “A Passcode SHALL be included as a 27-bit unsigned integer, which serves as proof of possession during commissioning.” “A Discriminator SHALL be included as a 12-bit unsigned integer, which SHALL match the value which a device advertises during commissioning.” Refer to the Matter specification’s “Onboarding Payload” section for more details on commissioning data.For the optional
CastingPlayer
/ Commissioner-Generated Passcode User Directed Commissioning (UDC) feature, the CommissioningDataProvider
needs to be updated during the commissioning process. In this scenario, theCastingPlayer
generates a Passcode and displays it for the user. The user enters the Passcode on the UX of the Casting Client which should update its CommissioningDataProvider
. This allows the Matter Casting Library to run commissioning with theCastingPlayer
using a PAKE verifier based on the user-entered passcode. See the Matter specification’s UDC section for more information on the Commissioner-Generated Passcode feature.On Linux, define a function
InitCommissionableDataProvider
to initialize aLinuxCommissionableDataProvider
that can provide the required values to theCastingApp
.CHIP_ERROR InitCommissionableDataProvider(LinuxCommissionableDataProvider & provider, LinuxDeviceOptions & options) { chip::Optional<uint32_t> setupPasscode; if (options.payload.setUpPINCode != 0) { setupPasscode.SetValue(options.payload.setUpPINCode); } else if (!options.spake2pVerifier.HasValue()) { // default to TestOnlyCommissionableDataProvider for demonstration uint32_t defaultTestPasscode = 0; chip::DeviceLayer::TestOnlyCommissionableDataProvider TestOnlyCommissionableDataProvider; VerifyOrDie(TestOnlyCommissionableDataProvider.GetSetupPasscode(defaultTestPasscode) == CHIP_NO_ERROR); setupPasscode.SetValue(defaultTestPasscode); options.payload.setUpPINCode = defaultTestPasscode; } uint32_t spake2pIterationCount = chip::Crypto::kSpake2p_Min_PBKDF_Iterations; if (options.spake2pIterations != 0) { spake2pIterationCount = options.spake2pIterations; } return provider.Init(options.spake2pVerifier, options.spake2pSalt, spake2pIterationCount, setupPasscode, options.payload.discriminator.GetLongValue()); }
On Linux, if using the
CastingPlayer
/ Commissioner-Generated Passcode UDC feature, set up a newLinuxCommissionableDataProvider
when called back on theCommissionerDeclarationCallback
during the VerifyOrEstablishConnection() API call (described later). TheCastingPlayer
generated passcode (as entered by the user on the Casting Client UX) should be set in thisLinuxCommissionableDataProvider
which should then be passed to the CastingApp using theUpdateCommissionableDataProvider
API.LinuxDeviceOptions::GetInstance().payload.setUpPINCode = userEnteredPasscode; LinuxCommissionableDataProvider gCommissionableDataProvider; CHIP_ERROR err = CHIP_NO_ERROR; err = InitCommissionableDataProvider(gCommissionableDataProvider, LinuxDeviceOptions::GetInstance()); if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "CommandHandler() setcommissionerpasscode InitCommissionableDataProvider() err %" CHIP_ERROR_FORMAT, err.Format()); return err; } err = matter::casting::core::CastingApp::GetInstance()->UpdateCommissionableDataProvider(&gCommissionableDataProvider); if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "CommandHandler() setcommissionerpasscode InitCommissionableDataProvider() err %" CHIP_ERROR_FORMAT, err.Format()); return err; }
On Android, define a
commissioningDataProvider
that can provide the required values to theCastingApp
. If using theCastingPlayer
/ Commissioner-Generated Passcode UDC feature, the Casting Client needs to update thiscommissioningDataProvider
during the verifyOrEstablishConnection() API call (described later). In the example below,updateCommissionableDataSetupPasscode
updates the CommissionableData with theCastingPlayer
generated passcode entered by the user on the Casting Client UX.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); } };
On iOS, add a
func commissioningDataProvider
to theMCAppParametersDataSource
class defined above, that can provide the required values to theMCCastingApp
. If using theCastingPlayer
/ Commissioner-Generated Passcode UDC feature, the Casting Client needs to update thiscommissioningDataProvider
during the VerifyOrEstablishConnection() API call (described later). In the example below, theupdate
function updates the CommissionableData with theCastingPlayer
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 }
Device Attestation Credentials - This object contains the
DeviceAttestationCertificate
,ProductAttestationIntermediateCertificate
, etc. and implements a way to sign messages when called upon by the Matter TV Casting Library as part of the Matter Device Attestation process during commissioning.On Linux, implement a define a
dacProvider
to provide the Casting Client’s Device Attestation Credentials, by implementing achip::Credentials::DeviceAttestationCredentialsProvider
. For this example, we will use thechip::Credentials::Examples::ExampleDACProvider
On Android, define a
dacProvider
to provide the Casting Client’s Device Attestation Credentials, by implementing acom.matter.casting.support.DACProvider
:private static final DACProvider dacProvider = new DACProviderStub(); private final static 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; } };
On iOS, add functions
castingAppDidReceiveRequestForDeviceAttestationCredentials
anddidReceiveRequestToSignCertificateRequest
to theMCAppParametersDataSource
class defined above, that can returnMCDeviceAttestationCredentials
and sign messages for the Casting Client, respectively.// 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) }
Once you have created the DataProvider
objects above, you are ready to
initialize the Casting App as described below. Note: When you initialize the
Casting client, make sure your code initializes it only once, before it starts a
Matter casting session.
On Linux, create an AppParameters
object using the
RotatingDeviceIdUniqueIdProvider
, LinuxCommissionableDataProvider
,
CommonCaseDeviceServerInitParamsProvider
, ExampleDACProvider
and
DefaultDACVerifier
, and call CastingApp::GetInstance()->Initialize
with it.
Then, call Start
on the CastingApp
.
LinuxCommissionableDataProvider gCommissionableDataProvider;
int main(int argc, char * argv[]) {
// Create AppParameters that need to be passed to CastingApp.Initialize()
AppParameters appParameters;
RotatingDeviceIdUniqueIdProvider rotatingDeviceIdUniqueIdProvider;
CommonCaseDeviceServerInitParamsProvider serverInitParamsProvider;
CHIP_ERROR err = CHIP_NO_ERROR;
err = InitCommissionableDataProvider(gCommissionableDataProvider, LinuxDeviceOptions::GetInstance());
VerifyOrReturnValue(
err == CHIP_NO_ERROR, 0,
ChipLogError(AppServer, "Initialization of CommissionableDataProvider failed %" CHIP_ERROR_FORMAT, err.Format()));
err = appParameters.Create(&rotatingDeviceIdUniqueIdProvider, &gCommissionableDataProvider,
chip::Credentials::Examples::GetExampleDACProvider(),
GetDefaultDACVerifier(chip::Credentials::GetTestAttestationTrustStore()), &serverInitParamsProvider);
VerifyOrReturnValue(err == CHIP_NO_ERROR, 0,
ChipLogError(AppServer, "Creation of AppParameters failed %" CHIP_ERROR_FORMAT, err.Format()));
// Initialize the CastingApp
err = CastingApp::GetInstance()->Initialize(appParameters);
VerifyOrReturnValue(err == CHIP_NO_ERROR, 0,
ChipLogError(AppServer, "Initialization of CastingApp failed %" CHIP_ERROR_FORMAT, err.Format()));
// Initialize Linux KeyValueStoreMgr
chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl().Init(CHIP_CONFIG_KVS_PATH);
// Start the CastingApp
err = CastingApp::GetInstance()->Start();
VerifyOrReturnValue(err == CHIP_NO_ERROR, 0,
ChipLogError(AppServer, "CastingApp::Start failed %" CHIP_ERROR_FORMAT, err.Format()));
...
}
On Android, create an AppParameters
object using the
rotatingDeviceIdUniqueIdProvider
, commissioningDataProvider
, dacProvider
and DataProvider<ConfigurationManager>
, and call
CastingApp.getInstance().initialize
with it. Then, call start
on the
CastingApp
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 CastingApp");
return err;
}
return err;
}
On iOS, call MCCastingApp.initialize
with an object of the
MCAppParametersDataSource
.
func initialize() -> MatterError {
if let castingApp = MCCastingApp.getSharedInstance() {
return castingApp.initialize(with: MCAppParametersDataSource())
} else {
return MATTER_ERROR_INCORRECT_STATE
}
}
After initialization, on iOS, call start
and stop
on the MCCastingApp
shared instance when the App sends the
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
Note about on-device cache: The Casting App maintains an on-device cache
containing information about the Casting Players it has connected with so far.
This cached information allows the Casting App to connect with Casting Players
(that it had previously connected with) faster and using fewer resources, by
potentially skipping the longer commissioning process and instead, simply
re-establishing the CASE session. This cache can be cleared by calling the
ClearCache
API on the CastingApp
, say when the user signs out of the app.
See API and its documentation for Linux,
Android
and iOS.
Discover Casting Players#
{Complete Discovery examples: Linux | Android | iOS}
The Casting Client discovers CastingPlayers
using Matter Commissioner
discovery over DNS-SD by listening for CastingPlayer
events as they are
discovered, updated, or lost from the network.
On Linux, define a DiscoveryDelegateImpl
that implements the
matter::casting::core::DiscoveryDelegate
.
class DiscoveryDelegateImpl : public DiscoveryDelegate {
private:
int commissionersCount = 0;
public:
void HandleOnAdded(matter::casting::memory::Strong<CastingPlayer> player) override {
if (commissionersCount == 0) {
ChipLogProgress(AppServer, "Select discovered CastingPlayer to request commissioning");
ChipLogProgress(AppServer, "Example: cast request 0");
}
++commissionersCount;
ChipLogProgress(AppServer, "Discovered CastingPlayer #%d", commissionersCount);
player->LogDetail();
}
void HandleOnUpdated(matter::casting::memory::Strong<CastingPlayer> player) override {
ChipLogProgress(AppServer, "Updated CastingPlayer with ID: %s", player->GetId());
}
};
On Android, implement the 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());
}
});
}
};
On iOS, implement a func addDiscoveredCastingPlayers
,
func removeDiscoveredCastingPlayers
and func updateDiscoveredCastingPlayers
which listen 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
}
}
}
Finally, register these listeners and start discovery.
On Linux, register an instance of the DiscoveryDelegateImpl
with
matter::casting::core::CastingPlayerDiscovery
by calling SetDelegate
on its
singleton instance. Then, call StartDiscovery
by optionally specifying the
kTargetPlayerDeviceType
to filter results by.
const uint64_t kTargetPlayerDeviceType = 35; // 35 represents device type of Matter Video Player
...
...
DiscoveryDelegateImpl delegate;
CastingPlayerDiscovery::GetInstance()->SetDelegate(&delegate);
VerifyOrReturnValue(err == CHIP_NO_ERROR, 0,
ChipLogError(AppServer, "CastingPlayerDiscovery::SetDelegate failed %" CHIP_ERROR_FORMAT, err.Format()));
err = CastingPlayerDiscovery::GetInstance()->StartDiscovery(kTargetPlayerDeviceType);
VerifyOrReturnValue(err == CHIP_NO_ERROR, 0,
ChipLogError(AppServer, "CastingPlayerDiscovery::StartDiscovery failed %" CHIP_ERROR_FORMAT, err.Format()));
chip::DeviceLayer::PlatformMgr().RunEventLoop();
...
On Android, add the implemented castingPlayerChangeListener
as a listener to
the singleton instance of MatterCastingPlayerDiscovery
to listen to changes in
the discovered Casting 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;
}
On iOS, register the listeners by calling addObserver
on the
NotificationCenter
with the appropriate selector, and then call start on the
sharedInstance
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()
...
}
Note: You will need to connect with a Casting Player as described below to se the list of Endpoints that they support. Refer to the Connection section for details on how to discover available endpoints supported by a Casting Player.
Connect to a Casting Player#
{Complete Connection examples: Linux | Android | iOS}
Each CastingPlayer
object created during
Discovery contains information such as
deviceName
, vendorId
, productId
, etc. which can help the user pick the
right CastingPlayer
. A Casting Client can attempt to connect to the
selectedCastingPlayer
using Matter User Directed Commissioning (UDC), where
the Casting Client generates the passcode. Alternately, a Casting Client can
attempt to connect to a CastingPlayer
, using the CastingPlayer
/
Commissioner-Generated Passcode UDC feature, if the
supportsCommissionerGeneratedPasscode
flag on the selectedCastingPlayer
is
set to true
.
For a Casting Client to connect to a CastingPlayer
using the optional
CastingPlayer
/ Commissioner-Generated Passcode UDC feature, the Casting
Client may specify optional parameters in the VerifyOrEstablishConnection
function call and then respond to the CastingPlayer’s CommissionerDeclaration
message as follows:
In
VerifyOrEstablishConnection
the Casting Client should set the UDC IdentificationDeclarationCommissionerPasscode
field to true and provide aCommissionerDeclarationCallback
in theConnectionCallbacks
parameter to handle the CastingPlayer’s CommissionerDeclaration message during commissioning.Upon receiving the CastingPlayer’s CommissionerDeclaration message with PasscodeDialogDisplayed set to true, the Casting Client should prompt the user to input the Passcode displayed on the
CastingPlayer
UX. If the user declines to enter the Passcode, the Casting Client should callStopConnecting
to alert theCastingPlayer
that the commissioning attempt was canceled.The Casting Client should then update the passcode to be used for commissioning session to the user-entered Passcode. Refer to how to set up the
commissioningDataProvider
in Initialize the Casting Client section above.Finally, the Casting Client should call
ContinueConnecting
to send a second IdentificationDeclaration message to theCastingPlayer
withCommissionerPasscodeReady
inIdentificationDeclarationOptions
set to true.
The Matter TV Casting library locally caches information required to reconnect
to a CastingPlayer
, once the Casting client has been commissioned by it. After
that, the Casting client is able to skip the full UDC process by establishing
CASE with the CastingPlayer
directly. Once connected, the CastingPlayer
object will contain the list of available Endpoints on that CastingPlayer
.
Optionally, the following arguments may also be passed in. The optional
commissioningWindowTimeoutSec
indicates how long to keep the commissioning
window open, if commissioning is required. And DesiredEndpointFilter
specifies
the attributes, such as Vendor ID and Product ID of the Endpoint
, the Casting
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 is not available in the Casting client’s cache.
On Linux, the Casting Client can connect to a CastingPlayer
by successfully
calling VerifyOrEstablishConnection
on it. Alternately, the Casting Client can
connect to a CastingPlayer
using the CastingPlayer
/ Commissioner-Generated
Passcode UDC feature, by successfully calling VerifyOrEstablishConnection
,
updating the commissioning passcode and then calling ContinueConnecting
on the
CastingPlayer
.
// VendorId of the Endpoint on the CastingPlayer that the CastingApp desires to interact with after connection
const uint16_t kDesiredEndpointVendorId = 65521;
void ConnectionHandler(CHIP_ERROR err, matter::casting::core::CastingPlayer * castingPlayer)
{
if(err == CHIP_NO_ERROR)
{
ChipLogProgress(AppServer, "ConnectionHandler: Successfully connected to CastingPlayer(ID: %s)", castingPlayer->GetId());
...
}
}
// If using the alternate CastingPlayer / Commissioner-Generated Passcode UDC feature:
// Define a callback to handle CastingPlayer’s CommissionerDeclaration messages.
void CommissionerDeclarationCallback(const chip::Transport::PeerAddress & source,
chip::Protocols::UserDirectedCommissioning::CommissionerDeclaration cd)
{
ChipLogProgress(AppServer,
"simple-app-helper.cpp::CommissionerDeclarationCallback() called with CommissionerDeclaration message:");
if (cd.GetCommissionerPasscode())
{
// A Passcode is now displayed for the user by the CastingPlayer. Prompt the user to enter the Passcode.
...
// Update the commissioning session's passcode with the user-entered Passcode
LinuxDeviceOptions::GetInstance().payload.setUpPINCode = userEnteredPasscode;
LinuxCommissionableDataProvider gCommissionableDataProvider;
CHIP_ERROR err = CHIP_NO_ERROR;
err = InitCommissionableDataProvider(gCommissionableDataProvider, LinuxDeviceOptions::GetInstance());
err = matter::casting::core::CastingApp::GetInstance()->UpdateCommissionableDataProvider(&gCommissionableDataProvider);
// Call continueConnecting to complete commissioning.
err = targetCastingPlayer->ContinueConnecting();
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "CommandHandler() setcommissionerpasscode ContinueConnecting() err %" CHIP_ERROR_FORMAT,
err.Format());
// Since continueConnecting() failed, Attempt to cancel the connection attempt with
// the CastingPlayer/Commissioner by calling StopConnecting().
err = targetCastingPlayer->StopConnecting();
if (err != CHIP_NO_ERROR)
{
ChipLogError(AppServer, "CommandHandler() setcommissionerpasscode, ContinueConnecting() failed and then StopConnecting failed due to err %" CHIP_ERROR_FORMAT, err.Format());
}
}
}
}
// 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
matter::casting::core::IdentificationDeclarationOptions idOptions;
chip::Protocols::UserDirectedCommissioning::TargetAppInfo targetAppInfo;
targetAppInfo.vendorId = kDesiredEndpointVendorId;
CHIP_ERROR result = idOptions.addTargetAppInfo(targetAppInfo);
matter::casting::core::ConnectionCallbacks connectionCallbacks;
connectionCallbacks.mOnConnectionComplete = ConnectionHandler;
// 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 ConnectionCallbacks.
idOptions.mCommissionerPasscode = true;
connectionCallbacks.mCommissionerDeclarationCallback = CommissionerDeclarationCallback;
// targetCastingPlayer is a discovered CastingPlayer
targetCastingPlayer->VerifyOrEstablishConnection(connectionCallbacks,
matter::casting::core::kCommissioningWindowTimeoutSec,
idOptions);
...
On Android, the Casting Client may call verifyOrEstablishConnection
on the
CastingPlayer
object it wants to connect to. Alternately, the Casting Client
can connect to a CastingPlayer
, using the CastingPlayer
/
Commissioner-Generated Passcode UDC feature by successfully calling
verifyOrEstablishConnection
, updating the commissioning passcode and then
calling continueConnecting
on the CastingPlayer
.
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");
});
}
On iOS, the Casting Client may call verifyOrEstablishConnection
on the
MCCastingPlayer
object it wants to connect to and handle any NSErrors
that
may happen in the process. Alternately, the Casting Client can connect to a
CastingPlayer
using the CastingPlayer
/ Commissioner-Generated Passcode UDC
feature, by successfully calling verifyOrEstablishConnection
, updating the
commissioning passcode and then calling continueConnecting
on the
CastingPlayer
.
// 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)")
}
}
Select an Endpoint on the Casting Player#
{Complete Endpoint selection examples: Linux | Android | iOS}
On a successful connection with a CastingPlayer
, a Casting Client may select
one of the Endpoints to interact with based on its attributes (e.g. Vendor ID,
Product ID, list of supported Clusters, etc).
On Linux, for example, it may select an Endpoint with a particular VendorID.
// VendorId of the Endpoint on the CastingPlayer that the CastingApp desires to interact with after connection
const uint16_t kDesiredEndpointVendorId = 65521;
std::vector<matter::casting::memory::Strong<matter::casting::core::Endpoint>> endpoints = castingPlayer->GetEndpoints();
// Find the desired Endpoint and auto-trigger some Matter Casting demo interactions
auto it = std::find_if(endpoints.begin(), endpoints.end(),
[](const matter::casting::memory::Strong<matter::casting::core::Endpoint> & endpoint) {
return endpoint->GetVendorId() == 65521;
});
if (it != endpoints.end())
{
// The desired endpoint is endpoints[index]
unsigned index = (unsigned int) std::distance(endpoints.begin(), it);
...
}
On Android, it can select an Endpoint
as shown below.
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;
}
On iOS, it can select an MCEndpoint
similarly and 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
static func selectEndpoint(from castingPlayer: MCCastingPlayer, sampleEndpointVid: Int) -> MCEndpoint? {
Log.info("MCEndpointSelector.selectEndpoint()")
if let endpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid }).first {
Log.info("MCEndpointSelector.selectEndpoint() Found endpoint matching the sampleEndpointVid: \(sampleEndpointVid)")
return endpoint
}
...
Log.error("No endpoint matching the example VID or identifier 1 found")
return nil
}
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/response types available for use with the Matter TV Casting library.
For Linux, refer to the following files:
For a list of supported clusters, commands and attributes: tv-casting-common/clusters/Clusters.h
For the IDs and request / response types to use with these APIs: /zzz_generated/app-common/app-common/zap-generated/cluster-objects.h
For Android, refer to the following files:
For a list of supported clusters, commands and attributes: /src/controller/java/generated/java/chip/devicecontroller/ChipClusters.java
On iOS, refer to the following files:
For a list of supported clusters, commands and attribute: darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCClusterObjects.h
For the IDs and request / response types to use with the commands: darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCCommandObjects.h and darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCCommandPayloads.h
For attribute read operations and subscriptions: darwin/MatterTvCastingBridge/MatterTvCastingBridge/zap-generated/MCAttributeObjects.h
Issuing Commands#
{Complete Command invocation examples: Linux | Android | iOS}
The Casting Client can get a reference to an Endpoint
on a CastingPlayer
,
check if it supports the required cluster/command, and send commands to it. It
can then handle any command response / error the CastingPlayer
sends back.
On Linux, for example, given an Endpoint
, it can send a LaunchURL
command
(part of the Content Launcher cluster) by calling the Invoke
API on a
Command
of type
matter::casting::core::Command<chip::app::Clusters::ContentLauncher::Commands::LaunchURL::Type>
void InvokeContentLauncherLaunchURL(matter::casting::memory::Strong<matter::casting::core::Endpoint> endpoint)
{
// get contentLauncherCluster from the endpoint
matter::casting::memory::Strong<matter::casting::clusters::content_launcher::ContentLauncherCluster> contentLauncherCluster =
endpoint->GetCluster<matter::casting::clusters::content_launcher::ContentLauncherCluster>();
VerifyOrReturn(contentLauncherCluster != nullptr);
// get the launchURLCommand from the contentLauncherCluster
matter::casting::core::Command<chip::app::Clusters::ContentLauncher::Commands::LaunchURL::Type> * launchURLCommand =
static_cast<matter::casting::core::Command<chip::app::Clusters::ContentLauncher::Commands::LaunchURL::Type> *>(
contentLauncherCluster->GetCommand(chip::app::Clusters::ContentLauncher::Commands::LaunchURL::Id));
VerifyOrReturn(launchURLCommand != nullptr, ChipLogError(AppServer, "LaunchURL command not found on ContentLauncherCluster"));
// create the LaunchURL request
chip::app::Clusters::ContentLauncher::Commands::LaunchURL::Type request;
request.contentURL = chip::CharSpan::fromCharString(kContentURL);
request.displayString = chip::Optional<chip::CharSpan>(chip::CharSpan::fromCharString(kContentDisplayStr));
request.brandingInformation =
chip::MakeOptional(chip::app::Clusters::ContentLauncher::Structs::BrandingInformationStruct::Type());
// call Invoke on launchURLCommand while passing in success/failure callbacks
launchURLCommand->Invoke(
request, nullptr,
[](void * context, const chip::app::Clusters::ContentLauncher::Commands::LaunchURL::Type::ResponseType & response) {
ChipLogProgress(AppServer, "LaunchURL Success with response.data: %.*s", static_cast<int>(response.data.Value().size()),
response.data.Value().data());
},
[](void * context, CHIP_ERROR error) {
ChipLogError(AppServer, "LaunchURL Failure with err %" CHIP_ERROR_FORMAT, error.Format());
},
chip::MakeOptional(kTimedInvokeCommandTimeoutMs)); // time out after kTimedInvokeCommandTimeoutMs
}
On Android, given an Endpoint
, it can send a LaunchURL
command (part of the
Content Launcher cluster) by calling the launchURL
API 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());
On iOS, given an MCEndpoint
endpoint, it can send a LaunchURL
command (part
of the Content Launcher cluster) by calling the invoke
API on a
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
Read Operations#
{Complete Attribute Read examples: Linux | Android | iOS}
The CastingClient
may read an Attribute from the Endpoint
on the
CastingPlayer
. It should ensure that the desired cluster / attribute are
available for reading on the endpoint before trying to read it.
On Linux, for example, given an endpoint
, it can read the VendorID
(part of
the Application Basic cluster) by calling the Read
API on an Attribute
of
type
matter::casting::core::Attribute<chip::app::Clusters::ApplicationBasic::Attributes::VendorID::TypeInfo>
void ReadApplicationBasicVendorID(matter::casting::memory::Strong<matter::casting::core::Endpoint> endpoint)
{
// get applicationBasicCluster from the endpoint
matter::casting::memory::Strong<matter::casting::clusters::application_basic::ApplicationBasicCluster> applicationBasicCluster =
endpoint->GetCluster<matter::casting::clusters::application_basic::ApplicationBasicCluster>();
VerifyOrReturn(applicationBasicCluster != nullptr);
// get the vendorIDAttribute from the applicationBasicCluster
matter::casting::core::Attribute<chip::app::Clusters::ApplicationBasic::Attributes::VendorID::TypeInfo> * vendorIDAttribute =
static_cast<matter::casting::core::Attribute<chip::app::Clusters::ApplicationBasic::Attributes::VendorID::TypeInfo> *>(
applicationBasicCluster->GetAttribute(chip::app::Clusters::ApplicationBasic::Attributes::VendorID::Id));
VerifyOrReturn(vendorIDAttribute != nullptr,
ChipLogError(AppServer, "VendorID attribute not found on ApplicationBasicCluster"));
// call Read on vendorIDAttribute while passing in success/failure callbacks
vendorIDAttribute->Read(
nullptr,
[](void * context,
chip::Optional<chip::app::Clusters::ApplicationBasic::Attributes::VendorID::TypeInfo::DecodableArgType> before,
chip::app::Clusters::ApplicationBasic::Attributes::VendorID::TypeInfo::DecodableArgType after) {
if (before.HasValue())
{
ChipLogProgress(AppServer, "Read VendorID value: %d [Before reading value: %d]", after, before.Value());
}
else
{
ChipLogProgress(AppServer, "Read VendorID value: %d", after);
}
},
[](void * context, CHIP_ERROR error) {
ChipLogError(AppServer, "VendorID Read failure with err %" CHIP_ERROR_FORMAT, error.Format());
});
}
On Android, given an Endpoint
, the VendorID
can be read, by calling
readVendorIDAttribute
on the ChipClusters.ApplicationBasicCluster
object.
// get ChipClusters.ApplicationBasic from the endpoint
ChipClusters.ApplicationBasicCluster cluster = endpoint.getCluster(ChipClusters.ApplicationBasicCluster.class);
if (cluster == null) {
Log.e(TAG, "Could not get ApplicationBasicCluster for endpoint with ID: " + endpoint.getId());
return;
}
// call readVendorIDAttribute on the cluster object while passing in a
// ChipClusters.IntegerAttributeCallback
cluster.readVendorIDAttribute(new ChipClusters.IntegerAttributeCallback() {
@Override
public void onSuccess(int value) {
Log.d(TAG, "ReadVendorID success. Value: " + value);
new Handler(Looper.getMainLooper())
.post(
() -> {
TextView vendorIdResult = getView().findViewById(R.id.vendorIdResult);
vendorIdResult.setText(
"Read VendorID result\nValue: " + value );
});
}
@Override
public void onError(Exception error) {
Log.e(TAG, "ReadVendorID failure " + error);
new Handler(Looper.getMainLooper())
.post(
() -> {
TextView vendorIdResult = getView().findViewById(R.id.vendorIdResult);
vendorIdResult.setText("Read VendorID result\nError: " + error);
});
}
});
On iOS, given a MCEndpoint
, the VendorID
can be read similarly, by calling
the read
API on the 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))"
}
}
}
Subscriptions#
{Complete Attribute subscription examples: Linux | Android | |iOS}
A Casting Client may subscribe to an attribute on an Endpoint
of the
CastingPlayer
to get data reports when the attributes change.
On Linux, for example, given an endpoint
, it can subscribe to the
CurrentState
(part of the Media Playback Basic cluster) by calling the
Subscribe
API on an Attribute
of type
matter::casting::core::Attribute<chip::app::Clusters::MediaPlayback::Attributes::CurrentState::TypeInfo>
void SubscribeToMediaPlaybackCurrentState(matter::casting::memory::Strong<matter::casting::core::Endpoint> endpoint)
{
// get mediaPlaybackCluster from the endpoint
matter::casting::memory::Strong<matter::casting::clusters::media_playback::MediaPlaybackCluster> mediaPlaybackCluster =
endpoint->GetCluster<matter::casting::clusters::media_playback::MediaPlaybackCluster>();
VerifyOrReturn(mediaPlaybackCluster != nullptr);
// get the currentStateAttribute from the mediaPlaybackCluster
matter::casting::core::Attribute<chip::app::Clusters::MediaPlayback::Attributes::CurrentState::TypeInfo> *
currentStateAttribute =
static_cast<matter::casting::core::Attribute<chip::app::Clusters::MediaPlayback::Attributes::CurrentState::TypeInfo> *>(
mediaPlaybackCluster->GetAttribute(chip::app::Clusters::MediaPlayback::Attributes::CurrentState::Id));
VerifyOrReturn(currentStateAttribute != nullptr,
ChipLogError(AppServer, "CurrentState attribute not found on MediaPlaybackCluster"));
// call Subscribe on currentStateAttribute while passing in success/failure callbacks
currentStateAttribute->Subscribe(
nullptr,
[](void * context,
chip::Optional<chip::app::Clusters::MediaPlayback::Attributes::CurrentState::TypeInfo::DecodableArgType> before,
chip::app::Clusters::MediaPlayback::Attributes::CurrentState::TypeInfo::DecodableArgType after) {
if (before.HasValue())
{
ChipLogProgress(AppServer, "Read CurrentState value: %d [Before reading value: %d]", static_cast<int>(after),
static_cast<int>(before.Value()));
}
else
{
ChipLogProgress(AppServer, "Read CurrentState value: %d", static_cast<int>(after));
}
},
[](void * context, CHIP_ERROR error) {
ChipLogError(AppServer, "CurrentState Read failure with err %" CHIP_ERROR_FORMAT, error.Format());
},
kMinIntervalFloorSeconds, kMaxIntervalCeilingSeconds);
}
On Android, given an Endpoint
, CurrentState
can be subscribe to 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);
On iOS, given a MCEndpoint
, CurrentState
can be subscribed to by calling the
subscribe
API on the it can subscribe to the CurrentState
(part of the Media
Playback Basic cluster) by calling the Subscribe
API on the
MCMediaPlaybackClusterCurrentStateAttribute
// validate that the selected endpoint supports the MediaPlayback cluster
if(!endpoint.hasCluster(MCEndpointClusterTypeMediaPlayback))
{
self.Log.error("No MediaPlayback cluster supporting endpoint found")
DispatchQueue.main.async
{
self.status = "No MediaPlayback cluster supporting endpoint found"
}
return
}
// get MediaPlayback cluster from the endpoint
let mediaPlaybackCluster: MCMediaPlaybackCluster = endpoint.cluster(for: MCEndpointClusterTypeMediaPlayback) as! MCMediaPlaybackCluster
// get the currentStateAttribute from the mediaPlaybackCluster
let currentStateAttribute: MCMediaPlaybackClusterCurrentStateAttribute? = mediaPlaybackCluster.currentStateAttribute()
if(currentStateAttribute == nil)
{
self.Log.error("CurrentState attribute not supported on cluster")
DispatchQueue.main.async
{
self.status = "CurrentState attribute not supported on cluster"
}
return
}
// call read on currentStateAttribute and pass in a completion block
currentStateAttribute!.subscribe(nil, completion: { context, before, after, err in
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let currentTime = dateFormatter.string(from: Date())
DispatchQueue.main.async
{
if(err != nil)
{
self.Log.error("Error when reading CurrentState value \(String(describing: err)) at \(currentTime)")
self.status = "Error when reading CurrentState value \(String(describing: err)) at \(currentTime)"
return
}
if(before != nil)
{
self.Log.info("Read CurrentState value: \(String(describing: after)) Before: \(String(describing: before)) at \(currentTime)")
self.status = "Read CurrentState value: \(String(describing: after)) Before: \(String(describing: before)) at \(currentTime)"
}
else
{
self.Log.info("Read CurrentState value: \(String(describing: after)) at \(currentTime)")
self.status = "Read CurrentState value: \(String(describing: after)) at \(currentTime)"
}
}
}, minInterval: 0, maxInterval: 1)
The Casting client can Shutdown all running Subscriptions by calling the
ShutdownAllSubscriptions
API on the CastingApp
on Linux/Android and
MCCastingApp
on iOS. See API and its documentation for
Linux,
Android
and iOS.