スマートウォッチでタブレット上のビデオプレーヤーを制御できる、ビデオ再生の分散デモ

# ビデオ再生制御シナリオ
最近では、ユーザーはスマートデバイスでビデオを見ることが多いですが、次のような多くの課題に直面しています。

- ビデオプレーヤーを制御するには、大画面デバイスを手に持つ必要
- ビデオの再生中はデバイス上で他のタスクを動作できない

これらの課題を解決するには、以下の解決策を提案します。

- 軽量のデバイス(スマートウォッチなど)をリモコンとして使用し、大画面デバイスでビデオプレーヤーを操作する　。
- マルチコラボレーション API を使用してHarmonyOSアプリを開発し、クロスデバイスをデプロイ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50470/5929d0a9-220c-1c42-d054-eed198071187.png)
#Harmony OSはビデオ再生制御シナリオをどのように解決するか
Harmony OS がサポートするマルチデバイスコラボレーションは次の通リです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50470/b90fa503-9879-60ac-0efe-135a9d9ed975.png)
具体的には、分散スケジューラを使用して、アビリティーの起動または停止できます。

 * 分散スケジューラは、リモートの能力管理機能を提供
リモートデバイスからFA (Feature Ability, UIがありアビリティを起動できるし、また、PA（Particle Ability,UIがないアビリティ、サービスまたデータのアビリティ）を起動また停止できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50470/6d9aa45a-5663-974c-a133-48affa9cdb6b.png)

 * アビリティーへの接続または切断
分散スケジューラは、クロスデバイスPA制御を提供します。リモート PA に接続することで、タスク スケジュール用にクロスデバイス クライアントを取得できます。クロスデバイス タスクが完了すると、リモート PA から切断してこのクライアントを登録解除できます。

 * アビリティーの移行
分散スケジューラを使用すると、デバイス間のアビリティ移行が可能になります。移行メソッドを呼び出して、ローカルデバイスから指定したリモートデバイスにFAをシームレスに移行できます。
このシナリオを解決するために、リモート PA に接続してクロスデバイスを制御するHarmony OSの分散スケジューラを使用しました。
# Demoを開発
プロジェクト構造: 2 つのメイン モジュールがあります
 * <strong>スマートウォッチ モジュール</strong>
  * リモート コントロール モジュール
  * ウェアラブルデバイスだけでなく、スマートフォンやタブレットにも対応 
 * <strong>タブレット モジュール</strong>
  * ビデオ プレーヤー モジュール
  * スマートウォッチモジュールからのコマンドリクエストを処理して、再生、一時停止、シーク、復帰、スキップなどの操作を実行します。
データ処理フローチャートは、以下になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50470/56278ba3-fa9c-9d19-3822-7f5f6ea956fd.png)

##スマートウォッチモジュールを開発
**スマートウォッチモジュール**の概要:

- <strong>MainAbility.java</strong>: エンドポイントのアビリティ
- <strong>MyApplication.java</strong>: アプリケーションクラス
- <strong>controller</strong>
 - <strong>Const.java</strong>:定数値クラス
 - <strong>LogUtil.java</strong>:ログを記入
 - <strong>PlayerRemoteProxy.java</strong>: リモートコントロール クラス
- <strong>slice</strong>
 - <strong>MainAbilitySlice.java</strong>:ユーザーと対話するためのメインUIクラス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50470/efc410bf-e587-565c-900c-6fc9f6495783.png)

### UIを作成
- <strong>base.layout</strong>ダイレクトリーの中に<strong>ability_main.layout</strong> を作成

```xml
<?xml version="1.0" encoding="utf-8"?>
<DependentLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:orientation="vertical">

    <Image
        ohos:id="$+id:play_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:pause_button"
        ohos:center_in_parent="true" />

    <Image
        ohos:id="$+id:forward_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:forward_button"
        ohos:center_in_parent="true"
        ohos:align_parent_right="true" />

    <Image
        ohos:id="$+id:rewind_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:rewind_button"
        ohos:center_in_parent="true"
        ohos:align_parent_left="true"/>

    <Image
        ohos:id="$+id:next_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:next_button"
        ohos:center_in_parent="true"
        ohos:align_parent_top="true"/>

    <Image
        ohos:id="$+id:previous_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:previous_button"
        ohos:center_in_parent="true"
        ohos:align_parent_bottom="true"/>

</DependentLayout>
```
![代替テキスト](https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/043/403/889/5190081000043403889.20210506110755.22774358928058976700781556861572:50520822052325:2800:22D15D5070E298EBCDBD4C2C69C065672482D6E781DEE5A663CCA499E310CF1D.png)


