開発者コンソール

Matterキャストの統合

Matterキャストの統合

アプリでMatterキャストを有効にするには、以下を更新する必要があります。

  • クライアント: これはユーザーが使用するAndroidまたはiOS用のスマートフォンアプリです。スマートフォンアプリをMatterクライアントにすると、ユーザーがFire TVなどのキャストターゲットを検出できるようになります。さらに、コンテンツをキャストしたり、キャストセッションを制御したりすることもできます。
  • コンテンツアプリ: これはFire TVでユーザーが使用するアプリです。TV対応アプリをMatterコンテンツアプリにすると、クライアントがコンテンツアプリを制御できるようになります。たとえば、クライアントで特定のコンテンツの再生を開始できます。

クライアントとプレーヤー間の通信はMatterプロトコルに従います。開発者は、Fire TVデバイス向けのクライアントを開発することも、Matterオープンソースリポジトリ(英語のみ)にあるいずれかのサンプルプレーヤーを使用することもできます。このリポジトリには、Android TVで実行できるAndroidプレーヤー(英語のみ)が含まれています。同様に、プレーヤーとコンテンツアプリ間の通信はMatter固有のAndroidインテントを使用して行われるため、開発にはFire TVデバイスを使用することも、一般的なAndroid TV上のAndroidプレーヤーを使用することもできます。以下では、MatterキャストSDKをAndroidまたはiOSクライアントアプリに統合する方法と、その操作方法について説明します。

手順1: MatterキャストSDKをクライアントアプリに統合する

AndroidまたはiOSのモバイルアプリにMatter SDKを統合して初期化するには、以下の手順が必要です。

ビルドとセットアップ

このドキュメントで説明されているAPIを使用するには、AndroidまたはiOSプロジェクトにビルドされたMatterキャストライブラリをクライアントに組み込む必要があります。

キャッシュ

クライアントは、以前に接続したプレーヤーの情報をデバイス上のキャッシュに保持します。このキャッシュされた情報を使用することで、クライアントは時間のかかるコミッショニングプロセスをスキップできます。CASEセッションを再確立するだけで済むため、該当するプレーヤーによりすばやく、より少ないリソースで接続できます。このキャッシュをクリアするには、クライアントでClearCacheメソッドを呼び出します。キャッシュのクリアは、たとえばユーザーがクライアントからサインアウトするときに役立ちます。

Androidでの統合

CSAのGitHubリポジトリからAndroid SDKをダウンロードし、プロジェクトに追加します。プロジェクトに追加されたら、以下の手順(リスト全体)でライブラリを初期化します。

1.ローテーションデバイス識別子を定義する

これはデバイスごとの一意の識別子で、ランダムに生成された128ビット以上のオクテット文字列で構成されます。ローテーションデバイス識別子の生成方法の詳細については、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仕様のオンボーディングペイロードに関するセクションを参照してください。

クリップボードにコピーしました。

public static class CommissionableDataProvider implements DataProvider < CommissionableData > {
    CommissionableData commissionableData =
    // コミッショニングのデモ用限定のダミー値。これらは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;
    }

    // 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...<中略>...CXE1M="; // デモ用限定のダミー値
    private static final String kDevelopmentDAC_PrivateKey_FFF1_8001 = "qrYAror...<中略>...StE+/8=";
    private static final String KPAI_FFF1_8000_Cert_Array = "MIIByzC...<中略>...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キャストセッションの開始前に1回だけ行うことができます。

クリップボードにコピーしました。

