すべてのプロダクト
Search
ドキュメントセンター

ApsaraVideo VOD:ストラテジーシステム

最終更新日:Sep 10, 2026

本トピックでは、 AliPlayerKit Strategy System の設計背景、コアコンセプト、組み込みストラテジー、使用方法について説明します。

背景情報

ストラテジーシステムは、大規模な AUI Kit プロジェクトにおけるビジネスのカスタマイズとフレームワークの進化の間の対立を解決するために設計されました。

ローコードソリューションとして、AUI Kit はさまざまなビジネス向けに UI とコアプロセスのカスタマイズをサポートする必要があります。ビジネスロジックがフレームワークのコードに直接組み込まれていると、密結合が生じます。これにより、コードの競合、フレームワークのアップグレード時におけるビジネスロジックの破損、バージョンイテレーションの妨げといった問題が発生する可能性があります。

この種のロジックはビジネス要件であり、フレームワークのコア機能ではありません。これを処理するための適切なメカニズムがない場合、ビジネス固有のロジックがフレームワークに紛れ込む可能性があります。これは、システムの安定性と進化能力を脅かします。

ストラテジーシステムは、この問題を解決します:

  • ビジネス固有のロジックを、独立して拡張可能なストラテジーユニットとして抽象化します。

  • ビジネスロジックをフレームワークのコアからデカップリングします。

  • ビジネスは、フレームワークを変更することなく、ストラテジーレイヤーでシステムを拡張できます。

このメカニズムにより、ビジネス機能は体系的で再利用可能な基盤となります。これは、AUI Kit の長期的な進化をサポートする、反復、再利用、拡張が可能な基盤を構築します。

コンセプト

ストラテジーシステムとは

ストラテジーシステム は、ストラテジーコンポーネントの管理を統合するアーキテクチャメカニズムです。ストラテジーの登録メカニズム、ライフサイクル管理、コンテキストの注入、イベントサブスクリプションを担います。また、プレーヤーの実行中にストラテジーの作成、開始、一時停止、リセット、破棄も管理します。
このメカニズムにより、開発者は必要に応じてストラテジーを有効化または無効化できます。また、カスタムポリシーを迅速に実装したり、デフォルトのストラテジーを完全に置き換えたりして、ビジネスのカスタマイズ要件に対応できます。

ストラテジーとは

ストラテジー は、プレーヤーのビジネスロジックをカプセル化したユニットです。初回フレームまでの時間 (TTFF) の統計、コマ落ち検出、トラフィック保護、レジューム再生など、特定のビジネス機能またはカスタムロジックを実装します。
コア機能:

  • 単一責任:独立した機能目標に集中します。

  • イベント駆動:イベントバスを介してプレーヤーの状態に応答します。

  • 独立かつ分離:ストラテジー同士が干渉しません。

プラガブルかつコンポーザブルな設計により、コアではない機能をストラテジーユニットとして抽象化します。これにより、プレーヤーコアをシンプルに保ちながら、柔軟な拡張性を提供します。

機能

解決する課題

戦略システムは、以下の課題を解決します。

  1. メンテナンスの困難さ: 分散したビジネスロジックはメンテナンスと再利用が困難です。

  2. 設定の欠如: 異なるシナリオでは異なるロジックの組み合わせが必要ですが、柔軟な設定メカニズムがありません。

  3. ソースコードの変更: カスタムロジックにはフレームワークのソースコードの変更が必要です。

  4. インスタンスの干渉: マルチプレーヤーシナリオでは、インスタンス間のビジネスロジックが相互に干渉する可能性があります。

コアバリュー

戦略システムは、プレーヤーのモニタリングおよび分析ロジックを抽象化し、カプセル化します。これにより、お客様は必要に応じて戦略を設定し、組み合わせることができます。

使用方法

説明

メリット

デフォルト戦略の使用

プレーヤーコンポーネントは公式のデフォルト戦略を使用します。

統合プロセスを簡素化します。すぐに使えます。

カスタム戦略

特定のビジネス要件に合わせた戦略を実装します。

多様なカスタマイズのニーズに対応します。

戦略を使用しない

再生機能のみを使用します。

ロジックに依存しない、純粋な再生シナリオに適しています。