- **setContentUI** APIを使用、AbilitySliceクラスの``onStart``メソッドにレイアウトをロード

```java
super.setUIContent(ResourceTable.Layout_ability_main);
```
- **setMainRoute** APIを使用、Abilityクラスの``onStart``メソッドにAbilitySliceへメインルートを設定　 

```java
super.setMainRoute(MainAbilitySlice.class.getName());
``` 

### 必須な権限を設定　
- **config.json**ファイルに以下の権限を設定
    - 分散権限
        - ohos.permission.DISTRIBUTED_DATASYNC
        - ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE
        - ohos.permission.GET_DISTRIBUTED_DEVICE_INFO　
    - バンドル情報を取得 
        - ohos.permission.GET_BUNDLE_INFO
- AbilitySliceクラスの``onStart``メソッドにランタイムで権限をリクエスト

```java
@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_main);
    requestPermissions(
            SystemPermission.DISTRIBUTED_DATASYNC
    );
}

private void requestPermissions(String... permissions) {
    for (String permission : permissions) {
        if (verifyCallingOrSelfPermission(permission) != IBundleManager.PERMISSION_GRANTED) {
            requestPermissionsFromUser(
                    new String[] {
                            permission
                    },
                    MainAbility.REQUEST_CODE);
        }
    }
}  
```

### 接続中のタブレットを取得
権限を認可次第、以下のメソッドを使用、接続中の端末一覧を取得とタブレットを指定：

```java
private void getTabletDevice() {
    List<DeviceInfo> devices = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
    if (devices.isEmpty()) {
        showToast("No device found");
    } else {
        devices.forEach(deviceInfo -> {
            LogUtil.info(TAG, "Found device " + deviceInfo.getDeviceType());
            if (deviceInfo.getDeviceType() == DeviceInfo.DeviceType.SMART_PAD &&
                    (tablet == null || !tablet.getDeviceId().equals(deviceInfo.getDeviceId()))) {
                tablet = deviceInfo;
                startVideoPlayerOnTablet();
            }
        });
    }
}
```

端末がオンラインまたはオフラインのケースを対応するため、デバイス状態変更のコールバックを定義：

```java
private final EventHandler handler = new EventHandler(EventRunner.current()) {
    @Override
    protected void processEvent(InnerEvent event) {
        if (event.eventId == EVENT_STATE_CHANGE) {
            getTabletDevice();
        }
    }
};

private final IDeviceStateCallback callback = new IDeviceStateCallback() {
    @Override
    public void onDeviceOffline(String deviceId, int deviceType) {
        if (tablet != null && tablet.getDeviceId().equals(deviceId)) {
            showToast("Device offline");
            disconnectAbility(connection);
            tablet = null;
        }
    }

    @Override
    public void onDeviceOnline(String deviceId, int deviceType) {
        handler.sendEvent(EVENT_STATE_CHANGE);
    }
};

@Override
public void onActive() {
    super.onActive();
    if (tablet == null) {
        getTabletDevice();
        DeviceManager.registerDeviceStateCallback(callback);
    } else {
        startVideoPlayerOnTablet();
    }
}
```

### タブレット上のService Abilityの接続を確立
接続中タブレットを取得後、そのサービスアビリティへの接続を確立