public static MatterError initAndStart(Context applicationContext) {
    // グローバルキャッシュパラメーターをSDKに渡すためのAppParametersオブジェクトを作成します。
    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リポジトリのGitクローンを作成します。
  • ターミナルで、フォルダを変更してリポジトリの場所に移動し、source scripts/bootstrap.shを実行します。これにより、必要なライブラリと関連付けがビルドされます。
  • サンプルiOSアプリを開き、ローカルでフレームワークをビルドします。
  • iOSアプリにフレームワークを組み込みます。

B. アプリにMatterキャストフレームワークを統合する

1.ローテーションデバイス識別子を定義する

これはデバイスごとの一意の識別子で、ランダムに生成された128ビット以上のオクテット文字列で構成されます。ローテーションデバイス識別子の生成方法の詳細については、Matterの仕様を参照してください。以下のようにDataProviderオブジェクトをインスタンス化して、この識別子を提供します。

クリップボードにコピーしました。

class MCAppParametersDataSource: NSObject, MCDataSource {
    func castingAppDidReceiveRequestForRotatingDeviceIdUniqueId(_ sender: Any) - > Data {
            // 16バイト(ConfigurationManager::kMinRotatingDeviceIDUniqueIDLength)以上のダミー値、デモ用限定
            return "0123456789ABCDEF".data(using: .utf8) !
        }
        ...
}

2.コミッショニングデータを初期化する

このオブジェクトは、パスコード、識別子、その他の情報を格納します。これによりアプリが識別され、コミッショニングプロセス中にCastingPlayerオブジェクトが提供されます。パスコードは27ビットの符号なし整数として含める必要があります。これは、コミッショニング中に所有証明として機能します。識別子は12ビットの符号なし整数として含める必要があり、コミッショニング中にデバイスがアドバタイズする値と一致する必要があります。Matter仕様のオンボーディングペイロードに関するセクションの詳細については、Matterコア仕様の第5章(コミッショニングの章)を参照してください。

上記で定義したMCAppParametersDataSourceクラスにfunc commissioningDataProviderを追加します。これにより、必要な値がMCCastingAppに提供されます。CastingPlayer/コミッショナーによって生成されたパスコードを入力するUDC機能を使用する場合は、キャストクライアントで、VerifyOrEstablishConnection() APIの呼び出し中にcommissioningDataProviderを更新する必要があります(後述)。以下の例では、update関数により、ユーザーがキャストクライアントのUXで入力した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.デバイス証明認証情報を追加する

このオブジェクトは、DeviceAttestationCertificateProductAttestationIntermediateCertificateなどの情報を格納し、コミッショニング中のMatterデバイス証明プロセスの一部として、Matter TVキャストライブラリからの呼び出しに応じてメッセージに署名する方法を実装します。

上記で定義したMCAppParametersDataSourceクラスに、castingAppDidReceiveRequestForDeviceAttestationCredentials関数とdidReceiveRequestToSignCertificateRequest関数を追加します。前者はMCDeviceAttestationCredentialsを返し、後者はキャストクライアントのメッセージに署名します。

クリップボードにコピーしました。

// デモ用限定のダミーのDAC値。
let kDevelopmentDAC_Cert_FFF1_8001: Data = Data(base64Encoded: "MIIB..<中略>..CXE1M=") !;
let kDevelopmentDAC_PrivateKey_FFF1_8001: Data = Data(base64Encoded: "qrYA<中略>tE+/8=") !;
let kDevelopmentDAC_PublicKey_FFF1_8001: Data = Data(base64Encoded: "BEY6<中略>I=") !;
let KPAI_FFF1_8000_Cert_Array: Data = Data(base64Encoded: "MIIB<中略>4kQ==") !;
let kCertificationDeclaration: Data = Data(base64Encoded: "MII<中略>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("メッセージに署名できませんでした。エラー:\(String(describing: error))")
        return MATTER_ERROR_INVALID_ARGUMENT
    } else if (asn1SignatureData == nil) {
        Log.error("メッセージに署名できませんでした。asn1SignatureDataがnilです。")
        return MATTER_ERROR_INVALID_ARGUMENT
    }

    // ASN.1 DER署名を未加工のSEC1形式に変換します。
    return MCCryptoUtils.ecdsaAsn1SignatureToRaw(withFeLengthBytes: 32,
        asn1Signature: asn1SignatureData!,
        outRawSignature: & outRawSignature.pointee)
}

4.SDKを初期化する

上記のDataProviderオブジェクトを作成したら、以下の説明に従ってキャストアプリを初期化できます。

MCCastingApp.initializeを呼び出し、MCAppParametersDataSourceのオブジェクトを渡します。

クリップボードにコピーしました。

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

初期化後、アプリでUIApplication.didBecomeActiveNotificationUIApplication.willResignActiveNotificationが通知されたときに、MCCastingApp共有インスタンスのstartとstopを呼び出します。

クリップボードにコピーしました。

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の停止に失敗しました:\(err)")
                            }
                        })
                    }
                }
        } // WindowGroup
    } // body
} // App

手順2:MatterをFire TV対応アプリに統合する