アーキテクチャ上のメリット:

  • デカップリング: 戦略はプレーヤーのコアロジックから分離されているため、責任分担が明確になります。

  • 再利用性: 戦略は異なるプレーヤーインスタンス間で再利用できます。

  • 拡張性: フレームワークを変更することなく戦略をカスタマイズできます。

  • アイソレーション: 戦略は独立しており、相互に干渉しません。

コア機能

機能

説明

イベント駆動

戦略はイベントバスを通じてプレーヤーイベントをサブスクライブし、イベントに応答して実行されます。

ライフサイクル管理

戦略の開始、停止、リセット、破棄を管理します。

コンテキストインジェクション

戦略はコンテキストを通じてプレーヤーのステータスとデータにアクセスできます。

ファクトリーパターン

戦略ファクトリーのグローバル登録をサポートし、すべてのプレーヤーインスタンスに自動的に適用されます。

組み込みストラテジー

AliPlayerKit には、すぐに使える 3 つの組み込みストラテジーが用意されています。

初回フレーム表示時間 (TTFF) ストラテジー (FirstFrameStrategy)

このストラテジーは、プレーヤーが PREPARING ステージから初回フレームのレンダリングに至るまでに要する時間を算出します。これは 3 つのフェーズに分かれます。

フェーズ

説明

Preparing

PREPARING → Prepared

Rendering

Prepared → FirstFrameRendered

Total time

PREPARING → FirstFrameRendered

シナリオ:パフォーマンスモニタリング、ユーザーエクスペリエンスの最適化、およびプレーヤーのチューニング。

カクつき検出ストラテジー (StutterDetectStrategy)

このストラテジーは、再生中のカクつきをモニタリングし、カクつきの統計を収集します。

メトリック

説明

カクつき回数

カクつきが発生した回数。

カクつき時間

各カクつきイベントの継続時間。

合計カクつき時間

再生セッション全体におけるカクつきの合計継続時間。

有効再生時間

一時停止とカクつきを除いた、実際の視聴時間。

シナリオ:再生品質のモニタリングとユーザーエクスペリエンス分析。

トラフィック保護ストラテジー (TrafficProtectionStrategy)

このストラテジーはネットワークステータスの変化をリッスンし、特定のシナリオでユーザーに通知します。

シナリオ

動作

Wi-Fi からモバイルネットワークへの切り替え

再生にモバイルデータが使用されていることをユーザーに通知します。

モバイルネットワークでの再生開始

モバイルデータが使用されていることをユーザーに通知します。

シナリオ:データ使用量に注意が必要なシナリオと、ユーザーフレンドリーな通知。

基本的な使用方法

ストラテジーシステムでは、ストラテジーを使用する方法が 3 つあります。ニーズに最適なものを選択してください。

方法

説明

シナリオ

デフォルトストラテジーの使用

最もシンプルな方法です。プレーヤーコンポーネントが自動的にデフォルトストラテジーを登録します。

迅速な統合と標準的な再生シナリオ。

一部のストラテジーのカスタマイズ

特定のストラテジーのみを登録し、それ以外は使用しません。

部分的なカスタマイズと必要に応じたストラテジーの有効化。

ストラテジーの完全なカスタマイズ

すべてのストラテジーをカスタマイズして、完全にカスタマイズされたモニタリングシステムを作成します。

詳細なカスタマイズと特定のビジネス要件。

方法 1: 組み込みストラテジーの使用

これは最もシンプルな方法です。プレーヤーコンポーネントが自動的にデフォルトストラテジーを登録します。

// 1. プレーヤーコントローラーを作成します。デフォルトのストラテジーが自動的に登録されます。
AliPlayerController controller = new AliPlayerController(this);

// 2. 再生データを準備します。
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();

// 3. データモデルを設定します。
controller.configure(model);

// 4. コントローラーをビューにアタッチします。
playerView.attach(controller);

デフォルトで登録されるストラテジーには、以下が含まれます:

  • FirstFrameStrategy (TTFF)

  • StutterDetectStrategy (コマ落ち検出)

  • TrafficProtectionStrategy (トラフィック保護)

方法 2: ストラテジーのカスタマイズ

必要なストラテジーのみを登録し、残りを無効にします。