```java
private void startVideoPlayerOnTablet() {
    String deviceId = tablet.getDeviceId();
    if (deviceId == null) {
        return;
    }
    Intent intent = new Intent();
    Operation operation = new Intent.OperationBuilder()
            .withDeviceId(deviceId)
            .withBundleName(Const.BUNDLE_NAME)
            .withAbilityName(Const.ABILITY_NAME)
            .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
            .build();
    intent.setOperation(operation);
    try {
        List<AbilityInfo> abilityInfoList = getBundleManager().queryAbilityByIntent(
                intent,
                IBundleManager.GET_BUNDLE_DEFAULT,
                0);
        if (abilityInfoList != null && !abilityInfoList.isEmpty()) {
            connectAbility(intent, connection);
            LogUtil.info(TAG, "connect ability on tablet with id " + deviceId );
        } else {
            showToast("Cannot start video player on tablet");
            getMainTaskDispatcher().delayDispatch(this::terminate, MainAbility.TERMINATE_DELAY_TIME);
        }
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}    
```

クロスデバイスアビリティを接続するには、**FLAG_ABILITY_MULTI_DEVICE** を使用する必要があります。
**BUNDLE_NAME** と **ABILITY_NAME** は、const クラスで次のように定義されます。

```java
public class Const {
    public static final String BUNDLE_NAME = "com.huawei.superdevicedemo";
    public static final String ABILITY_NAME = "com.huawei.superdevicedemo.VideoPlayerServiceAbility";
    ...
 }   
```

接続を確立次第、リモートプロキシコントロールを作成、リモートボタンを設定：

```java
private final IAbilityConnection connection = new IAbilityConnection() {
    @Override
    public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {
        remoteProxy = new PlayerRemoteProxy(remote);
        isPlaying = true;
        LogUtil.info(TAG, "ability connect done!");
        remoteProxy.remoteControl(Const.START);
        setupRemoteButton();
    }

    @Override
    public void onAbilityDisconnectDone(ElementName elementName, int i) {
        LogUtil.info(TAG, "ability disconnect done!");
        disconnectAbility(connection);
    }
};

private void setupRemoteButton() {
    Image playButton = (Image) findComponentById(ResourceTable.Id_play_button);
    playButton.setClickedListener(component -> {
        isPlaying = !isPlaying;
        remoteProxy.remoteControl(isPlaying ? Const.PLAY : Const.PAUSE);
        playButton.setPixelMap(isPlaying ? ResourceTable.Media_pause_button : ResourceTable.Media_play_button);
    });

    Image forwardButton = (Image) findComponentById(ResourceTable.Id_forward_button);
    forwardButton.setClickedListener(component -> {
        remoteProxy.remoteControl(Const.FORWARD);
    });

    Image rewindButton = (Image) findComponentById(ResourceTable.Id_rewind_button);
    rewindButton.setClickedListener(component -> {
        remoteProxy.remoteControl(Const.REWIND);
    });

    Image nextButton = (Image) findComponentById(ResourceTable.Id_next_button);
    nextButton.setClickedListener(component -> {
        remoteProxy.remoteControl(Const.NEXT);
    });

    Image previousButton = (Image) findComponentById(ResourceTable.Id_previous_button);
    previousButton.setClickedListener(component -> {
        remoteProxy.remoteControl(Const.PREVIOUS);
    });
}

```

再生コントロールのアクションは const クラスで次のように定義されます。

```java
public class Const {
    ...
    public static final String START = "start";
    public static final String PLAY = "play";
    public static final String PAUSE = "pause";
    public static final String FORWARD = "forward";
    public static final String REWIND = "rewind";
    public static final String NEXT = "next";
    public static final String PREVIOUS = "previous";
}
```

### リモートコントロールを設定
**PlayerRemoteProxy** は、**sendRequest** を使ってクロスデバイスコントローラをサポートするように設計されています。

```java
public class PlayerRemoteProxy implements IRemoteBroker {
    private static final int REMOTE_COMMAND = 0;
    private final String TAG = PlayerRemoteProxy.class.getSimpleName();
    private final IRemoteObject remote;

    public PlayerRemoteProxy(IRemoteObject remote) {
        this.remote = remote;
    }

    @Override
    public IRemoteObject asObject() {
        return remote;
    }

    public void remoteControl(String action) {
        MessageParcel data = MessageParcel.obtain();
        MessageParcel reply = MessageParcel.obtain();
        MessageOption option = new MessageOption(MessageOption.TF_SYNC);
        data.writeString(action);
        try {
            remote.sendRequest(REMOTE_COMMAND, data, reply, option);
        } catch (RemoteException e) {
            LogUtil.error(TAG, "remote action error " + e.getMessage());
        } finally {
            data.reclaim();
            reply.reclaim();
        }
    }
}

```

