Matter投屏集成
要在您的应用中启用Matter投屏,请更新以下内容:
- 客户端: 这是适用于Android或iOS的面向用户的手机应用。通过将您的手机应用设为Matter客户端,用户可以发现诸如Fire TV之类的投屏目标。用户还可以投射内容,以及控制投屏会话。
- 内容应用: 这是您在Fire TV上的面向用户的应用。通过将您的电视应用设置为Matter内容应用,客户端可以对其进行控制。例如,客户端可以启动特定内容的播放。
客户端和播放器之间的通信遵循Matter协议。因此,您可以针对Fire TV设备开发客户端,也可以使用Matter开源存储库中包含的示例播放器之一。该存储库包括一个可以在Android电视上运行的Android播放器。同样,播放器和内容应用之间的通信是使用特定于Matter的Android意图完成的,因此可以使用Fire TV设备或使用通用Android电视上的Android播放器完成开发。以下内容将说明如何将Matter投屏SDK集成到您的Android或iOS客户端应用中以及如何与之交互。
步骤1: 将Matter投屏SDK集成到您的客户端应用中
以下是初始化Matter SDK并将其集成到您的移动Android或iOS应用中所需的步骤。
构建和设置
要使用本文档中描述的任何API,您的客户端必须在您的Android或iOS项目中包括Matter投屏库。
缓存
客户端维护设备上的缓存,其中包含有关先前连接的播放器的信息。该缓存的信息可以让客户端跳过较长的配网过程,直接重新建立CASE会话,从而更快地与这些播放器连接,并且使用的资源更少。可以通过在客户端上调用ClearCache
方法来清除此缓存。例如,当用户注销客户端时,清除缓存可能很有用。
在Android上集成
从连接标准联盟 (CSA) Github存储库下载Android SDK并将其添加到您的项目中。将其添加到您的项目后,使用以下步骤初始化该库(完整列出)。
1. 定义轮换设备标识符
这是每台设备的唯一标识符,由随机生成的128位或更长的8位组字符串组成。查看Matter规范,了解有关如何生成轮换设备标识符的详细信息。按如下所述实例化DataProvider对象以提供此标识符。
private static final DataProvider < byte[] > rotatingDeviceIdUniqueIdProvider =
new DataProvider < byte[] > () {
private static final String ROTATING_DEVICE_ID_UNIQUE_ID =
"EXAMPLE_IDENTIFIER"; // 仅用于演示的虚拟值
@Override
public byte[] get() {
return ROTATING_DEVICE_ID_UNIQUE_ID.getBytes();
}
};
2. 初始化配网数据
commissioningDataProvider对象包含密码和其他可标识客户端的工作内容,并在配网过程中被提供给播放器。有关配网数据的详细信息,请参阅Matter规范的Onboarding Payload(引入有效负载)部分。
DUMMY_SETUP_PASSCODE
和DUMMY_DISCRIMINATOR
更改为您的随机值。
public static class CommissionableDataProvider implements DataProvider < CommissionableData > {
CommissionableData commissionableData =
// 虚拟值仅用于配网演示。在示例电视应用中,这些值是硬编码的:
// 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;
}
// 如果使用备用CastingPlayer/配网者生成的密码UDC功能:
public void updateCommissionableDataSetupPasscode(long setupPasscode, int discriminator) {
commissionableData.setSetupPasscode(setupPasscode);
commissionableData.setDiscriminator(discriminator);
}
};
3. 添加设备认证证书
设备认证证书 (DAC) 对象在配网期间提供对消息进行签名所需的证书,该签名操作是设备认证流程的一部分。Matter规范提供了有关该证书工作原理的背景信息。出于开发目的,您可以使用示例代码中提供的DAC。
通过实现com.matter.casting.support.DACProvider定义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="; // 仅用于演示的虚拟值
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. 初始化客户端
创建对象后,按如下所示初始化客户端。在Matter投屏会话启动之前,只能将客户端初始化一次。
public static MatterError initAndStart(Context applicationContext) {
// 创建AppParameters对象以将全局投屏参数传递给SDK
final AppParameters appParameters =
new AppParameters(
applicationContext,
new DataProvider < ConfigurationManager > () {
@Override
public ConfigurationManager get() {
return new PreferencesConfigurationManager(
applicationContext, "chip.platform.ConfigurationManager");
}
},
rotatingDeviceIdUniqueIdProvider,
commissionableDataProvider,
dacProvider);
// 使用appParameters初始化SDK并检查它是否成功返回
MatterError err = CastingApp.getInstance().initialize(appParameters);
if (err.hasError()) {
Log.e(TAG, "无法初始化Matter CastingApp");
return err;
}
// 启动CastingApp
err = CastingApp.getInstance().start();
if (err.hasError()) {
Log.e(TAG, "无法启动Matter投屏客户端");
return err;
}
return err;
}
在iOS上集成
A. 构建Matter框架
开源Github存储库提供了一个框架,用于处理您的iOS客户端与视频投屏播放器和内容应用之间的低级通信。使用以下步骤构建框架。
- 使用git clone https://github.com/project-chip/connectedhomeip/tree/master将Github存储库克隆到您的开发计算机
- 在终端中,将文件夹更改为存储库的位置并运行源scripts/bootstrap.sh。这将建立所需的库和关联。
- 打开示例iOS应用以在本地构建框架。
- 将该框架包含在您的iOS应用中。
B. 将Matter投屏框架集成到您的应用中
1. 定义轮换设备标识符
这是每台设备的唯一标识符,由随机生成的128位或更长的8位组字符串组成。查看Matter规范,了解有关如何生成轮换设备标识符的详细信息。按如下所述实例化DataProvider
对象以提供此标识符。
class MCAppParametersDataSource: NSObject, MCDataSource {
func castingAppDidReceiveRequestForRotatingDeviceIdUniqueId(_ sender: Any) - > Data {
// 虚拟值,有至少16个字节(ConfigurationManager::kMinRotatingDeviceIDUniqueIDLength),仅用于演示
return "0123456789ABCDEF".data(using: .utf8) !
}
...
}
2. 初始化配网数据
该对象包含密码、鉴别器等,用于识别应用并在配网过程中提供CastingPlayer
对象。密码必须以27位无符号整数的形式包括在内。此项用作配网期间所有权的证明。鉴别器必须以12位无符号整数的形式包括在内,该整数必须与设备在配网期间公布的值相匹配。有关Matter规范的Onboarding Payload部分的更多详细信息,请参阅Matter Core规范中的第5章Commissioning(配网)。
将函数commissioningDataProvider
添加到上面定义的MCAppParametersDataSource
类中,该类为MCCastingApp提供所需的值。如果使用CastingPlayer/配网者生成的密码UDC功能,则投屏客户端必须在VerifyOrEstablishConnection()
API调用期间更新commissioningDataProvider
(稍后说明)。在下面的示例中,更新函数使用用户在投屏客户端用户体验上输入的CastingPlayer
生成的密码来更新CommissionableData
。
// 仅用于演示的虚拟值。
private
var commissionableData: MCCommissionableData = MCCommissionableData(
passcode: 20202021,
discriminator: 3874,
spake2pIterationCount: 1000,
spake2pVerifier: nil,
spake2pSalt: nil
)
func castingAppDidReceiveRequestForCommissionableData(_ sender: Any) - > MCCommissionableData {
return commissionableData
}
// 如果使用备用CastingPlayer/配网者生成的密码UDC功能:
func update(_ newCommissionableData: MCCommissionableData) {
self.commissionableData = newCommissionableData
}
3. 添加设备认证证书
该对象包含DeviceAttestationCertificate
、ProductAttestationIntermediateCertificate
等,配网期间它可在由Matter电视投屏库调用时,实现一种作为Matter设备认证流程的一部分对消息进行签名的方法。
向上面定义的MCAppParametersDataSource
类中添加函数castingAppDidReceiveRequestForDeviceAttestationCredentials
和didReceiveRequestToSignCertificateRequest
,该类返回MCDeviceAttestationCredentials
并为投屏客户端将消息签名。
// 仅用于演示的虚拟DAC值
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")
// 获取私有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) !
// 签署csrData以获取asn1SignatureData
var error: Unmanaged < CFError > ?
let asn1SignatureData: CFData ? = SecKeyCreateSignature(privateSecKey, .ecdsaSignatureMessageX962SHA256, csrData as CFData, & error)
if (error != nil) {
Log.error("无法对消息签名。Error: \(String(describing: error))")
return MATTER_ERROR_INVALID_ARGUMENT
} else if (asn1SignatureData == nil) {
Log.error("无法对消息签名。asn1SignatureData为空")
return MATTER_ERROR_INVALID_ARGUMENT
}
// 将ASN.1 DER签名转换为SEC1原始格式
return MCCryptoUtils.ecdsaAsn1SignatureToRaw(withFeLengthBytes: 32,
asn1Signature: asn1SignatureData!,
outRawSignature: & outRawSignature.pointee)
}
4. 初始化SDK
创建上面的DataProvider
对象后,就可以按如下所述初始化投屏应用。
使用MCAppParametersDataSource
的对象调用MCCastingApp.initialize
。
func initialize() - > MatterError {
if let castingApp = MCCastingApp.getSharedInstance() {
return castingApp.initialize(with: MCAppParametersDataSource())
} else {
return MATTER_ERROR_INCORRECT_STATE
}
}
初始化后,当应用发送UIApplication.didBecomeActiveNotification
和UIApplication.willResignActiveNotification
时,调用在MCCastingApp
共享实例上开始和停止。
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初始化失败 \(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启动失败 \(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
} // 正文
} // 应用
步骤2:将Matter与您的Fire TV应用集成
将Matter投屏SDK集成到您的客户端应用后,您的客户端就能够在同一Matter网络上发现播放器和内容应用。在从Fire TV上的内容应用侦听、发送和接收Matter命令之前,必须将其与Fire TV Matter投屏服务集成。此步骤向您展示如何使用Android电视示例应用来启动已经启用Matter投屏的Android电视的内容应用。
1. 更新您的Android清单
启用Matter投屏需要扩展Android清单中提供的信息。播放器和Matter代理客户端使用Android清单中提供的信息来了解哪些客户端允许投射至内容应用,以及内容应用支持哪些Matter集群。
1. 产品和供应商信息
更新应用的标签,以包括您的产品ID和供应商ID。有关更多信息,请参阅应用认证。
对于开发,您可以使用以下示例中提供的产品ID 65521
、供应商ID 32769
和供应商名称。
vendor_id
和product_id
值替换为DAC提供的值。<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. 更新权限
要与播放器的Matter代理服务绑定,您的内容应用必须在其清单文件中提供以下权限。
<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"/>
权限字符串在common-api模块中定义为PERMISSION_MATTER_AGENT_BIND。AndroidManifest.xml可以用作参考点。
3. 支持的集群
接下来,要添加您的内容应用支持的集群,请将以下内容添加到Android清单中。
<meta-data android:name="com.matter.tv.application.api.clusters" android:resource="@raw/static_matter_clusters" />
现在创建JSON文件static_matter_clusters,其中包含内容应用支持的集群列表。
{
"clusters": [
// 账户登录集群
{
"identifier": 1294
},
{
"identifier": 1289,
"features": [
"NV",
"LK",
"NK"
]
},
{
"identifier": 1290,
"features": [
"CS"
]
},
{
"identifier": 1286,
"features": [
"AS"
]
}
]
}
2. 集成Matter AIDL文件
Matter投屏使用Android接口定义语言 (AIDL) 来定义编程接口,以便在播放器和内容应用之间进行通信。将以下3个AIDL文件从示例应用复制到应用的存储库: IMatterapplicationAgent.aidl、SetSupportedClustersRequest.aidl和SupportedCluster.aidl。这样将帮助您与以下必需的方法setSupportedClusters
和reportAttributeChange
集成。这些方法对于以下各项是必需的:
setSupportedClusters
- 此方法动态地将集群报告给Matter代理。
- 此方法不是增量的,并且每次调用时,都要报告整组集群。任何在之前添加的最新方法调用中省略的集群都将被删除。
- 上述行为不会影响应用资源中声明的静态集群,它们不会被删除。
- 动态集群可用于根据集群名称覆盖和隐藏静态集群。
reportAttributeChange
- 此方法报告内容应用对属性的更改。
- 它还接收所报告的属性更改的集群ID和属性ID。
其他文档可以在Matter tv app common-api(仅提供英⽂版)中找到。
3. 创建Matter代理客户端
创建Matter代理客户端,如示例所示。该代理用于连接到Matter服务并使用上述AIDL步骤中提供的方法。
以下是建立binder连接的示例。
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, "设备上没有可用于意图绑定的服务" + intent);
return false;
}
final ComponentName component =
new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
intent.setComponent(component);
}
try {
Log.d(TAG, "绑定到服务");
latch = new CountDownLatch(1);
return context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
} catch (final Throwable e) {
Log.e(TAG, "绑定到服务时发生异常", e);
}
return false;
}
4. 注册Matter命令接收器
内容应用将自身注册为接收者,以处理传入的Matter命令。为了接收从客户端发送的命令,内容应用注册一个接收器实例,该实例通过权限侦听MATTER_COMMAND意图。
<!-- 接收Matter指令的意图操作-->
<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>
为了发送数据和响应命令,如上所示加入接收器的权限com.matter.tv.app.api.permission.SEND_DATA
。收到意图后,即可在内容应用中读取Matter命令。
long commandId = intent.getLongExtra(MatterIntentConstants.EXTRA_COMMAND_ID, -1);
long clusterId = intent.getLongExtra(MatterIntentConstants.EXTRA_CLUSTER_ID, -1);
使用该类来映射命令和集群ID。
5. 执行命令
内容应用启动后,它将绑定到Matter代理服务以接收命令并通过reportClusters
调用注册其所有动态终端节点和支持的集群。
executorService.execute(() -> matterAgentClient.reportClusters(supportedClustersRequest));
如果内容应用需要报告属性变更,可使用Matter代理上的reportAttributeChange
来通知SDK。
当接收来自Matter SDK的命令时,将使用内容应用实现的BroadcastReceiver
接收ACTION_MATTER_COMMAND
类型的意图。该意图包括命令ID、集群ID和相应的有效负载数据。调用命令处理程序后,必须发送对这些命令的响应。在MatterCommandReceiver.java中可以找到一个示例接收器。意图中的所有内部字段都位于通过common-api
模块提供的MatterIntentConstants
下。
6. 按需安装您的Fire TV应用
以下内容假设客户已经安装了您的内容应用,并且他们正在向该应用投射。但是,情况可能并非如此。如果未安装内容应用,并且启动应用集群有效负载的ApplicationID
信息中提供了该应用的程序包名称,则Fire TV可以提示客户安装应用。
步骤3:与播放器交互
在此步骤中,您将学习如何发现、选择和连接播放器,发出命令,读取播放状态等属性以及订阅播放事件。
Android上的交互
1. 发现投屏播放器
客户端经由DNS-SD
使用Matter Commissioner Discovery,通过侦听播放器事件(例如探索、更新播放器,或播放器与网络断开)来发现播放器,例如Fire TV等。
首先实现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() 发现了CastingPlayer deviceId:" + castingPlayer.getDeviceId());
// 在屏幕上显示CastingPlayer信息
new Handler(Looper.getMainLooper()).post(() - > {
arrayAdapter.add(castingPlayer);
});
}
@Override
public void onChanged(CastingPlayer castingPlayer) {
Log.i(TAG, "onChanged() 发现了对于CastingPlayer的更改,其deviceId为:" + castingPlayer.getDeviceId());
// 更新屏幕上的CastingPlayer
new Handler(Looper.getMainLooper()).post(() - > {
final Optional < CastingPlayer > playerInList = castingPlayerList.stream().filter(node - > castingPlayer.equals(node)).findFirst();
if (playerInList.isPresent()) {
Log.d(TAG, "onChanged() 正在更新现有CastingPlayer条目" + playerInList.get().getDeviceId() + "于castingPlayerList列表中");
arrayAdapter.remove(playerInList.get());
}
arrayAdapter.add(castingPlayer);
});
}
@Override
public void onRemoved(CastingPlayer castingPlayer) {
Log.i(TAG, "onRemoved() 删除了CastingPlayer,其deviceId为:" + castingPlayer.getDeviceId());
// 从屏幕上删除CastingPlayer
new Handler(Looper.getMainLooper()).post(() - > {
final Optional < CastingPlayer > playerInList = castingPlayerList.stream().filter(node - > castingPlayer.equals(node)).findFirst();
if (playerInList.isPresent()) {
Log.d(TAG, "onRemoved() 正在删除现有CastingPlayer条目" + playerInList.get().getDeviceId() + "于castingPlayerList列表中");
arrayAdapter.remove(playerInList.get());
}
});
}
};
然后注册这些侦听器并开始探索。将castingPlayerChangeListener
作为侦听器添加至MatterCastingPlayerDiscovery
,以侦听已发现播放器中的更改并调用startDiscovery
。
MatterError err = MatterCastingPlayerDiscovery.getInstance().addCastingPlayerChangeListener(castingPlayerChangeListener);
if (err.hasError()) {
Log.e(TAG, "调用了startDiscovery() addCastingPlayerChangeListener(),添加时出错:" + err);
return false;
}
// 开始探索
Log.i(TAG, "startDiscovery() 正在调用CastingPlayerDiscovery.startDiscovery()");
err = MatterCastingPlayerDiscovery.getInstance().startDiscovery(DISCOVERY_TARGET_DEVICE_TYPE);
if (err.hasError()) {
Log.e(TAG, "Error in startDiscovery(): " + err);
return false;
}
连接播放器,查看它们支持的终端节点列表。有关如何发现投屏播放器支持的可用终端节点的详细信息,请参阅连接部分(仅提供英文版)。
2. 连接已发现的播放器
探索期间创建的每个播放器对象都包含deviceName
、vendorId
和productId
等信息,这些信息可以帮助用户选择正确的播放器。客户端可以尝试使用Matter用户直接配网 (UDC) 连接到selectedCastingPlayer
。客户端通过播放器对象进行了配网后,Matter电视投屏库会在本地缓存重新连接到播放器对象所需的信息。之后,投屏客户端可以通过直接与播放器建立CASE会话来跳过整个UDC流程。连接后,播放器对象包含该播放器上可用终端节点的列表,以下参数也可以作为选项传入。commissioningWindowTimeoutSec
表示如果需要配网,配网窗口应保持打开多长时间。DesiredEndpointFilter
指定客户端在连接后希望与之交互的终端节点的属性,例如供应商ID和产品ID。这样在客户端缓存中所需终端节点不可用时,会强制Matter TV投屏库完成完整的UDC流程来搜索所需的终端节点。
客户端可以在它想要连接的CastingPlayer
对象上调用verifyOrEstablishConnection
。
private static final short MIN_CONNECTION_TIMEOUT_SEC = 3 * 60;
private static final Integer DESIRED_TARGET_APP_VENDOR_ID = 65521;
// 指定配网后客户端想要与之交互的TargetApp。如果传入此值,
// 在设备上找不到所需的TargetApp时,VerifyOrEstablishConnection() 将强制执行UDC。
// CastingStore
IdentificationDeclarationOptions idOptions = new IdentificationDeclarationOptions();
TargetAppInfo targetAppInfo = new TargetAppInfo(DESIRED_TARGET_APP_VENDOR_ID);
idOptions.addTargetAppInfo(targetAppInfo);
// 如果使用备用CastingPlayer/配网设备生成的密码UDC功能,
// 设置IdentificationDeclaration CommissionerPasscode标记来指示CastingPlayer或
// 配网者使用其生成的密码来进行配网。
idOptions = new IdentificationDeclarationOptions(commissionerPasscode: true);
idOptions.addTargetAppInfo(targetAppInfo);
ConnectionCallbacks connectionCallbacks =
new ConnectionCallbacks(
new MatterCallback < Void > () {
@Override
public void handle(Void v) {
Log.i(
TAG,
"成功连接至CastingPlayer,其deviceId为:" +
targetCastingPlayer.getDeviceId());
getActivity()
.runOnUiThread(
() - > {
connectionFragmentStatusTextView.setText(
"成功连接到投屏播放器,其设备名称为:" +
targetCastingPlayer.getDeviceName() +
"\n\n");
connectionFragmentNextButton.setEnabled(true);
});
}
},
new MatterCallback < MatterError > () {
@Override
public void handle(MatterError err) {
Log.e(TAG, "CastingPlayer连接失败:" + err);
getActivity()
.runOnUiThread(
() - > {
connectionFragmentStatusTextView.setText(
"投屏播放器连接失败,原因是:" + err + "\n\n");
});
}
},
// 如果使用备用CastingPlayer/配网者生成的密码UDC功能,
// 定义回调来处理CastingPlayer的CommissionerDeclaration消息。
// 如果使用投屏客户端/受配网者生成的密码进行配网,则此值可为空。
new MatterCallback < CommissionerDeclaration > () {
@Override
public void handle(CommissionerDeclaration cd) {
getActivity()
.runOnUiThread(
() - > {
connectionFragmentStatusTextView.setText(
"从投屏播放器处收到了CommissionerDeclaration消息:\n\n");
if (cd.getCommissionerPasscode()) {
displayPasscodeInputDialog(getActivity());
...
// 使用用户输入的密码更新配网会话的密码
InitializationExample.commissionableDataProvider.updateCommissionableDataSetupPasscode(
passcodeLongValue, DEFAULT_DISCRIMINATOR_FOR_CGP_FLOW);
// 调用continueConnecting以完成配网过程。
MatterError err = targetCastingPlayer.continueConnecting();
if (err.hasError()) {
...
Log.e(
TAG,
"displayPasscodeInputDialog() continueConnecting() 失败,正在调用stopConnecting(),原因是:" +
err);
// 由于continueConnecting() 失败,请尝试通过调用
// stopConnecting() 来取消与CastingPlayer/配网者连接的尝试。
err = targetCastingPlayer.stopConnecting();
if (err.hasError()) {
Log.e(TAG, "displayPasscodeInputDialog() stopConnecting() 失败,原因是:" + err);
}
}
}
});
}
}
);
MatterError err = targetCastingPlayer.verifyOrEstablishConnection(
connectionCallbacks, MIN_CONNECTION_TIMEOUT_SEC, idOptions);
if (err.hasError()) {
getActivity()
.runOnUiThread(
() - > {
connectionFragmentStatusTextView.setText(
"投屏播放器连接失败,原因是:" + err + "\n\n");
});
}
3. 在播放器上选择一个终端节点
成功连接播放器对象后,投屏客户端可以根据其属性(例如供应商ID、产品ID或支持的集群列表)选择要与之交互的终端节点之一。在Matter投屏的背景下,终端节点是指您的内容应用。
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() 未在CastingPlayer中找到终端节点");
} else {
endpoint =
endpoints
.stream()
.filter(e - > SAMPLE_ENDPOINT_VID.equals(e.getVendorId()))
.findFirst()
.orElse(null);
}
}
return endpoint;
}
4. 与投屏终端节点交互
投屏客户端选择了一个终端节点后,它就能够向其发出命令、读取当前播放状态以及订阅播放事件。可以在示例代码中找到Android上支持的集群、命令和属性的列表。
发出命令
给定上一步中的终端节点,您可以通过在ChipClusters.ContentLauncherCluster
对象上调用launchURL
方法来发送LaunchURL
命令(内容启动器集群的一部分)。
// 从终端节点获取ChipClusters.ContentLauncherCluster
ChipClusters.ContentLauncherCluster cluster =
endpoint.getCluster(ChipClusters.ContentLauncherCluster.class);
if (cluster == null) {
Log.e(TAG, "无法为终端节点获取ContentLauncherCluster,该终端节点ID为:" + endpoint.getId());
return;
}
// 在传入ChipClusters.ContentLauncherCluster.LauncherResponseCallback和
// 请求参数时,在集群对象上调用launchURL。
cluster.launchURL(
new ChipClusters.ContentLauncherCluster.LauncherResponseCallback() {
@Override
public void onSuccess(Integer status, Optional < String > data) {
Log.d(TAG, "LaunchURL成功。状态:" + status + ",数据:" + 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失败" + 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());
读取操作和订阅
以下内容演示如何从内容应用读取数据并更新客户端用户界面以表示当前的播放位置。
在此示例中,客户通过在ChipClusters.MediaPlaybackCluster
对象上调用subscribeCurrentStateAttribute
进行订阅。
// 从终端节点获取ChipClusters.MediaPlaybackCluster
ChipClusters.MediaPlaybackCluster cluster =
endpoint.getCluster(ChipClusters.MediaPlaybackCluster.class);
if (cluster == null) {
Log.e(TAG,
“无法为终端节点获取ApplicationBasicCluster,该终端节点ID为:+
endpoint.getId());
return;
}
// 为最小间隔和最大间隔参数传入ChipClusters.IntegerAttributeCallback和 [0, 1] 时,
// 在集群对象上调用
// subscribeCurrentStateAttribute
cluster.subscribeCurrentStateAttribute(
new ChipClusters.IntegerAttributeCallback() {
@Override
public void onSuccess(int value) {
Log.d(TAG,
"在订阅上读取成功。值:" + value + " @ " +
new Date());
new Handler(Looper.getMainLooper()).post(() - > {
TextView currentStateResult =
getView().findViewById(R.id.currentStateResult);
currentStateResult.setText("当前状态结果\n值:" + value);
});
}
@Override
public void onError(Exception error) {
Log.e(TAG, "在订阅上读取失败:" + error);
new Handler(Looper.getMainLooper()).post(() - > {
TextView currentStateResult =
getView().findViewById(R.id.currentStateResult);
currentStateResult.setText("当前状态结果\n错误:" + error);
});
}
},
0, 1);
iOS上的交互
1. 发现投屏播放器
现在,您已将Matter库集成到客户端和内容应用中,您可以在同一Matter结构上发现视频投屏播放器和内容应用并与其通信。
实现函数addDiscoveredCastingPlayers、函数removeDiscoveredCastingPlayers
和函数updateDiscoveredCastingPlayers
,这些函数在添加、移除或更新了投屏播放器时侦听通知。
@objc
func didAddDiscoveredCastingPlayers(notification: Notification) {
Log.info("调用了didAddDiscoveredCastingPlayers()")
guard
let userInfo = notification.userInfo,
let castingPlayer = userInfo["castingPlayer"] as ? MCCastingPlayer
else {
self.Log.error("在没有MCCastingPlayer的情况下调用了didAddDiscoveredCastingPlayers")
return
}
self.Log.info("didAddDiscoveredCastingPlayers收到有关MCCastingPlayer的通知,后者ID为:\(castingPlayer.identifier())")
DispatchQueue.main.async {
self.displayedCastingPlayers.append(castingPlayer)
}
}
@objc
func didRemoveDiscoveredCastingPlayers(notification: Notification) {
Log.info("调用了didRemoveDiscoveredCastingPlayers()")
guard
let userInfo = notification.userInfo,
let castingPlayer = userInfo["castingPlayer"] as ? MCCastingPlayer
else {
self.Log.error("在没有MCCastingPlayer的情况下调用了didRemoveDiscoveredCastingPlayers")
return
}
self.Log.info("didRemoveDiscoveredCastingPlayers收到有关MCCastingPlayer的通知,后者ID为:\(castingPlayer.identifier())")
DispatchQueue.main.async {
self.displayedCastingPlayers.removeAll(where: {
$0 == castingPlayer
})
}
}
@objc
func didUpdateDiscoveredCastingPlayers(notification: Notification) {
Log.info("调用了didUpdateDiscoveredCastingPlayers()")
guard
let userInfo = notification.userInfo,
let castingPlayer = userInfo["castingPlayer"] as ? MCCastingPlayer
else {
self.Log.error("在没有MCCastingPlayer的情况下调用了didUpdateDiscoveredCastingPlayers")
return
}
self.Log.info("didUpdateDiscoveredCastingPlayers收到有关MCCastingPlayer的通知,后者ID为:\(castingPlayer.identifier())")
if let index = displayedCastingPlayers.firstIndex(where: {
castingPlayer.identifier() == $0.identifier()
}) {
DispatchQueue.main.async {
self.displayedCastingPlayers[index] = castingPlayer
}
}
}
注册这些侦听器,以使用相应的选择器调用NotificationCenter
中的addObserver
开始探索,然后在MCCastingPlayerDiscovery
的sharedInstance
对象上开始调用。
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. 连接投屏播放器
探索期间创建的每个CastingPlayer
对象都包含deviceName、vendorId和productId等信息,这些信息可以帮助用户选择正确的CastingPlayer
。投屏客户端可以尝试使用Matter用户直接配网 (UDC) 连接到selectedCastingPlayer
。投屏客户端通过播放器对象进行了配网后,Matter电视投屏库会在本地缓存重新连接到CastingPlayer
对象所需的信息。之后,投屏客户端可以通过直接与CastingPlayer
建立CASE会话来跳过整个UDC流程。连接后,CastingPlayer
对象将包含该CastingPlayer
对象上可用终端节点的列表。如果需要,commissioningWindowTimeoutSec
会显示配网窗口保持打开状态多长时间。DesiredEndpointFilter
指定投屏客户端在连接后希望与之交互的终端节点的属性,例如供应商ID和产品ID。这样在投屏客户端缓存中所需终端节点不可用时,会强制Matter TV投屏库完成完整的UDC流程来搜索所需的终端节点。
投屏客户端可以在其想要连接的MCCastingPlayer
对象上调用verifyOrEstablishConnection
,并通过NSErrors
处理该过程中可能发生的任何错误。
// 连接后MCCastingApp希望与之交互的MCCastingPlayer上MCEndpoint的VendorId
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() 已完成,并出现:\(err)")
DispatchQueue.main.async {
if err == nil {
self.connectionSuccess = true
self.connectionStatus = "成功连接到投屏播放器!"
} else {
self.connectionSuccess = false
self.connectionStatus = "连接投屏播放器失败,并出现:\(String(describing: err))"
}
}
}
// 如果使用备用CastingPlayer/配网者生成的密码UDC功能,
// 定义回调来处理CastingPlayer的CommissionerDeclaration消息。
let commissionerDeclarationCallback: (MCCommissionerDeclaration) - > Void = {
commissionerDeclarationMessage in
DispatchQueue.main.async {
self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,收到来自MCCastingPlayer的消息:\n\(commissionerDeclarationMessage)")
if commissionerDeclarationMessage.commissionerPasscode {
if let topViewController = self.getTopMostViewController() {
self.displayPasscodeInputDialog(on: topViewController, continueConnecting: {
userEnteredPasscode in
self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,继续连接用户输入的MCCastingPlayer/配网者生成的密码:\(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() 失败,正在调用stopConnecting()")
self.connectionStatus = "使用用户输入的密码更新MCAppParametersDataSource失败:\n\n请原路返回并重试。"
self.connectionSuccess = false
// 由于我们更新密码失败,尝试取消与
// CastingPlayer/配网者的连接。
let err = selectedCastingPlayer ? .stopConnecting()
if err == nil {
self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,InitializationExample.getAppParametersDataSource() 失败,然后stopConnecting() 成功")
} else {
self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,InitializationExample.getAppParametersDataSource() 失败,然后stopConnecting() 失败,原因是:\(err)")
}
return
}
// 调用continueConnecting以完成配网过程。
let errContinue = selectedCastingPlayer ? .continueConnecting()
if errContinue == nil {
self.connectionStatus = "继续使用用户输入的密码进行连接:\(userEnteredPasscode)"
} else {
self.connectionStatus = "继续连接到投屏播放器失败,并出现:\(String(describing: errContinue)) \n\n请原路返回并重试。"
self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,MCCastingPlayer.continueConnecting() 失败,原因是:\(errContinue)")
// 由于continueConnecting() 失败,请尝试通过调用
// stopConnecting() 来取消与CastingPlayer/配网者连接的尝试。
let err = selectedCastingPlayer ? .stopConnecting()
if err == nil {
self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,MCCastingPlayer.continueConnecting() 失败,然后stopConnecting() 成功")
} else {
self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,MCCastingPlayer.continueConnecting() 失败,然后stopConnecting() 失败,原因是:\(err)")
}
}
}, cancelConnecting: {
self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback,连接尝试已由用户取消,正在调用MCCastingPlayer.stopConnecting()")
let err = selectedCastingPlayer ? .stopConnecting()
...
})
}
}
}
}
let identificationDeclarationOptions: MCIdentificationDeclarationOptions
let targetAppInfo: MCTargetAppInfo
let connectionCallbacks: MCConnectionCallbacks
// 指定配网后客户端想要与之交互的TargetApp。如果传入此值,
// 在设备上找不到所需的TargetApp时,VerifyOrEstablishConnection() 将强制执行UDC。
// CastingStore
identificationDeclarationOptions = MCIdentificationDeclarationOptions()
targetAppInfo = MCTargetAppInfo(vendorId: kDesiredEndpointVendorId)
connectionCallbacks = MCConnectionCallbacks(
callbacks: connectionCompleteCallback,
commissionerDeclarationCallback: nil
)
identificationDeclarationOptions.addTargetAppInfo(targetAppInfo)
// 如果使用备用CastingPlayer/配网者生成的密码UDC功能,
// 设置IdentificationDeclaration CommissionerPasscode标记来指示CastingPlayer或
// 配网者使用其生成的密码来进行配网。设置
// MCConnectionCallbacks中的CommissionerDeclarationCallback。
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() 失败,原因是:\(err)")
}
}
3. 选择投屏播放器终端节点
成功连接CastingPlayer
对象后,投屏客户端可以根据其属性(例如供应商ID、产品ID或支持的集群列表)选择要与之交互的终端节点之一。选择MCEndpoint
,如下所示。
// 连接后MCCastingApp希望与之交互的MCCastingPlayer上MCEndpoint的VendorId
let sampleEndpointVid: Int = 65521
...
// 在MCCastingPlayer上选择MCEndpoint来调用命令
if
let endpoint: MCEndpoint = castingPlayer.endpoints().filter({
$0.vendorId().intValue == sampleEndpointVid
}).first {
...
}
4. 与投屏终端节点交互
投屏客户端选择了一个终端节点后,它就能够向其发出命令、读取当前播放状态以及订阅播放事件。请参阅以下操作系统特定文件,查找集群、命令和属性列表及其可用于Matter电视投屏库的请求和响应类型。
请参阅以下文件(仅提供英文版):
- 有关支持的集群、命令和属性的列表,请参阅: MCClusterObjects.h.
- 有关要在命令中使用的ID以及请求和响应类型,请参阅: MCCommandObjects.h和MCCommandPayloads.h。
- 有关属性读取操作,请参阅: 读取操作和订阅: MCAttributeObjects.h。
5. 发出命令
给定一个终端节点 (MCEndpoint
),它可以通过调用MCContentLauncherClusterLaunchURLCommand
上的invoke方法来发送LaunchURL
命令,该命令是内容启动器集群的一部分。
// 验证所选终端节点是否支持ContentLauncher集群
if (!endpoint.hasCluster(MCEndpointClusterTypeContentLauncher)) {
self.Log.error("未找到支持终端节点的ContentLauncher集群")
DispatchQueue.main.async {
self.status = "未找到支持终端节点的ContentLauncher集群"
}
return
}
// 从终端节点获取ContentLauncher集群
let contentLaunchercluster: MCContentLauncherCluster = endpoint.cluster(
for: MCEndpointClusterTypeContentLauncher) as!MCContentLauncherCluster
// 从contentLauncherCluster获取launchURLCommand
let launchURLCommand: MCContentLauncherClusterLaunchURLCommand ? = contentLaunchercluster.launchURLCommand()
if (launchURLCommand == nil) {
self.Log.error("集群上不支持LaunchURL")
DispatchQueue.main.async {
self.status = "集群上不支持LaunchURL"
}
return
}
// 创建LaunchURL请求
let request: MCContentLauncherClusterLaunchURLParams = MCContentLauncherClusterLaunchURLParams()
request.contentURL = contentUrl
request.displayString = displayString
// 传入完成块时在launchURLCommand上调用invoke
launchURLCommand!.invoke(request, context: nil, completion: {
context,
err,
response in
DispatchQueue.main.async {
if (err == nil) {
self.Log.info("LaunchURLCommand调用成功完成,并出现\(String(describing: response))")
self.status = "成功。响应数据:\(String(describing: response?.data))"
} else {
self.Log.error("LaunchURLCommand调用完成失败,并出现\(String(describing: err))")
self.status = "失败:\(String(describing: err))"
}
}
},
timedInvokeTimeoutMs: 5000 ) // 5000毫秒后超时
6. 读取操作
CastingClient
可以从CastingPlayer
对象上的终端节点读取一个属性。在尝试读取之前,它应确保所需的集群和属性在终端节点上可供读取。
在MCEndpoint
中,可以通过调用MCApplicationBasicClusterVendorIDAttribute
上的read方法,以类似方式读取VendorID
。
// 验证所选终端节点是否支持ApplicationBasic集群
if (!endpoint.hasCluster(MCEndpointClusterTypeApplicationBasic)) {
self.Log.error("未找到支持终端节点的ApplicationBasic集群")
DispatchQueue.main.async {
self.status = "未找到支持终端节点的ApplicationBasic集群"
}
return
}
// 从终端节点获取ApplicationBasic
let applicationBasiccluster: MCApplicationBasicCluster = endpoint.cluster(
for: MCEndpointClusterTypeApplicationBasic) as!MCApplicationBasicCluster
// 从applicationBasiccluster获取vendorIDAttribute
let vendorIDAttribute: MCApplicationBasicClusterVendorIDAttribute ? = applicationBasiccluster.vendorIDAttribute()
if (vendorIDAttribute == nil) {
self.Log.error("集群上不支持VendorID属性")
DispatchQueue.main.async {
self.status = "集群上不支持VendorID属性"
}
return
}
// 在vendorIDAttribute上调用read并传入完成块
vendorIDAttribute!.read(nil) {
context,
before,
after,
err in
DispatchQueue.main.async {
if (err != nil) {
self.Log.error("读取VendorID值时出错\(String(describing: err))")
self.status = "读取VendorID值时出错\(String(describing: err))"
return
}
if (before != nil) {
self.Log.info("读取VendorID值:\(String(describing: after)),之前:\(String(describing: before))")
self.status = "读取VendorID值:\(String(describing: after)),之前:\(String(describing: before))"
} else {
self.Log.info("读取VendorID值:\(String(describing: after))")
self.status = "读取VendorID值:\(String(describing: after))"
}
}
}
相关主题
Last updated: 2025年6月13日