// 1. プレーヤーコントローラーを作成します。
AliPlayerController controller = new AliPlayerController(this);

// 2. ストラテジーマネージャーを取得します。
StrategyManager strategyManager = controller.getStrategyManager();

// 3. 必要なストラテジーのみを登録します。
strategyManager.register(new FirstFrameStrategy());
strategyManager.register(new StutterDetectStrategy());

// 4. 再生データを準備します。
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();
controller.configure(model);
playerView.attach(controller);

方法 3: ストラテジーの完全なカスタマイズ

カスタムストラテジーを実装して、完全にカスタマイズされたモニタリングシステムを作成します。

// 1. プレーヤーコントローラーを作成します。
AliPlayerController controller = new AliPlayerController(this);

// 2. ストラテジーマネージャーを取得します。
StrategyManager strategyManager = controller.getStrategyManager();

// 3. カスタムストラテジーを登録します。
strategyManager.register(new MyCustomStrategy());
strategyManager.register(new MyAnalyticsStrategy());

// 4. 再生データを準備します。
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();
controller.configure(model);
playerView.attach(controller);

高度な使用方法

カスタムストラテジーの実装方法

AliPlayerKit では、2 つの実装方法を提供しています。要件に応じていずれかを選択してください:

方法

機能

シナリオ

BaseStrategy を継承

コード量が少なく、フレームワークがイベントサブスクリプションを管理します。

ほとんどのモニタリングストラテジー。

IStrategy インターフェイスを実装する

柔軟性が高く、完全な自律制御が可能です。

特殊な制御ロジックが必要な場合。

方法 1: BaseStrategy の継承 (推奨)

BaseStrategy を継承する方法が最も簡単です。フレームワークがイベントサブスクリプションとライフサイクル管理をすでにカプセル化しているためです。

シナリオ: 統計、検出、ログ記録など、ほとんどのモニタリングおよび分析ストラテジー。

  1. ストラテジークラスを作成します。

    BaseStrategy を継承し、必要なメソッドを実装します:

    public class MyAnalyticsStrategy extends BaseStrategy {
    
        private static final String TAG = "MyAnalyticsStrategy";
    
        @NonNull
        @Override
        public String getName() {
            return TAG;  // ログ記録とデバッグのためにストラテジー名を返します。
        }
    
        @Nullable
        @Override
        protected List<Class<? extends PlayerEvent>> observedEvents() {
            // サブスクライブするイベントタイプを宣言します。
            return Arrays.asList(
                PlayerEvents.StateChanged.class,
                PlayerEvents.Prepared.class,
                PlayerEvents.Info.class
            );
        }
    
        @Override
        public void onEvent(@NonNull PlayerEvent event) {
            // 他のプレーヤーからのイベントを除外します。
            if (!isCurrentPlayer(event)) return;
    
            // イベントを処理します。
            if (event instanceof PlayerEvents.StateChanged) {
                handleStateChanged((PlayerEvents.StateChanged) event);
            } else if (event instanceof PlayerEvents.Prepared) {
                handlePrepared((PlayerEvents.Prepared) event);
            } else if (event instanceof PlayerEvents.Info) {
                handleInfo((PlayerEvents.Info) event);
            }
        }
    
        @Override
        public void onReset() {
            super.onReset();
            // 新しい動画に備えて内部状態をリセットします。
        }
    
        private void handleStateChanged(PlayerEvents.StateChanged event) {
            // 状態の変更を処理します。
        }
    
        private void handlePrepared(PlayerEvents.Prepared event) {
            // 準備完了を処理します。
        }
    
        private void handleInfo(PlayerEvents.Info event) {
            // 再生情報の更新を処理します。
        }
    }
  2. 登録します。

    StrategyManager strategyManager = controller.getStrategyManager();
    strategyManager.register(new MyAnalyticsStrategy());

例: 詳細については、「 strategies/FirstFrameStrategy.java 」をご参照ください。

方法 2: IStrategy インターフェイスの実装

IStrategy インターフェイスを直接実装すると、より高い柔軟性を得られますが、イベントサブスクリプションは自分で処理する必要があります。