## タブレットモジュールを開発
**タブレットモジュール**の概要:

- <strong>MainAbility.java</strong>: エンドポイントのアビリティ
- <strong>MyApplication.java</strong>: アプリケーションクラス
- **VideoPlayerServiceAbility.java**: サービスアビリティ(スマートウォッチから接続)
- <strong>controller</strong>
 - <strong>Const.java</strong>:定数値クラス
 - **DistributeNotificationPlugin.java**:内部イベントをパブリッシュ、サブクライブ、サブクライブ解除のプラグイン
 - <strong>LogUtil.java</strong>:ログを記入
 - **PlayerRemote.java**: スマートウォッチからのリモートリクエストを処理クラス
 - **ThreadPoolManager.java**: スレッドプールを管理クラス
 - **VideoElementManager.java**: ビデオリソースを管理クラス
 - **VideoPlayerPlugin.java**: ビデオプレーヤープラグイン
- <strong>slice</strong>
 - <strong>MainAbilitySlice.java</strong>:動画再生クラス
 
![](https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/043/403/889/5190081000043403889.20210506113552.21773546345298363923039670987751:50520822052325:2800:0A5C5AB4C6A8C99C4D80ECB370B209FFE4F22844A3BF2EC0953B4A0842106BC1.png) 

### UIを作成
- **base.layout**ダイレクトリーの中に**ability_main.layout**ファイルを作成

```xml
<?xml version="1.0" encoding="utf-8"?>
<DependentLayout
    xmlns:ohos="http://schemas.huawei.com/res/ohos"
    ohos:height="match_parent"
    ohos:width="match_parent"
    ohos:id="$+id:main_layout"
    ohos:orientation="vertical">

    <SurfaceProvider
        ohos:id="$+id:surface_provider"
        ohos:height="match_parent"
        ohos:width="match_parent"
        ohos:background_element="#000000"/>

    <Text
        ohos:id="$+id:title"
        ohos:text_size="40fp"
        ohos:width="match_content"
        ohos:height="match_content"
        ohos:text="Video"
        ohos:left_margin="5vp"
        ohos:top_margin="20vp"
        ohos:align_parent_top="true"
        ohos:center_in_parent="true"/>

    <RoundProgressBar
        ohos:id="$+id:round_progress_bar"
        ohos:height="200vp"
        ohos:width="200vp"
        ohos:visibility="hide"
        ohos:progress_width="10vp"
        ohos:progress_color="#47CC47"
        ohos:center_in_parent="true"/>

    <Image
        ohos:id="$+id:play_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:play_button"
        ohos:center_in_parent="true" />

    <Image
        ohos:id="$+id:forward_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:forward_button"
        ohos:center_in_parent="true"
        ohos:align_parent_right="true" />

    <Image
        ohos:id="$+id:rewind_button"
        ohos:height="match_content"
        ohos:width="match_content"
        ohos:image_src="$media:rewind_button"
        ohos:center_in_parent="true"
        ohos:align_parent_left="true"/>

</DependentLayout>
```
![](https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/043/403/889/5190081000043403889.20210506114118.50769581671137278026769171503481:50520822052325:2800:7CC72EB10EB69C3FDCB42FBF582AD99956A417A50658A355B7D6F5720FD4A37D.png)

- AbilitySliceクラスの``onStart``メソッドに**setContent**を使用、レイアウトをロード

```java
super.setUIContent(ResourceTable.Layout_ability_main);
```
- Abilityクラスの``onStart``メソッドに**setMainRoute**を使用、AbilitySliceへメインルートを設定します。また、他のアビリティから起動できるように、アクションルートを設定必要です

```java
super.setMainRoute(MainAbilitySlice.class.getName());
addActionRoute(Const.VIDEO_PLAY_ACTION, MainAbilitySlice.class.getName());
```
### 必須な権限を設定
- **config.json**ファイルに以下の権限を設定
    - 分散権限
        - ohos.permission.DISTRIBUTED_DATASYNC
        - ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE
        - ohos.permission.GET_DISTRIBUTED_DEVICE_INFO　
    - メディア権限 
        - ohos.permission.READ_MEDIA
        - ohos.permission.MEDIA_LOCATION
    - インターネット権限 
    　　-　ohos.permission.INTERNET 