MatterキャストSDKをクライアントアプリに統合したら、クライアントで同じMatterネットワーク上にあるプレーヤーとコンテンツアプリを検出する準備は完了です。Fire TV上のコンテンツアプリからMatterコマンドをリッスン、送信、受信できるようにするには、コンテンツアプリをFire TV Matterキャストサービスと統合する必要があります。この手順では、Android TVのサンプルアプリを使用して、既にMatterキャストが有効になっているAndroid TV用のコンテンツアプリをブートストラップする方法を示します。

1.Androidマニフェストを更新する

Matterキャストを有効にするには、Androidマニフェストで提供する情報を拡張する必要があります。プレーヤーとMatterエージェントクライアントは、Androidマニフェストに指定された情報を使用して、どのクライアントにコンテンツアプリへのキャストを許可するか、どのMatterクラスターがコンテンツアプリでサポートされているかを把握します。

1.製品とベンダーの情報

アプリのタグを更新して、製品IDとベンダーIDを含めます。詳細については、「アプリの証明」を参照してください。

開発目的では、次の例に示す製品ID(65521)、ベンダーID(32769)、ベンダー名を使用できます。

<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" />

static_matter_clustersというJSONファイルを作成し、コンテンツアプリでサポートされるクラスターのリストを記述します。

クリップボードにコピーしました。

{
   "clusters": [
     // アカウントログインクラスター
     {
       "identifier": 1294
     },
     {
       "identifier": 1289,
       "features": [
         "NV",
         "LK",
         "NK"
       ]
     },
     {
       "identifier": 1290,
       "features": [
         "CS"
       ]
     },
     {
       "identifier": 1286,
       "features": [
         "AS"
       ]
     }
   ]
 }

2.Matter AIDLファイルと統合する

Matterキャストでは、Android Interface Definition Language(AIDL)を使用して、プレーヤーとコンテンツアプリ間で通信を行うためのプログラミングインターフェイスを定義します。サンプルアプリから、3つのAIDLファイル (IMatterapplicationAgent.aidlSetSupportedClustersRequest.aidlSupportedCluster.aidl)をアプリのリポジトリにコピーします。これにより、必須のメソッドであるsetSupportedClustersreportAttributeChangeを統合しやすくなります。これらのメソッドは以下の処理に必要です。

  • setSupportedClusters
    • このメソッドは、クラスターを動的にMatterエージェントにレポートします。
    • レポートは増分ではなく、呼び出しごとにすべてのクラスターのセットを含めます。以前に追加されたクラスターが最新のメソッド呼び出しで省略された場合、それらのクラスターは削除されます。
    • アプリリソースで宣言された静的クラスターは上記の動作の影響を受けず、削除されることはありません。
    • 動的クラスターは、クラスター名に基づいて、静的クラスターをオーバーライドして隠すために使用できます。
  • reportAttributeChange
    • このメソッドは、コンテンツアプリによる属性の変更をレポートします。
    • また、レポートされた属性変更のクラスターIDと属性IDも受け取ります。

追加のドキュメントについては、Matter tv app common-api(英語のみ)を参照してください。

3.Matterエージェントクライアントを作成する

サンプルに含まれているMatterエージェントクライアントを作成します。エージェントは、Matterサービスに接続したり、上記のAIDLの手順で提供されるメソッドを使用したりするために使用されます。

バインダー接続を確立する例を以下に示します。

クリップボードにコピーしました。

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対応アプリをオンデマンドでインストールする

以降では、ユーザーがコンテンツアプリをインストール済みで、そのアプリにキャスト中であることを前提としています。ただし、これに該当しない場合もあります。コンテンツアプリがインストールされていない場合、Fire TVでは、アプリランチャークラスターのペイロードのApplicationID情報にアプリのパッケージ名が指定されていれば、ユーザーにアプリのインストールを促すことができます。

手順3:プレーヤーを操作する

この手順では、プレーヤーを検出、選択、接続する方法、コマンドを発行する方法、再生状態などの属性を読み取る方法、再生イベントにサブスクライブする方法を説明します。

Androidでの操作

1.キャストプレーヤーを検出する

クライアントでは、Fire TVなどのプレーヤーを検出するために、DNS-SD経由でMatter Commissioner Discoveryを使用して、プレーヤーの検出、更新、ネットワークからの消失を通知するプレーヤーイベントをリッスンします。

まず、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():castingPlayerListリスト内の既存のCastingPlayerエントリ" + playerInList.get().getDeviceId() + "を更新します");
                    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():castingPlayerListリスト内の既存のCastingPlayerエントリ" + playerInList.get().getDeviceId() + "を削除します");
                    arrayAdapter.remove(playerInList.get());
                }
            });
        }
    };

