blob: d7f41ac5b376c17770705e9ace2a190a5758e3ec [file] [log] [blame] [view]
# 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](https://github.com/project-chip/connectedhomeip/tree/master/examples/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](https://github.com/project-chip/connectedhomeip/tree/master/examples/tv-app/android/App/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](https://github.com/project-chip/connectedhomeip/tree/master/examples/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](https://github.com/project-chip/connectedhomeip/tree/master/examples/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](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/app_clusters/media/VideoPlayerArchitecture.adoc),
a `CastingPlayer` would map to
[Casting "Video" Player](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/app_clusters/media/VideoPlayerArchitecture.adoc#1-introduction).
The `CastingPlayer` is expected to be hosting one or more `Endpoints` (similar
to
[Content Apps](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/app_clusters/media/VideoPlayerArchitecture.adoc#1-introduction)
in the Matter Video Player architecture) that support one or more Matter Media
`Clusters`.
The steps to start a casting session are:
1. [Initialize](#initialize-the-casting-client) the `CastingClient` using the
Matter SDK.
1. [Discover](#discover-casting-players) `CastingPlayer` devices using Matter
Commissioner discovery.
1. [Connect](#connect-to-a-casting-player) to the `CastingPlayer` to discover
available endpoints. By connecting, the `CastingClient` will send a User
Directed Commissioning (UDC) request to the `CastingPlayer` device in order
to make a Matter commissioning request. The `CastingPlayer` will then obtain
the appropriate user consent to allow a connection from this `CastingClient`
and obtain the setup code needed to commission the `CastingClient`. The setup
code will typically come from a corresponding TV content app or be input by
the user.
1. [Select](#select-an-endpoint-on-the-casting-player) an available `Endpoint`
hosted by the `CastingPlayer`.
Next, you're ready to:
1. [Issue commands](#issuing-commands) to the `Endpoint`.
1. [Read](#read-operations) endpoint attributes like playback state.
1. [Subscribe](#subscriptions) to playback events.
## 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](linux/README.md),
Android and [iOS](darwin/TvCasting/README.md) to understand how to build and
consume each platform's specific libraries.
### Initialize the Casting Client
_{Complete Initialization examples: [Linux](linux/simple-app.cpp) |
[Android](android/App/app/src/main/java/com/matter/casting/InitializationExample.java)
| [iOS](darwin/TvCasting/TvCasting/MTRInitializationExample.swift)}_
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:
1. **Rotating Device Identifier** - Refer to the Matter specification for
details on how to generate the
[Rotating Device Identifier](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/rendezvous/DeviceDiscovery.adoc#245-rotating-device-identifier)).
Then, instantiate a `DataProvider` object as described below.
On Linux, define a `RotatingDeviceIdUniqueIdProvider` to provide the Casting
Client's `RotatingDeviceIdUniqueId`, by implementing a
`matter:casting::support::MutableByteSpanDataProvider`:
```c
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's `RotatingDeviceIdUniqueId`, by implementing a
`com.matter.casting.support.DataSource`:
```java
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,
`MTRAppParametersDataSource`, that implements the `MTRDataSource`:
```objectivec
class MTRAppParametersDataSource : NSObject, MTRDataSource
{
func castingAppDidReceiveRequestForRotatingDeviceIdUniqueId(_ sender: Any) -> Data {
// dummy value, with at least 16 bytes (ConfigurationManager::kMinRotatingDeviceIDUniqueIDLength), for demonstration only
return "0123456789ABCDEF".data(using: .utf8)!
}
...
}
```
2. **Commissioning Data** - This object contains the passcode, discriminator,
etc which identify the app and are provided to the `CastingPlayer` during
the commissioning process. Refer to the Matter specification's
[Onboarding Payload](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/qr_code/OnboardingPayload.adoc#ref_OnboardingPayload)
section for details on commissioning data.
On Linux, define a function `InitCommissionableDataProvider` to initialize
initialize a `LinuxCommissionableDataProvider` that can provide the required
values to the `CastingApp`.
```c
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 Android, define a `commissioningDataProvider` that can provide the
required values to the `CastingApp`.
```java
private static final DataProvider<CommissionableData> commissionableDataProvider =
new DataProvider<CommissionableData>() {
@Override
public CommissionableData get() {
// dummy values for demonstration only
return new CommissionableData(20202021, 3874);
}
};
```
On iOS, add a `func commissioningDataProvider` to the
`MTRAppParametersDataSource` class defined above, that can provide the
required values to the `MTRCastingApp`.
```objectivec
func castingAppDidReceiveRequestForCommissionableData(_ sender: Any) -> MTRCommissionableData {
// dummy values for demonstration only
return MTRCommissionableData(
passcode: 20202021,
discriminator: 3874,
spake2pIterationCount: 1000,
spake2pVerifier: nil,
spake2pSalt: nil)
}
```
3. **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
[Device Attestation process](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/device_attestation/Device_Attestation_Specification.adoc)
during commissioning.
On Linux, implement a define a `dacProvider` to provide the Casting Client's
Device Attestation Credentials, by implementing a
`chip::Credentials::DeviceAttestationCredentialsProvider`. For this example,
we will use the `chip::Credentials::Examples::ExampleDACProvider`
On Android, define a `dacProvider` to provide the Casting Client's Device
Attestation Credentials, by implementing a
`com.matter.casting.support.DACProvider`:
```java
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` and
`didReceiveRequestToSignCertificateRequest` to the
`MTRAppParametersDataSource` class defined above, that can return
`MTRDeviceAttestationCredentials` and sign messages for the Casting Client,
respectively.
```objectivec
// 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) -> MTRDeviceAttestationCredentials {
return MTRDeviceAttestationCredentials(
certificationDeclaration: kCertificationDeclaration,
firmwareInformation: Data(),
deviceAttestationCert: kDevelopmentDAC_Cert_FFF1_8001,
productAttestationIntermediateCert: KPAI_FFF1_8000_Cert_Array)
}
func castingApp(_ sender: Any, didReceiveRequestToSignCertificateRequest csrData: Data) -> Data {
var privateKey = Data()
privateKey.append(kDevelopmentDAC_PublicKey_FFF1_8001);
privateKey.append(kDevelopmentDAC_PrivateKey_FFF1_8001);
let privateKeyRef: SecKey = SecKeyCreateWithData(privateKey as NSData,
[
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrKeySizeInBits: 256
] as NSDictionary, nil)!
let _:Unmanaged<SecKey> = Unmanaged<SecKey>.passRetained(privateKeyRef);
// use SecKey above to sign csrData and return resulting value
}
```
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.
```c
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.
```java
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 `MTRCastingApp.initialize` with an object of the
`MTRAppParametersDataSource`.
```objectivec
func initialize() -> MatterError {
if let castingApp = MTRCastingApp.getSharedInstance() {
return castingApp.initialize(with: MTRAppParametersDataSource())
} else {
return MATTER_ERROR_INCORRECT_STATE
}
}
```
### Discover Casting Players
_{Complete Discovery examples: [Linux](linux/simple-app-helper.cpp)}_
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`.
```c
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());
}
};
```
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.
```c
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();
...
```
### Connect to a Casting Player
_{Complete Connection examples: [Linux](linux/simple-app-helper.cpp)}_
Each `CastingPlayer` object created during
[Discovery](#discover-casting-players) 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)](https://github.com/CHIP-Specifications/connectedhomeip-spec/blob/master/src/rendezvous/UserDirectedCommissioning.adoc).
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`.
On Linux, the Casting Client can connect to a `CastingPlayer` by successfully
calling `VerifyOrEstablishConnection` on it.
```c
void ConnectionHandler(CHIP_ERROR err, matter::casting::core::CastingPlayer * castingPlayer)
{
ChipLogProgress(AppServer, "ConnectionHandler called with %" CHIP_ERROR_FORMAT, err.Format());
}
...
// targetCastingPlayer is a discovered CastingPlayer
targetCastingPlayer->VerifyOrEstablishConnection(ConnectionHandler);
...
```
### Select an Endpoint on the Casting Player
## Interacting with a Casting Endpoint
### Issuing Commands
### Read Operations
### Subscriptions