- AbilitySliceクラスの``onStart``メソッドにランタイムで権限をリクエスト

```java
@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    super.setUIContent(ResourceTable.Layout_ability_main);
    requestPermissions(
            SystemPermission.READ_MEDIA,
            SystemPermission.MEDIA_LOCATION
    );
    setupUI();
    initData();
}

@Override
public void onActive() {
    super.onActive();
    requestPermissions(SystemPermission.DISTRIBUTED_DATASYNC);
}
```
### ビデオプレーヤーを作成
ビデオプレーヤーのデータ処理フローチャート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50470/268434d4-1c3f-084e-a838-483a02d752b5.png)
**VideoElementManager**はビデオリソースを管理します。
ビデオリソースは以下のソースからロード：
- 端末のライブラリ

```java
private void loadFromMediaLibrary(Context context) {
    Uri remoteUri = AVStorage.Video.Media.EXTERNAL_DATA_ABILITY_URI;
    DataAbilityHelper helper = DataAbilityHelper.creator(context, remoteUri, false);
    try {
        ResultSet resultSet = helper.query(remoteUri, null, null);
        LogUtil.info(TAG, "The result size: " + resultSet.getRowCount());
        processResult(resultSet);
        resultSet.close();
    } catch (DataAbilityRemoteException e) {
        LogUtil.error(TAG, "Query system media failed.");
    } finally {
        helper.release();
    }
}

private void processResult(ResultSet resultSet) {
    while (resultSet.goToNextRow()) {
        String path = resultSet.getString(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.DATA));
        String title = resultSet.getString(resultSet.getColumnIndexForName(AVStorage.AVBaseColumns.TITLE));
        AVDescription bean =
                new AVDescription.Builder().setTitle(title).setIMediaUri(Uri.parse(path)).setMediaId(path).build();
        avElements.add(new AVElement(bean, AVElement.AVELEMENT_FLAG_PLAYABLE));
    }
    writeDefault();
}

```
- インターネットリンク

```java
private void writeDefault() {
    avElements.add(
            new AVElement(new AVDescription.Builder()
                    .setTitle("web_video")
                    .setIMediaUri(Uri.parse(WEB_VIDEO_PATH))
                    .setMediaId(WEB_VIDEO_PATH)
                    .build(),
            AVElement.AVELEMENT_FLAG_PLAYABLE));
    avElements.add(
            new AVElement(new AVDescription.Builder()
                    .setTitle("harmony_video_1")
                    .setIMediaUri(Uri.parse(HARMONY_VIDEO_PATH_1))
                    .setMediaId(HARMONY_VIDEO_PATH_1)
                    .build(),
                    AVElement.AVELEMENT_FLAG_PLAYABLE));
    avElements.add(
            new AVElement(new AVDescription.Builder()
                    .setTitle("harmony_video_2")
                    .setIMediaUri(Uri.parse(HARMONY_VIDEO_PATH_2))
                    .setMediaId(HARMONY_VIDEO_PATH_2)
                    .build(),
                    AVElement.AVELEMENT_FLAG_PLAYABLE));
}
```

**VideoPlayerPlugin**はビデオプレーヤーの動作を提供:
- 再生

```java
public synchronized void startPlay(AVElement avElement, Surface surface) {
    if (videoPlayer != null) {
        videoPlayer.stop();
        videoPlayer.release();
    }

    if (videoRunnable != null) {
        ThreadPoolManager.getInstance().cancel(videoRunnable);
    }

    videoPlayer = new Player(context);
    videoPlayer.setPlayerCallback(this);
    videoRunnable = () -> play(avElement, surface);
    ThreadPoolManager.getInstance().execute(videoRunnable);
}

private void play(AVElement avElement, Surface surface) {
    Source source = new Source(avElement.getAVDescription().getMediaUri().toString());
    videoPlayer.setSource(source);
    videoPlayer.setVideoSurface(surface);
    LogUtil.info(TAG, source.getUri());

    videoPlayer.prepare();
}

...
@Override
public void onPrepared() {
    LogUtil.info(TAG, "onPrepared");
    videoPlayer.play();
}
```
- 一時停止とリズム