次に、これらのリスナーを登録し、検出を開始します。検出されたプレーヤーの変更をリッスンするために、castingPlayerChangeListenerMatterCastingPlayerDiscoveryのリスナーとして追加してから、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, "startDiscovery()でエラーが発生しました:" + err);
    return false;
}

プレーヤーでサポートされているエンドポイントのリストを確認するには、プレーヤーに接続します。キャストプレーヤーでサポートされている利用可能なエンドポイントを検出する方法の詳細については、接続に関するセクション(英語のみ)を参照してください。

2.検出されたプレーヤーに接続する

検出中に作成された各プレーヤーオブジェクトには、deviceNamevendorIdproductIdなど、ユーザーが適切なプレーヤーを選択するために役立つ情報が含まれています。クライアントは、Matter User Directed Commissioning(UDC)を使用してselectedCastingPlayerへの接続を試みることができます。プレーヤーオブジェクトによるクライアントのコミッショニングが完了すると、Matter TVキャストライブラリは、プレーヤーオブジェクトに再接続するために必要な情報をローカルにキャッシュします。それ以降、キャストクライアントでは、プレーヤーと直接CASEを確立することでUDCプロセス全体をスキップできます。接続後のプレーヤーオブジェクトには、そのプレーヤーで利用可能なエンドポイントのリストが格納されます。接続時には、次の引数をオプションとして渡すこともできます。commissioningWindowTimeoutSecは、コミッショニングが必要な場合に、コミッショニングウィンドウを開いたままにしておく時間を示します。DesiredEndpointFilterは、接続後にクライアントが操作するエンドポイントの属性(ベンダーIDや製品IDなど)を指定します。これを指定すると、目的のエンドポイントがクライアントのキャッシュで利用できない場合に、UDCプロセス全体を実行してエンドポイントを検索するようにMatter TVキャストライブラリに強制します。

クライアントは、接続するCastingPlayerオブジェクトのverifyOrEstablishConnectionを呼び出すことができます。

クリップボードにコピーしました。

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

// コミッショニング後にクライアントが操作するTargetAppを指定します。この値が渡された場合、
// verifyOrEstablishConnection()は、目的のTargetAppがデバイス上のCastingStoreに見つからなければ
// UDCを強制します。
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メッセージを処理するコールバックを定義します。
        // これは、キャストクライアント/コミッショニー生成のパスコードをコミッショニングに使用する場合はnullにできます。
        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、サポートされているクラスターのリストなど)に基づいて、操作するエンドポイントを1つ選択できます。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でサポートされているクラスター、コマンド、属性のリストは、サンプルコードで確認できます。

コマンドの発行

前の手順で取得したエンドポイントを使用すると、LaunchURLコマンド(コンテンツランチャークラスターの一部)を送信できます。これを行うには、ChipClusters.ContentLauncherClusterオブジェクトのlaunchURLメソッドを呼び出します。

クリップボードにコピーしました。

// エンドポイントからChipClusters.ContentLauncherClusterを取得します。
ChipClusters.ContentLauncherCluster cluster =
    endpoint.getCluster(ChipClusters.ContentLauncherCluster.class);
if (cluster == null) {
    Log.e(TAG, "エンドポイントのContentLauncherClusterを取得できませんでした。エンドポイントID:" + endpoint.getId());
    return;
}

// クラスターオブジェクトのlaunchURLを呼び出し、
// ChipClusters.ContentLauncherCluster.LauncherResponseCallbackとリクエストパラメーターを渡します。
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の結果\nステータス:" + status + "、データ:" + 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の結果\nエラー:" + 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;
}