シナリオ: ストラテジーの動作を完全に制御する必要がある場合、または特殊な初期化ロジックが必要な場合。

  1. ストラテジークラスを作成します。

    IStrategy インターフェイスを実装し、イベントサブスクリプションを自分で管理します:

    public class MyCustomStrategy implements IStrategy, PlayerEventBus.EventListener<PlayerEvent> {
    
        private static final String TAG = "MyCustomStrategy";
    
        private StrategyContext mContext;
    
        @NonNull
        @Override
        public String getName() {
            return TAG;
        }
    
        @Override
        public void onStart(@NonNull StrategyContext context) {
            mContext = context;
            // 手動でイベントをサブスクライブします。
            PlayerEventBus.getInstance().subscribe(PlayerEvents.StateChanged.class, this);
        }
    
        @Override
        public void onStop() {
            // 手動でイベントをアンサブスクライブします。
            PlayerEventBus.getInstance().unsubscribe(PlayerEvents.StateChanged.class, this);
            mContext = null;
        }
    
        @Override
        public void onReset() {
            // 内部状態をリセットします。
        }
    
        @Override
        public void onEvent(@NonNull PlayerEvent event) {
            // イベントを処理します。
        }
    }
  2. 登録します。

    StrategyManager strategyManager = controller.getStrategyManager();
    strategyManager.register(new MyCustomStrategy());

コールバック付きストラテジーの実装方法

ストラテジーはコールバックインターフェイスを使用して、結果をビジネス層に通知できます:

public class MyAnalyticsStrategy extends BaseStrategy {

    public interface Callback {
        void onAnalysisComplete(AnalysisResult result);
    }

    @Nullable
    private final Callback mCallback;

    // 引数なしコンストラクターをサポートします。
    public MyAnalyticsStrategy() {
        this(null);
    }

    // コールバックの受け渡しをサポートします。
    public MyAnalyticsStrategy(@Nullable Callback callback) {
        mCallback = callback;
    }

    @Override
    public void onEvent(@NonNull PlayerEvent event) {
        // ... ビジネスロジック

        // コールバックを通じて結果を通知します。
        if (mCallback != null) {
            mCallback.onAnalysisComplete(result);
        }
    }
}

使用方法:

strategyManager.register(new MyAnalyticsStrategy(result -> {
    // 分析結果を処理します。
    Log.d("Analytics", "Result: " + result);
}));

例: 詳細については、「 strategies/FirstFrameStrategy.java 」をご参照ください。

プレーヤー状態へのアクセス方法

ストラテジーは StrategyContext を通じて、プレーヤーの状態とデータにアクセスできます:

@Override
public void onEvent(@NonNull PlayerEvent event) {
    if (!isCurrentPlayer(event)) return;

    // プレーヤー ID を取得します。
    String playerId = getPlayerId();

    // 再生データモデルを取得します。
    AliPlayerModel model = mContext.getModel();
    if (model != null) {
        String title = model.getVideoTitle();
        VideoSource source = model.getVideoSource();
    }

    // プレーヤーの状態ストアを取得します。
    IPlayerStateStore stateStore = mContext.getPlayerStateStore();
    long position = stateStore.getCurrentPosition();
    long duration = stateStore.getDuration();
    PlayerState state = stateStore.getPlayState();
}
重要

mContext はコンストラクター内では null です。 onStart() が呼び出された後にのみ初期化されます。

グローバルストラテジーの登録方法

StrategyRegistry を使用してグローバルストラテジーを登録できます。登録したストラテジーは、新しく作成されたすべてのプレーヤーインスタンスに自動的に適用されます:

// グローバルストラテジーを登録します。
StrategyRegistry.addGlobalStrategy(() -> new MyAnalyticsStrategy());

// 以降に作成されるすべての AliPlayerController インスタンスに、このストラテジーが自動的に登録されます。
AliPlayerController controller = new AliPlayerController(this);   

デフォルト設定を復元する:

StrategyRegistry.resetToDefault();

ベストプラクティス

ライフサイクル管理

public class MyStrategy extends BaseStrategy {

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        super.onStart(context);  // 必ず呼び出す必要があります。
        // リソースを初期化します。
    }

    @Overridepublic void onStop() {
        // リソースをクリーンアップします。
        super.onStop();  // 必ず呼び出す必要があります。
    }

    @Overridepublic void onReset() {
        super.onReset();
        // 新しいビデオに備えて内部状態をリセットします。
    }
}