```java
/**
 * start
 */
public synchronized void startPlay() {
    if (videoPlayer == null) {
        return;
    }
    videoPlayer.play();
    LogUtil.info(TAG, "start play");
}

/**
 * pause
 */
public synchronized void pausePlay() {
    if (videoPlayer == null) {
        return;
    }
    videoPlayer.pause();
    LogUtil.info(TAG, "pause play");
}
```

- シークとバック

```java
    /**
    * seek
    */
    public void seek() {
        if (videoPlayer == null) {
            return;
        }
        videoPlayer.rewindTo((videoPlayer.getCurrentTime() + REWIND_TIME) * MIL_TO_MICRO);
        LogUtil.info(TAG, "seek" + videoPlayer.getCurrentTime());
    }

    /**
    * back
    */
    public void back() {
        if (videoPlayer == null) {
            return;
        }
        videoPlayer.rewindTo((videoPlayer.getCurrentTime() - REWIND_TIME) * MIL_TO_MICRO);
        LogUtil.info(TAG, "seek" + videoPlayer.getCurrentTime());
    }
```

- ステータスを確認

```java
/**
 *  check is playing
 */
public synchronized boolean isPlaying() {
    if (videoPlayer == null) {
        return false;
    }
    return videoPlayer.isNowPlaying();
}
```
- コールバックを提供

```java
public interface MediaPlayerCallback {
    void onPlayBackComplete();
    void onBuffering(int percent);
}

...
@Override
public void onPlayBackComplete() {
    LogUtil.info(TAG, "onPlayBackComplete");
    if (this.callback != null) {
        this.callback.onPlayBackComplete();
    }
}

@Override
public void onBufferingChange(int percent) {
    LogUtil.info(TAG, "onBufferingChange" + percent);
    if (this.callback != null) {
        this.callback.onBuffering(percent);
    }
}
```

**MainAbilitySlice**クラスにデータをロードとビデオプレーヤープラグインを初期化

```java
private void initData() {
    videoPlayerPlugin = new VideoPlayerPlugin(this, new VideoPlayerPlugin.MediaPlayerCallback() {
        @Override
        public void onPlayBackComplete() {
            getUITaskDispatcher().asyncDispatch(() -> play(currentPosition + 1));
        }

        @Override
        public void onBuffering(int percent) {
            getUITaskDispatcher().asyncDispatch(() -> {
                if (percent == 100) {
                    progressBar.setVisibility(Component.HIDE);
                } else {
                    progressBar.setVisibility(Component.VISIBLE);
                    progressBar.setProgressValue(percent);
                }
            });
        }
    });
    VideoElementManager videoElementManager = new VideoElementManager(this);
    avElements = videoElementManager.getAvElements();
    currentPosition = 0;
    distributeNotificationPlugin = DistributeNotificationPlugin.getInstance();
    distributeNotificationPlugin.setEventListener(this);
    distributeNotificationPlugin.subscribeEvent();
}
```

### 通知プラグインの作成
このプラグインは、内部イベントをパブリッシュ、サブクライブ、サブクライブ解除を目的としています。
スマートウォッチからリクエストを受け取ったら、このプラグインを**MainAbilitySlice**というターゲットに渡します。
プラグインの機能は、以下のように表示されます。

- イベントをパブリッシュ

```java
/**
 * publish CommonEvent
 */
public void publishEvent(String event) {
    LogUtil.info(TAG, "publish CommonEvent begin");
    Intent intent = new Intent();
    intent.setAction(NOTIFICATION_ACTION);
    intent.setParam(NOTIFICATION_KEY, event);
    CommonEventData eventData = new CommonEventData(intent);
    try {
        CommonEventManager.publishCommonEvent(eventData);
        LogUtil.info(TAG, "the action of Intent is:" + NOTIFICATION_ACTION);
        if (eventListener != null) {
            eventListener.onEventPublish("CommonEvent Publish Success");
        }
    } catch (RemoteException e) {
        LogUtil.error(TAG, "CommonEvent publish Error!");
    }
}

```
- イベントをサブクライブ