// クラスターオブジェクトのsubscribeCurrentStateAttributeを呼び出して
// ChipClusters.IntegerAttributeCallbackを渡し、最小間隔と最大間隔のパラメーターとして
// [0, 1]を指定します。
cluster.subscribeCurrentStateAttribute(
    new ChipClusters.IntegerAttributeCallback() {
        @Override
        public void onSuccess(int value) {
            Log.d(TAG,
                "サブスクリプションでの読み取りに成功しました。Value: " + 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ファブリックに存在するビデオキャストプレーヤーとコンテンツアプリを検出して通信できます。

キャストプレーヤーの追加、削除、更新の通知をリッスンする、func addDiscoveredCastingPlayers、func removeDiscoveredCastingPlayers、func updateDiscoveredCastingPlayersを実装します。

クリップボードにコピーしました。

@objc
func didAddDiscoveredCastingPlayers(notification: Notification) {
    Log.info("didAddDiscoveredCastingPlayers()が呼び出されました")
    guard
    let userInfo = notification.userInfo,
        let castingPlayer = userInfo["castingPlayer"] as ? MCCastingPlayer
    else {
        self.Log.error("didAddDiscoveredCastingPlayersがMCCastingPlayerなしで呼び出されました")
        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("didRemoveDiscoveredCastingPlayersがMCCastingPlayerなしで呼び出されました")
        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("didUpdateDiscoveredCastingPlayersがMCCastingPlayerなしで呼び出されました")
        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
        }
    }
}

これらのリスナーを登録して検出を開始します。そのためには、NotificationCenteraddObserverを適切なselectorと共に呼び出してから、MCCastingPlayerDiscoverysharedInstanceオブジェクトのstartを呼び出します。

クリップボードにコピーしました。

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 User Directed Commissioning(UDC)を使用してselectedCastingPlayerへの接続を試みることができます。プレーヤーオブジェクトによるクライアントのコミッショニングが完了すると、Matter TVキャストライブラリは、CastingPlayerオブジェクトに再接続するために必要な情報をローカルにキャッシュします。それ以降、キャストクライアントでは、CastingPlayerオブジェクトと直接CASEを確立することでUDCプロセス全体をスキップできます。接続後のCastingPlayerオブジェクトには、そのCastingPlayerオブジェクトで利用可能なエンドポイントのリストが格納されます。commissioningWindowTimeoutSecは、コミッショニングウィンドウが必要な場合に、ウィンドウを開いたままにしておく時間を示します。DesiredEndpointFilterは、接続後にキャストクライアントが操作するエンドポイントの属性(ベンダーIDや製品IDなど)を指定します。これを指定すると、目的のエンドポイントがキャストクライアントのキャッシュで利用できない場合に、UDCプロセス全体を実行してエンドポイントを検索するようにMatter TVキャストライブラリに強制します。

キャストクライアントは、接続するMCCastingPlayerオブジェクトのverifyOrEstablishConnectionを呼び出すことができます。そのプロセスで発生する可能性のあるエラーはすべて、NSErrorを通じて処理できます。

クリップボードにコピーしました。

// 接続後に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を指定します。この値が渡された場合、
    // verifyOrEstablishConnection()は、目的のTargetAppがデバイス上のCastingStoreに見つからなければ
    // UDCを強制します。
    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、サポートされているクラスターのリストなど)に基づいて、操作するエンドポイントを1つ選択できます。以下に示すように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 TVキャストライブラリで使用できるクラスター、コマンド、属性のリストと、それらで使用されるリクエストおよびレスポンスの型を確認するには、オペレーティングシステム固有のファイルを参照してください。

次のファイルを参照してください。

5. コマンドの発行

エンドポイント(MCEndpoint)を使用すると、コンテンツランチャークラスターの一部であるLaunchURLコマンドを送信できます。これを行うには、MCContentLauncherClusterLaunchURLCommandのinvokeメソッドを呼び出します。

クリップボードにコピーしました。

// 選択したエンドポイントで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を呼び出し、completionブロックを渡します。
launchURLCommand!.invoke(request, context: nil, completion: {
        context,
        err,
        response in
        DispatchQueue.main.async {
            if (err == nil) {
                self.Log.info("launchURLCommandのinvokeが正常に完了しました。レスポンス:\(String(describing: response))")
                self.status = "成功。レスポンスデータ:\(String(describing: response?.data))"
            } else {
                self.Log.error("launchURLCommandのinvokeが失敗で完了しました。エラー:\(String(describing: err))")
                self.status = "失敗:\(String(describing: err))"
            }
        }
    },
    timedInvokeTimeoutMs: 5000) // 5000ミリ秒後にタイムアウト

6. 読み取り操作を行う

CastingClientでは、CastingPlayerオブジェクトのエンドポイントから属性を読み取ることができます。読み取りを試みる前に、目的のクラスターと属性がエンドポイントで読み取り可能であることを確認する必要があります。

MCEndpoint内のVendorIDも同様に、MCApplicationBasicClusterVendorIDAttributeのreadメソッドを呼び出して読み取ることができます。

クリップボードにコピーしました。

// 選択されたエンドポイントで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を呼び出し、completionブロックを渡します。
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日