イベントフィルタリング

マルチプレイヤーシナリオでは、常にイベントソースをフィルタリングしてください。

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // 他のプレーヤーからのイベントをフィルタリングして、干渉を回避します。
    if (!isCurrentPlayer(event)) return;

    // 現在のプレーヤーのイベントを処理します。
}

リソースのクリーンアップ

ストラテジーがハンドラや Runnable などの非同期リソースを持つ場合は、onStop() でクリーンアップしてください。

public class MyStrategy extends BaseStrategy {

    private final Handler mHandler = new Handler();
    private Runnable mRunnable;

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        super.onStart(context);
        mRunnable = () -> doSomething();
        mHandler.postDelayed(mRunnable, 1000);
    }

    @Overridepublic void onStop() {
        // メモリリークを回避するため、非同期タスクをクリーンアップします。
        mHandler.removeCallbacks(mRunnable);
        super.onStop();
    }
}

注意事項

シナリオ

推奨プラクティス

理由

マルチプレイヤーシナリオ

isCurrentPlayer() でイベントをフィルタリングします。

イベントの干渉を回避します。

長時間実行タスク

onStop() でタスクをキャンセルします。

メモリリークを回避します。

ビデオ切り替え

onReset() で状態をリセットします。

状態の正しさを確保します。

コンテキストへのアクセス

mContext.getModel() で取得します。

コンテキストの可用性を確保します。

サンプル

このプロジェクトには、playerkit-examples/example-strategy-system に完全なサンプルが用意されています。

サンプルの機能

機能

説明

再生再開ストラテジー

再生進捗を自動的に記録し、次回はその時点から再生を再開します。

ストラテジー登録のデモ

カスタムストラテジーの登録方法を示します。

サンプルの実行

デモアプリで、[Strategy System] サンプルを選択すると、動作を確認できます。

API リファレンス

コアインターフェイス

インターフェイス/クラス

説明

IStrategy

ストラテジーのインターフェイス。ライフサイクルメソッドを定義します。

BaseStrategy

ストラテジーの基底クラス。イベントサブスクリプション管理をカプセル化します。

StrategyManager

ストラテジーマネージャー。ストラテジーのライフサイクルを管理します。

StrategyContext

ストラテジーのコンテキスト。読み取り専用のプレーヤー情報を提供します。

StrategyRegistry

ストラテジーのサービスレジストリ。グローバルストラテジーを管理します。

BaseStrategy メソッド

メソッド

説明

getName()

ロギングとデバッグ用にストラテジー名を返します。

observedEvents()

サブスクライブするイベントタイプのリストを返します。

onEvent(event)

イベントコールバック。サブスクライブしたイベントを処理します。

isCurrentPlayer(event)

イベントが現在のプレーヤーからのものかを確認します。

getPlayerId()

現在のプレーヤー ID を取得します。

StrategyManager メソッド

メソッド

説明

register(strategy)

ストラテジーを登録します。

unregister(strategy)

ストラテジーを登録解除します。

getStrategy(name)

名前でストラテジーを取得します。

start(context)

すべてのストラテジーを開始します。

stop()

すべてのストラテジーを停止します。

reset()

すべてのストラテジーをリセットします。

destroy()

マネージャーを破棄します。

StrategyContext メソッド

メソッド

説明

getPlayerId()

プレーヤー ID を取得します。

getModel()

再生データモデルを取得します。

getPlayerStateStore()

プレーヤーの状態ストア (読み取り専用) を取得します。

技術原理

全体アーキテクチャ

ストラテジーシステムは、イベント駆動とマネージャーによるスケジュールのアーキテクチャパターンを採用しています。

設計上のポイント:

  • ストラテジーはプレーヤー参照を保持しない:イベント駆動設計により、完全なデカップリングを実現します。

  • ストラテジーはイベントにのみ注力する:サブスクライブする対象と、その処理方法を宣言するだけで済みます。

  • マネージャーがスケジューリングを処理する:マネージャーがライフサイクルとイベントディスパッチを制御します。

ライフサイクル

ストラテジーは 3 段階のライフサイクルを使用します:

ライフサイクル

トリガー条件

説明

onStart(context)

playerView.attach()

ストラテジーが開始され、イベントをサブスクライブし、リソースを初期化します。

onReset()

再生コンテンツの切り替え

新しいビデオに備えて内部状態をリセットします。

onStop()

playerView.detach() または破棄

ストラテジーが停止し、サブスクリプションを解除し、リソースをクリーンアップします。

イベント駆動メカニズム

ストラテジーは、パブリッシュ/サブスクライブ パターンを使用し、イベントバスを介してプレーヤーイベントをサブスクライブします。

イベントサブスクリプションのフロー:

  1. ストラテジーは、observedEvents() でサブスクライブする必要があるイベントタイプを宣言します。

  2. BaseStrategy は、onStart() でこれらのイベントを自動的にサブスクライブします。

  3. BaseStrategy は、onStop() で自動的にサブスクリプションを解除します。

  4. ストラテジーは、onEvent() で受信したイベントを処理します。

設計思想:イベント駆動メカニズムにより、ストラテジーはプレーヤーから完全にデカップリングされます。ストラテジーはプレーヤー参照を保持する必要がありません。イベントサブスクリプションと処理ロジックにのみ注力すればよく、関心の分離を実現します。

複数プレーヤーの分離

複数プレーヤーのシナリオでは、ストラテジーシステムは次のメカニズムにより分離を保証します:

  1. 独立した StrategyManager:各 AliPlayerController は、それぞれ独立した StrategyManager を持ちます。

  2. 独立した StrategyContext:各ストラテジーコンテキストは、特定のプレーヤー ID にバインドされます。

  3. イベントフィルタリング:ストラテジーは isCurrentPlayer(event) を使用してイベントソースをフィルタリングします。

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // 各プレーヤーインスタンスからのイベントには playerId が含まれます。
    // isCurrentPlayer() は、イベントが現在のプレーヤーからのものかどうかを確認します。
    if (!isCurrentPlayer(event)) return;

    // 現在のプレーヤーからのイベントのみを処理します。
}

よくある質問

ストラテジーはいつ開始されますか?

playerView.attach() を呼び出すと、登録済みのすべてのストラテジーが自動的に開始されます。

プレーヤーの現在の状態を取得するにはどうすればよいですか?

StrategyContext の getPlayerStateStore() メソッドを使用します:

IPlayerStateStore stateStore = mContext.getPlayerStateStore();
PlayerState state = stateStore.getPlayState();
long position = stateStore.getCurrentPosition();

ストラテジーをデバッグするにはどうすればよいですか?

LogHub を使用してログを確認します。TAG の形式は Policy Name.BaseStrategy または StrategyManager です。

クラッシュの原因となるよくある間違い

以下は、クラッシュの原因としてお客様からよく報告される問題です。これらの誤りを避けてください。

間違い 1:コンストラクターで mContext にアクセスする

誤ったコード:

public class MyStrategy extends BaseStrategy {

    public MyStrategy() {
        super();
        // コンストラクター内では mContext は null です。
        String playerId = getPlayerId();  // 空文字列を返します。
        AliPlayerModel model = mContext.getModel();  // NullPointerException!
    }
}

正しいコード:

@Overridepublic void onStart(@NonNull StrategyContext context) {
    super.onStart(context);  // ✅ 最初に呼び出す必要があります。
    // onStart の後に mContext にアクセスできます。
    String playerId = getPlayerId();  // 通常どおり ID を取得します。
    AliPlayerModel model = mContext.getModel();  // 通常どおりモデルを取得します。
}

クラッシュの理由:mContext は onStart() が呼び出された後に代入されます。コンストラクターの実行時点では、ストラテジーはまだ開始されていません。

間違い 2:super.onStart() の呼び出し忘れによりイベントサブスクリプションが失敗する

誤ったコード:

public class MyStrategy extends BaseStrategy {

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        // super.onStart(context) の呼び出しを忘れています。
        // mContext は null になり、イベントサブスクリプションも実行されません。
        initMyResources();
    }

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        // イベントサブスクリプションが実行されていないため、このメソッドは呼び出されません。
    }
}

正しいコード:

@Overridepublic void onStart(@NonNull StrategyContext context) {
    super.onStart(context);  // ✅ 最初に呼び出す必要があります。
    initMyResources();
}

クラッシュの理由:super.onStart(context) は mContext を初期化し、observedEvents() が返すイベントをサブスクライブします。これを呼び出さない場合、mContext が null のままとなり、イベントサブスクリプションも無効になります。

間違い 3:イベントをフィルタリングしないとマルチプレイヤーのシナリオで干渉が発生する

誤ったコード:

public class MyStrategy extends BaseStrategy {

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
            // イベントソースがフィルタリングされていないため、マルチプレイヤーのシナリオでクロストークが発生します。
            if (event instanceof PlayerEvents.StateChanged) {
            // すべてのプレーヤーの状態変更が、このロジックをトリガーします。
            updateUI();  // 誤ったプレーヤーの UI を更新する可能性があります。
        }
    }
}

正しいコード:

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // まず、現在のプレーヤー以外のイベントを除外します。
    if (!isCurrentPlayer(event)) return;

    if (event instanceof PlayerEvents.StateChanged) {
        updateUI();  // 現在のプレーヤーからのイベントのみを処理します。
    }
}

問題の原因:マルチプレイヤーのシナリオでは、すべてのプレーヤーからのイベントがすべてのポリシーにディスパッチされます。フィルタリングしない場合、ポリシーが他のプレーヤーからのイベントを処理してしまいます。

間違い 4:onStop() で非同期タスクをクリーンアップしないとメモリリークが発生する

誤ったコード:

public class MyStrategy extends BaseStrategy {

    private Handler handler = new Handler();

    @Overridepublic void onStart(@NonNull StrategyContext context) {
        super.onStart(context);
        handler.postDelayed(() -> updateProgress(), 1000);  // 遅延タスク。
    }

    @Overridepublic void onStop() {
        // 遅延タスクの削除を忘れています。
        super.onStop();
    }
}

正しいコード:

@Overridepublic void onStop() {
    handler.removeCallbacksAndMessages(null);  // すべての遅延タスクをクリアします
    super.onStop();
}

クラッシュの原因:遅延タスクがポリシーインスタンスへの参照を保持します。ポリシー停止後もインスタンスがガベージコレクションされず、メモリリークが発生します。タスク実行時に、すでに破棄されたリソースへアクセスする可能性があります。

間違い 5:observedEvents() が null を返すことによるイベントサブスクリプションの失敗

誤ったコード:

public class MyStrategy extends BaseStrategy {

    @Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
        // null を返してもクラッシュはしませんが、イベントサブスクリプションは実行されません。
        return null;
    }

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        // このメソッドは呼び出されません。
    }
}

正しいコード:

@Nullable@Overrideprotected List<Class<? extends PlayerEvent>> observedEvents() {
    return Arrays.asList(
        PlayerEvents.StateChanged.class,  // サブスクライブするイベントを宣言します
        PlayerEvents.Prepared.class
    );
}

@Overridepublic void onEvent(@NonNull PlayerEvent event) {
    // これでイベントを受信できます。
}

問題の理由:BaseStrategy はイベントのサブスクライブ時に observedEvents() の戻り値を確認します。戻り値が null または空のリストの場合、いずれのイベントもサブスクライブしません。

間違い 6:onReset() で内部状態をリセットしないとデータが不正になる

誤ったコード:

public class MyStrategy extends BaseStrategy {

    private int mPlayCount = 0;

    @Overridepublic void onEvent(@NonNull PlayerEvent event) {
        if (!isCurrentPlayer(event)) return;

        if (event instanceof PlayerEvents.Prepared) {
            mPlayCount++;  // ビデオ切り替え時にカウントがリセットされないため、蓄積します。
            Log.d(TAG, "Play count: " + mPlayCount);
        }
    }

    @Overridepublic void onReset() {
        super.onReset();
        // mPlayCount のリセットを忘れています。
    }
}

正しいコード:

@Overridepublic void onReset() {
    super.onReset();
    mPlayCount = 0;  // 内部状態をリセットします。
}

問題の原因:onReset() は、ビデオ切り替え時に呼び出され、ポリシーの内部状態をリセットします。状態をリセットしない場合、値が蓄積して統計の正確性に影響します。