```java
/**
 * CommonEvent Subscribe
 */
public void subscribeEvent() {
    LogUtil.info(TAG, "CommonEvent onSubscribe begin.");
    MatchingSkills skills = new MatchingSkills();
    skills.addEvent(NOTIFICATION_ACTION);
    skills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);

    CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(skills);
    commonEventSubscriber = new CommonEventSubscriber(subscribeInfo) {
        @Override
        public void onReceiveEvent(CommonEventData commonEventData) {
            LogUtil.info(TAG, "CommonEventData onReceiveEvent begin");
            if (commonEventData == null) {
                LogUtil.info(TAG, "commonEventData is null.");
                return;
            }
            Intent intent = commonEventData.getIntent();
            if (intent == null) {
                LogUtil.debug(TAG, "commonEventData getIntent is null.");
                return;
            }
            String receivedAction = intent.getAction();
            LogUtil.info(TAG, "onReceiveEvent action:" + receivedAction);
            if (receivedAction.equals(NOTIFICATION_ACTION)) {
                String notificationContent = intent.getStringParam(NOTIFICATION_KEY);
                if (eventListener != null) {
                    eventListener.onEventReceive(notificationContent);
                }
            }
        }
    };

    LogUtil.info(TAG, "CommonEventManager subscribeCommonEvent begin.");
    try {
        CommonEventManager.subscribeCommonEvent(commonEventSubscriber);
        if (eventListener != null) {
            eventListener.onEventSubscribe("CommonEvent Subscribe Success");
        }
    } catch (RemoteException exception) {
        LogUtil.error(TAG, "CommonEvent Subscribe Error!");
    }
}
```
- イベントをサブクライブ解除

```java
/**
 * CommonEvent Unsubscribe
 */
public void unsubscribeEvent() {
    LogUtil.info(TAG, "CommonEvent onUnsubscribe begin.");
    if (commonEventSubscriber == null) {
        LogUtil.info(TAG, "CommonEvent onUnsubscribe commonEventSubscriber is null");
        return;
    }
    try {
        CommonEventManager.unsubscribeCommonEvent(commonEventSubscriber);
        if (eventListener != null) {
            eventListener.onEventUnsubscribe("CommonEvent Unsubscribe Success");
        }
    } catch (RemoteException exception) {
        LogUtil.error(TAG, "CommonEvent Unsubscribe Error!");
    }
    commonEventSubscriber = null;
}
```

-　イベントリスナーを提供

```java
public interface DistributeNotificationEventListener {
    void onEventPublish(String result);

    void onEventSubscribe(String result);

    void onEventUnsubscribe(String result);

    void onEventReceive(String result);
}

public void setEventListener(DistributeNotificationEventListener eventListener) {
    this.eventListener = eventListener;
}
```

### プレーヤーリモートを作成
スマートウォッチとタブレット間で接続が確立されると、サービスアビリティー上にプレイヤーリモートが作成され、リクエストを処理します。

```java
public class VideoPlayerServiceAbility extends Ability {
    private final PlayerRemote remote = new PlayerRemote(this);

    @Override
    public IRemoteObject onConnect(Intent intent) {
        super.onConnect(intent);
        return remote.asObject();
    }
}
```

**START**リクエストの場合は、ビデオプレーヤーを起動します。
それ以外の場合は、**DistributionNotificationPlugin**を使用してイベントをパブリッシュし、ビデオプレーヤーで処理できるようにします。

```java
public class PlayerRemote extends RemoteObject implements IRemoteBroker {
    static final int REMOTE_COMMAND = 0;
    private final Ability ability;
    private final DistributeNotificationPlugin distributeNotificationPlugin;

    public PlayerRemote(Ability ability) {
        super("Player Remote");
        this.ability = ability;
        distributeNotificationPlugin = DistributeNotificationPlugin.getInstance();
    }

    @Override
    public IRemoteObject asObject() {
        return this;
    }

    @Override
    public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
        if (code == REMOTE_COMMAND) {
            String command = data.readString();
            if (Const.START.equals(command)) {
                startVideoPlayer();
            } else {
                distributeNotificationPlugin.publishEvent(command);
            }
            return true;
        }
        return false;
    }

    private void startVideoPlayer() {
        Intent intent = new Intent();
        Operation operation =
                new Intent.OperationBuilder()
                        .withBundleName(ability.getBundleName())
                        .withAbilityName(MainAbility.class.getName())
                        .withAction(Const.VIDEO_PLAY_ACTION)
                        .build();
        intent.setOperation(operation);
        ability.startAbility(intent);
    }
}
```

###ビデオ プレーヤーで受信したイベントを処理
受信したイベントを処理するには、次の手順でビデオプレーヤークラス(**MainAbilitySlice**)を設定する必要があります。

- イベントをサブクライブ

```java
distributeNotificationPlugin = DistributeNotificationPlugin.getInstance();
distributeNotificationPlugin.setEventListener(this);
distributeNotificationPlugin.subscribeEvent();
```
- **onEventReceive**メソッドを実装してイベントを処理

```java
@Override
public void onEventReceive(String result) {
    LogUtil.info(TAG, result);
    switch (result) {
        case Const.PLAY:
            play();
            break;
        case Const.PAUSE:
            pause();
            break;
        case Const.FORWARD:
            play(currentPosition + 1);
            break;
        case Const.REWIND:
            play(currentPosition - 1);
            break;
        case Const.NEXT:
            videoPlayerPlugin.seek();
            break;
        case Const.PREVIOUS:
            videoPlayerPlugin.back();
            break;
        default:
            break;
    }
}

private void play(int position) {
    progressBar.setVisibility(Component.HIDE);
    int maxPosition = avElements.size() - 1;
    if (position > maxPosition) {
        position = 0;
    } else if (position < 0) {
        position = maxPosition;
    }
    currentPosition = position;
    AVElement item = avElements.get(position);
    String itemText = item.getAVDescription().getTitle().toString();
    title.setText(itemText);
    playButton.setPixelMap(ResourceTable.Media_pause_button);
    videoPlayerPlugin.startPlay(avElements.get(position), surface);
}

private void play() {
    videoPlayerPlugin.startPlay();
    playButton.setPixelMap(ResourceTable.Media_pause_button);
}

private void pause() {
    videoPlayerPlugin.pausePlay();
    playButton.setPixelMap(ResourceTable.Media_play_button);
}
```

# デモの結果
## デモ環境を設置

- タブレットを PC に接続し、タブレット モジュールをデプロイ
- スマートウォッチ(Galileoバージョン)をPCに接続し、スマートウォッチモジュールをデプロイ
タブレットでマルチデバイスコラボレーション設定を有効にする
- Huaweiヘルスケアアプリを使用してスマートウォッチとタブレット間のBlueTooth接続を設定
- タブレットでマルチデバイスコラボレーション設定を有効にする
![](https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/043/403/889/5190081000043403889.20210507023005.44866996535240460001394489468420:50520902105404:2800:387B429B378931D2E564EDD0513A3B56D3C4E122B0EDF7043FB3F97D9CF1069D.png)
- Huaweiヘルスケアアプリを使用してスマートウォッチとタブレット間のBlueTooth接続を設定
- 両方の端末を**同じWIFIネットワーク**に接続
- 両方の端末で**同じHuawei ID**でログイン

##結果
<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://twitter.com/hashtag/HarmonyOS?src=hash&amp;ref_src=twsrc%5Etfw">#HarmonyOS</a> <a href="https://twitter.com/hashtag/SuperDevice?src=hash&amp;ref_src=twsrc%5Etfw">#SuperDevice</a> <a href="https://twitter.com/hashtag/IoT?src=hash&amp;ref_src=twsrc%5Etfw">#IoT</a> <a href="https://t.co/iZ6kTdzEzu">pic.twitter.com/iZ6kTdzEzu</a></p>&mdash; huy tran (@knighthedspi) <a href="https://twitter.com/knighthedspi/status/1433670076301905923?ref_src=twsrc%5Etfw">September 3, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

スマートウォッチを使用して、上記の操作と同じようにタブレットのビデオプレーヤーを制御できます ☝!
[Github のリポジトリ](https://github.com/APAC-DTSE-Projects/HarmonyOS-SuperDeviceDemo)も添付しました!自由にクローンしてカスタマイズして、独自のデモを構築してください。