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

Alibaba Cloud Model Studio:HappyOyster Android SDK 統合ガイド

最終更新日:Sep 25, 2026

HappyOyster Android SDK を統合することで、アプリは AI によって生成されたワールドにリアルタイムで入ることができ、アドベンチャー (ワールド探索)、ディレクティング (リアルタイム演出)、アクティング (ロールプレイ) の 3 つのモードでリアルタイムのインタラクティブビデオ体験を提供します。

Happy OysterはAIワールド探索プロダクトです。Happy Oyster Android SDKを統合することで、アプリはAIによってリアルタイムで生成された「ワールド」に入り、アドベンチャー、ディレクティング、またはアクティングでリアルタイムのインタラクティブビデオ体験を提供できます。

このドキュメントは、統合側のAndroid開発者向けです。インストール、認証、完全な統合フロー、イベント処理、およびベストプラクティスについて説明し、確実に統合する方法を解説しています。特定のAPI (シグネチャ、パラメータ、戻り値、エラーコード、データモデル) については、Happy Oyster Android SDK APIリファレンスを参照してください。

1. What You Can Do

  • 体験 (Travel) の開始: 一回限りの認証情報を使用して準備済みのワールドに入ります。SDKが自動的にリアルタイムビデオ接続を確立します。
  • リアルタイム再生: SDKはビデオViewを返し、それをレイアウトにマウントして、AIがリアルタイムで生成した映像を再生します。
  • Real-time interaction:
    • ディレクティングモード (directing): テキスト指示を送信してストーリーを進行させます。
    • アクティング: テキスト指示も送信します。一時停止/再開が利用可能です。巻き戻しは行わず、sendCommand は使用しないでください。参加レスポンスの aspectRatio を使用してプレイヤーの向きを設定します (§13 モード適応 を参照)。
    • アドベンチャーモード (adventure): 方向/視点/アクションの制御コマンドを送信してワールドと対話します。
  • プロセス制御: 一時停止 / 再開 (ディレクティングとアクティング)、巻き戻し (ディレクティングのみ)、終了 (3つのモードすべて)。
  • ステータスとエラーのコールバック: イベントリスナーを通じて、体験のステータスと例外をリアルタイムで感知します。

注記SDKはワールドの作成と管理を担当せず、低レベルのリアルタイム通信の詳細を直接公開することもありません。これらはサーバーまたはSDK内部で処理されます。「体験の開始 → 再生 → 対話 → 終了」にのみ集中する必要があります。

2. Installation

Happy Oyster SDK (cn.happyoyster:opensdk) は Maven Central で公開されています。その基盤となるリアルタイム通信エンジン (Alibaba Cloud ARTC) は Alibaba Cloud Maven で公開されています。両方のリポジトリを宣言する必要があります。

環境要件

項目

Requirement

minSdk

24 (Android 7.0) 以上

compileSdk

36

JDK

JDK 11 バイトコードターゲット (ホストツールチェーン: JDK 11 以上を推奨)

Language

Kotlin (コルーチン suspend API)

ABI

arm64-v8a / armeabi-v7a (リアルタイム通信エンジンにはネイティブライブラリが含まれます)

Network

パブリックインターネットアクセスが必要

プロジェクトルートの settings.gradle.kts にリポジトリを追加します。

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()                                       // Happy Oyster SDK (cn.happyoyster:opensdk)
        maven("https://maven.aliyun.com/repository/public")  // real-time communication engine (Alibaba Cloud ARTC)
    }
}

Maven Central で最新公開リリースを見つけ、以下の <version> をその正確なバージョン番号に置き換え、モジュールの build.gradle.kts に依存関係を追加します。

dependencies {
    implementation("cn.happyoyster:opensdk:<version>")
}

依存関係は正確なバージョンに固定してください。アップグレード前にリリースノートを確認し、アップグレード後にホストコードを再コンパイルしてください。

注記SDKは軽量なAARとして公開されており、サードパーティの依存関係は埋め込まれていません。リアルタイム通信エンジンなどの推移的依存関係は、解決時に上記のリポジトリから自動的にプルされるため、Alibaba Cloud Mavenリポジトリは不可欠です。これがないと com.aliyun.aio:AliVCSDK_ARTC の解決に失敗します。

Permissions

SDKライブラリ自体は INTERNET のみを宣言しています。リアルタイム通信エンジンは、ネットワーク/Bluetooth/オーディオ設定タイプの少数のパーミッションを自動的にマージします。ライブラリマニフェストにはマイクやカメラのパーミッションは含まれていません。ビデオストリームはサブスクライブ専用の再生であり、SDKはリアルなオーディオをリモート側に送信しません。

ホスト側で独自に宣言する必要があります: targetSdk が 33 以上の場合、ホストの AndroidManifest.xml で通知パーミッションを宣言する必要があります (リアルタイム通信エンジンにはフォアグラウンドサービスが含まれます)。

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

アドベンチャーモード(sendCommand)— マイク権限の宣言を推奨:アドベンチャーモードのリアルタイム制御コマンドは、ローカルの「パブリッシャーID」を確立するサイレントオーディオストリームによって運ばれるアップリンクに乗せられます。SDKは実際のオーディオを録音またはアップロードしません。RECORD_AUDIOはDataChannelの厳密な前提条件ではありません。これがなくても、サイレントストリームはパブリッシャーとして機能し、アップリンクは通常利用可能です。それでも、デバイス間での安定性のために、アプリがsendCommandを使用する場合は、ホストのAndroidManifest.xmlで権限を宣言し、呼び出し前にランタイムで要求することを推奨します。

<uses-permission android:name="android.permission.RECORD_AUDIO" />

エラーコード105004について:105004は、リアルタイムチャネルの準備ができていない、または送信が失敗したことを意味します(例:DataChannelの中断やリアルタイムチャネルの異常)。これは権限の欠如による必然的な結果ではありません。RECORD_AUDIOが許可されていないことによって直接トリガーされるものではなく、ディレクティングモード(sendInstruct)やビデオ再生には影響しません。アプリがアドベンチャーモードを使用しない場合、この権限は不要です。

マージされた権限をトリムするには、tools:node="remove" を使用します。

3. 認証モデル

SDKはトークンを取得または更新せず、軽量に保たれています。認証には2つのレイヤーがあります。

  1. Bailian (百炼) ゲートウェイ API Key: updateToken(token) を介してアプリにより Bearer トークンとして注入されます。SDK は最新のトークンのみを保持し、永続化や更新は行いません。Key が変更された後は再注入します。ゲートウェイは、startTravel を含むすべての SDK リクエストでこの Bearer を使用することを要求します。Bearer Key が期限切れまたは無効な場合、SDK は SDKError(101002) をスローします。この場合、新しいチケットと交換するのではなく、Bearer Key を再取得して再注入 (updateToken) する必要があります。開発 / デモ中は、フローを動作させるために、メインの Bailian (百炼) API Key を Bearer (updateToken) として直接使用できます。本番環境では、常にサーバーで発行された 短命トークン に切り替え、長命な API Key を配布アプリにバンドルしないでください。
  2. 1回限りの体験認証情報ticket:サーバーがオープンプラットフォームを通じて交換し、クライアントに配信します。単一のstartTravelに対してのみ使用されます。体験が終了すると(正常または異常を問わず)無効になり、再利用できません。チケットレベルの認証情報エラーは、6桁のサーバーコードで識別されます(例:401010 = チケットが無効または期限切れ、401011 = チケットは使用済み)。SDKはこれらのコードをそのまま通過させます。

サーバーは、Worldの作成/管理、Travelチケットとの交換、およびクライアントへの配信を担当します。Android SDKはトークンとチケットを消費するだけで、World管理インターフェースは提供しません。初期化時に、アカウントのBailian(百炼)のAPI HostをSDKConfig.apiHost(llm-xxxx.ap-southeast-1.maas.aliyuncs.comの形式、Bailian(百炼)コンソールのAPI Keyページの「API Host」フィールドからコピー)経由で渡し、必須でデフォルト値のないSDKConfig.model(happyoyster-1.0-directing / happyoyster-1.0-acting / happyoyster-1.0-adventure、Open APIエントリーと一致)を渡す必要があります。SDKはリクエストURLをhttps://{apiHost}/api/v2/apps/{model}/openapi/v1/{endpoint}として完成させるため、自分で組み立てる必要はありません。

SDKConfig の構築時に model を省略すると、コンパイル時エラーになります。model が空の場合、initialize は同期的に SDKError(100002) (raw = "SDKConfig.model must not be blank") をスローし、ランタイムの作成や置換を行わず、ネットワークリクエストも送信しません。API Host、モデル、および注入された API Key は、必要なアカウント、リージョン、およびモデルの認可と一致している必要があります。一致しない場合、ゲートウェイは通常 AccessDenied を返します (実行時には SDKError(106003) としてスローされ、元の AccessDenied ボディは SDKError.raw で利用できます。トラブルシューティングチェックリストについては、API リファレンスのエラーコード表の 106003 の行を参照してください)。セキュリティのため、長命な API Key をアプリにバンドルしたりコードリポジトリにコミットしたりするのではなく、クライアントが サーバーで発行された短命トークン を Bearer として注入することを強く推奨します。

注記リージョンコンプライアンスに関する通知: 適用される法律およびデータ要件への準拠をサポートするため、対象ユーザーに米国ユーザーが含まれる場合、それらのユーザーに提供されるサービスについて、SDK初期化時に米国リージョンのAPIホストを設定する必要があります。開発者は正しい設定を行う責任を負い、この要件に従わなかった場合、適用される法律の下で相応の責任を負います。

4. Quick Start

import cn.happyoyster.opensdk.*   // All entry-point classes live in this package: HappyOyster, SDKConfig, TravelStatusValue, ModeValue, SDKError, etc.

// Track the current experience status and this Travel's metadata; use them to decide whether interaction can be sent.
@Volatile private var currentStatus: TravelStatusValue? = null
@Volatile private var currentTravel: StartTravelData? = null

// 1) Initialize (recommended in Application.onCreate)
//    apiHost is required: your account's Bailian API Host (copied from the API Key page in the Bailian console)
//    model is required with no default: the complete model name and version; refer to the official Bailian HappyOyster model documentation for available values
HappyOyster.initialize(
    applicationContext,
    SDKConfig(
        apiHost = "llm-xxxx.ap-southeast-1.maas.aliyuncs.com",
        model = "happyoyster-1.0",
    ),
)

// 2) Inject the Bailian gateway API Key (as the Bearer token)
HappyOyster.updateToken(bailianApiKey)

// 3) Listen to SDK events: use onStatusChanged to drive the host state machine and gate interaction capabilities
HappyOyster.addListener(object : HappyOysterListener {
    override fun onStatusChanged(status: TravelStatusValue) {
        currentStatus = status
        when (status) {
            // Interaction is allowed only in running; adventure-mode sendCommand is valid only in running.
            TravelStatusValue.Running -> markInteractionAllowed()
            // Paused is the signal that pauseTravel has actually taken effect (asynchronous, see below); sendInstruct is still allowed here.
            TravelStatusValue.Paused -> markTravelPaused()
            // Terminal states: the SDK has already ended and released resources; clean up the host-side experience state.
            TravelStatusValue.Completed, TravelStatusValue.Failed -> clearActiveTravel()
            // init / pending: not yet ready, interaction unavailable.
            else -> markInteractionBlocked()
        }
    }
    override fun onError(error: SDKError) {
        // Unified error callback; see the error-codes section of the API Reference
    }
})

// 4) Start the experience (the ticket is delivered by your server)
lifecycleScope.launch {
    try {
        // Only one concurrent Travel is allowed per App at a time (alirtc limitation: even with a different ticket
        // you cannot start a second Travel concurrently within the same App). Calling again while an experience is in progress throws
        // SDKError(103004), whose raw carries only a redacted summary of the ticket currently playing, never the full ticket.
        val travel: StartTravelData = HappyOyster.startTravel(ticket)
        currentTravel = travel

        // Inspect the returned Travel metadata first, then decide how to interact:
        //   travel.mode          —— directing / adventure / acting
        //   travel.creationModel —— Simple / ScriptList
        //   travel.aspectRatio   —— Acting only; use to set player orientation
        val canSendInstruct = (travel.mode == ModeValue.Directing || travel.mode == ModeValue.Acting) &&
            travel.creationModel != CreationModelValue.ScriptList
        // Note: calling sendInstruct in ScriptList mode throws SDKError(103002); script content is not managed through this SDK.

        // 5) Mount the video View (the SDK returns the view; you add it to the layout)
        //    Acting: size the container from travel.aspectRatio first, then attachVideo() (see §13 Mode Adaptation) —
        //    the remote view is bound clip-to-fill, so a mismatched container orientation crops the picture.
        val videoView = HappyOyster.attachVideo()
        binding.videoContainer.addView(videoView)

        // 6) In-run interaction — you must wait for running before sending:
        //    - sendCommand (adventure): valid only in running; calling in init/pending/paused returns 103002.
        //    - sendInstruct (directing / Acting): valid in running or paused; calling in init/pending returns 103002.
        if (canSendInstruct && currentStatus == TravelStatusValue.Running) {
            HappyOyster.sendInstruct("突然下起了大雨")
        }
    } catch (e: SDKError) {
        // Handle start failure (e.g., 103004: an experience is already playing)
    }
}

// 7) Pause / Resume (directing and Acting; rewind is directing only)
//    pauseTravel is asynchronous: the method's return only means "accepted"; the actual pause is determined by onStatusChanged(paused).
//    Be sure to wait for the paused callback before allowing resumeTravel.
lifecycleScope.launch {
    if ((currentTravel?.mode == ModeValue.Directing || currentTravel?.mode == ModeValue.Acting) &&
        currentStatus == TravelStatusValue.Running
    ) {
        HappyOyster.pauseTravel()        // accepted; the host marks pausing and waits for the status callback
        // …after receiving onStatusChanged(Paused)…
    }
}
lifecycleScope.launch {
    if (currentStatus == TravelStatusValue.Paused) {
        HappyOyster.resumeTravel()       // call only after paused
    }
}

// 8) End the experience (the SDK automatically disconnects the real-time connection and releases resources)
lifecycleScope.launch { HappyOyster.endTravel() }

5. イベントサブスクリプションとエラー処理

ホストの状態マシンを駆動し、対話機能をゲート制御するために onStatusChanged を使用します。ランタイムエラーを一元的に受信するために onError を使用します。イベントインターフェースと完全なエラーコードについては、Happy Oyster Android SDK APIリファレンスを参照してください。

Must
  • HappyOyster.initialize(...) が正常に返された直後にリスナーを登録してください。addListener / removeListener は initialize の後に呼び出す必要があります (初期化前に呼び出すと SDKError(100001) がスローされます)。
  • onStatusChanged(Running) でゲート制御します。対話呼び出しは running の後でのみ許可されます。アドベンチャーモードの sendCommand は running でのみ有効です。
  • onError も処理してください。startTravel だけをキャッチしないでください。致命的なエラーも体験を終了させます (APIリファレンスのエラーコードセクションを参照)。
Recommended
  • メモリリークを避けるため、適切なライフサイクルポイント (onDestroy など) で removeListener を呼び出してください。
Avoid
  • removeListener を呼び出さずにリスナーを繰り返し登録すること。

6. 指示の送信: sendInstructとsendCommand

sendInstruct (演出 / 演技)

sendInstructは、ディレクティングとアクティングで、映像を駆動するテキスト指示を送信するために使用されます。running状態またはpaused状態のいずれでも呼び出すことができます。一時停止状態では、SDKは自動再開しません。ホストが最初にresumeTravelを呼び出してから指示を送信するかどうかを決定します(スロットリングや状態検証などの契約の詳細については、APIリファレンスを参照してください)。アクティングワールドのcreationModelは常にSimpleであるため、ScriptListの制限はディレクティングにのみ適用されます。

sendCommand (アドベンチャーモード)

sendCommandはアドベンチャーモード(adventure)で、方向/視点/アクションの制御コマンドを送信するために使用されます。running状態でのみ有効です。SDKには42ミリ秒(24 fps)の最新優先スロットルが組み込まれているため、ホストはゲームのフレームレートで呼び出すことができ、SDKが自動的にマージします。手動でのレート制限は不要です。ワンショットアクションの場合は1回だけ呼び出します。保持するアクションの場合は、保持されている間毎フレーム呼び出し続け、リリース時に明示的にNoneリセットを1回送信します(詳細なスロットリングと失敗時の表面化に関する契約については、APIリファレンスを参照してください)。

推奨される実践方法:ホストのアドベンチャーモードのインタラクションUIでは、3つの独立したコマンドエントリーポイント(移動方向 + 視点方向 + アクションインタラクション)を提供し、各グループの現在保持されている値を維持して、呼び出しのたびに完全な3フィールドのスナップショットを送信します。非アクティブなディメンションに対してのみ"None"を使用してください。異なるディメンションからの入力が同時に保持されている場合は、他のディメンションを"None"にリセットするのではなく、すべてのアクティブな値を保持して送信します。

7. 一時停止 / 再開 / 巻き戻し

対象モード:一時停止 / 再開はディレクティングとアクティングに適用され、両方で同じように動作します(以下の3秒のバリアを含む)。巻き戻しはディレクティングのみです。アドベンチャーモードでpauseTravel / resumeTravel / rewindTravelを呼び出すと、SDKによって103003で拒否されます。さらに、サーバーから報告されるversionは、そのモードが必要とするv2トークン(ディレクティングの場合はstoryV2、アクティングの場合はactingV2)である必要があります。そうでない場合、一時停止 / 再開は103002を返します。

一時停止は非同期です:pauseTravelメソッドの戻り値は、リクエストが受け付けられたことのみを意味し、実際の一時停止はonStatusChanged(Paused)によって決定されます。「pauseTravelの呼び出し」から「Pausedコールバックの受信」までの間、ホストはローカルの体験状態をpausingとしてマークできます。Pausedを受信した後でのみ、resumeTravelまたは一時停止状態に依存するrewindTravelの呼び出しを許可する必要があります。

SDK内部の一時停止→再開バリア:Pausedを受信すると、ホストは契約に従ってresumeTravel / rewindTravelを呼び出すことができます。一時停止確認後の3秒の確定ウィンドウ内に呼び出しが行われた場合、SDKの中断メソッドは、リアルタイムルームを再開するリクエストを送信する前に、残りの時間待機します。ホスト側で追加の一時停止→再開の遅延を用意する必要はなく、3秒が自然に経過した後は遅延が追加されることもありません。

ホスト側の呼び出しクールダウン (推奨): 頻繁な切り替えを避けるため、resumeTravel が正常に返された後 3 秒間は、ホストが別の pauseTravel を発行するのを控えることを推奨します。SDK自体はこのクールダウンを強制しません。

巻き戻し:rewindTravelはpaused状態でのみ利用可能です。巻き戻し後、サーバーは自動的に再開し、SDKは自動的にRTCを再接続するため、ホスト側の介入は不要です。典型的なシーケンス:pause → wait:paused → rewindTravel(sec)。巻き戻し秒数rewindToSecは4の倍数である必要があります(例:4、8、12)。倍数でない場合はサーバーによって切り捨てられます(例:7→4)。実際に有効な秒数は、返されるresumedAtSecによって決定されます。巻き戻しはディレクティングのみ:アクティングまたはアドベンチャーモードでrewindTravelを呼び出すと、HTTPリクエストを発行せずに、SDKによってローカルで103003で拒否されます。ホストは、ボタンをグレーアウトするだけでなく、巻き戻しエントリーを非表示にする必要があります。

非同期セマンティクスや 3× リトライバックオフ (resumeTravel のバックオフは 1 s / 2 s / 3 s) などの契約の詳細については、Happy Oyster Android SDK APIリファレンスを参照してください。

8. Logging

SDK Logcatタグ: HappyOysterSDK。SDKは2つの独立したログ出力パスを提供します。

  • 組み込みLogcat(デフォルトではオフ):SDKがLogcatに書き込むのはSDKConfig.logcatEnabled = trueの場合のみです(デフォルトはfalseで、完全にサイレント)。SDKConfig.logLevelは最小レベルをフィルタリングします。デフォルトのINFOには、セッションを再構築するために必要なライフサイクルアンカー(初期化、Travelの開始 / ステータス遷移 / 終了、RTC参加 / 最初のフレーム)がすでに含まれています。エラーのみの場合はWARNに下げ、より詳細な診断を行う場合はDEBUG / VERBOSEに上げます。logLevelはlogHandlerに影響しません。
  • ホストコールバックlogHandler(推奨):SDKからのすべてのLogRecord(完全なストリーム)を受信し、logLevel / logcatEnabledとは完全に独立しており、ホスト独自のログシステム(Logcat、ファイル、クラッシュプラットフォームなど)に転送できます。コールバックは高速でノンブロッキングである必要があり、SDKをコールバックしてはなりません。スローされた例外はサイレントにキャッチされます。LogRecord.messageはマスキングされており、Bearerトークン、ticket、またはRTCトークンが平文で含まれることはありません。
HappyOyster.initialize(
    context,
    SDKConfig(
        apiHost = "llm-xxxx.ap-southeast-1.maas.aliyuncs.com",  // required: your account's Bailian API Host
        model = "happyoyster-1.0",                          // required with no default: complete model name and version
        logcatEnabled = true,        // enable built-in Logcat (default false)
        logLevel = LogLevel.DEBUG,   // affects only the built-in Logcat
        logHandler = { record ->     // optional: forward to the host's own Logcat tag
            android.util.Log.d("MyApp/SDK", record.message, record.throwable)
        },
    ),
)
adb logcat -s HappyOysterSDK          # output only when built-in Logcat is enabled
adb logcat -s HappyOysterSDK MyApp    # also view the host App logs (replace MyApp with your tag)

セッションとリクエストの相関:SDKログ内のtravelId(つまりencryptedTravelId)は完全な形で出力され、サーバーに送信されるセッションキーです。セッションを調査したり、クライアントとサーバーのログを照合するためにチケットを起票したりする際には、これを含めてください。すべてのHTTPレスポンス行には、単一のリクエストを特定するためのサーバーのrequestId(reqId=フィールド、存在しない場合は-と表示)も含まれています。このIDはログにのみ表示され、公開される戻り値には決して表示されません。

9. Lifecycle & Memory

  • グローバルに1回、Application.onCreate で initialize を呼び出します。
  • アイドル状態での再初期化(たとえばAPIリージョンやmodelを切り替える場合)は、以前のアイドルランタイムを閉じ、登録済みのリスナーとフィーチャーゲートの状態をリセットします。処理が戻った後でリスナーを再登録してください。Travelの開始中、実行中、または終了中の再初期化は103004で拒否されます。必ず先にendTravel()を待機してください。SDKはinitializeの副作用として実行中のTravelを破棄することはありません。
  • 体験はホストのライフサイクルにバインドされています。リアルタイム接続とリソースが解放されるように、Activity/Fragment の onDestroy (またはViewModelの onCleared) で endTravel を呼び出してください。
  • 終了時に attachVideo() によって返されたViewをレイアウトから削除し (container.removeAllViews())、removeListener を呼び出してください。
  • SDKはアプリケーションコンテキストのみを保持します。同様に、ActivityをSDKに渡さないでください。

10. コルーチンとスレッド

  • ビジネスメソッドは suspend です。任意のコルーチンコンテキストから lifecycleScope / viewModelScope でそれらを呼び出してください。SDKはメインディスパッチャ上で状態とRTCの作業を調整し、HTTPはメインスレッド外で実行されます。
  • 呼び出し元のコルーチンがキャンセルされた場合、SDKはその呼び出しの進行中のHTTPリクエスト (存在する場合) をキャンセルし、SDKError に変換するのではなく CancellationException をそのまま伝播させます。運用上の障害としてキャッチしたり飲み込んだりしないでください。キャンセルは、サーバーがすでに受け入れたリクエストをロールバックしません。
  • attachVideo() と sendCommand() はメインスレッドで呼び出してください。メインスレッド外からの呼び出しは同期的に IllegalStateException をスローします。

11. Token Management

  • Bearer APIキーには有効期限があります。体験に入る前にトークンが最新であることを確認することを推奨します。onError(101002) (トークンの有効期限切れ) を受信した場合は、Bearerキーを再取得して updateToken を呼び出してください。新しいチケットと交換する必要はありません。

12. Error Recovery

  • 致命的なエラーの場合: 現在の体験状態をクリーンアップし (ビデオViewの削除を含む)、ユーザーにプロンプトを表示して、再起動を許可します。
  • ネットワークジッター (106001) および一時的なアップストリームサービスの異常 (106003) の場合: 限られた回数のリトライが実行される場合があります。
  • 初期化からの同期的な 100002 は、model が空であることを意味します。完全なモデル名とバージョンを指定して、再度初期化してください。空でない model の名前やバージョンが間違っている、廃止された、まだ公開されていない、または認可されていない場合、ゲートウェイは通常AccessDeniedを返し、106003 にマッピングされます。SDKError.raw を使用して、model、APIホスト、APIキー、アカウント、およびリージョンが一致していることを確認してください。

(致命的 / 非致命的な分類については、Happy Oyster Android SDK APIリファレンスのエラーコードセクションを参照してください。)

13. Mode Adaptation

  • startTravel によって返される mode を使用して、公開する対話機能を決めます。ディレクティングとアクティングはテキスト指示 sendInstruct を使用し (アクティングはプレイヤーの向きに aspectRatio も使用し、巻き戻しを非表示にします)、アドベンチャーモードは制御コマンド sendCommand を使用します。
  • アドベンチャーモードでは、オーバーロードstartTravel(ticket, maxExperienceTimeSec)を使用して、この体験の最大継続時間(秒単位、達するとセッションは自動的に終了します)を上限設定できます。許可される値はサーバー側で設定されます(現在は60 / 90 / 120、デフォルトは60)。ディレクティングとアクティングはこの値を無視します(サーバーは無視してnullをエコーバックします)。サポートされていない値を渡すと、サーバーは400000を返し、この開始は失敗します。パラメータの詳細については、APIリファレンスを参照してください。

アクティング: aspectRatio からプレイヤーの向きを設定する

StartTravelData.aspectRatioがnon-nullになるのはアクティングの場合のみです。"9:16"(縦長、ワールド作成時のサーバー側のデフォルト)または"16:9"(横長)となり、アドベンチャーとディレクティングの場合はnullです。キャンバスはワールド作成時に固定されるため(サーバーがOpen APIを通じて指定)、クライアントにとっては読み取り専用の結果となります。SDKは認識できない値をそのまま保持するため、ホストは認識できない値をnullと同様に扱い、独自のデフォルトの向きにフォールバックする必要があります。

タイミング:再生コンテナの向きは、startTravel()が戻った後、かつattachVideo()を呼び出して返されたSurfaceViewをレイアウトにマウントする前に決定します。SDKはホストのビューがアタッチされてから初めてリモートストリームのバインドとレンダリングを開始するため、その時点で向きを決定しても、最初のレンダリングフレームより前になります。この値はstartTravel()の戻り値とともに配信されますが、SDKがリアルタイム通信チャネルに参加する前に到着することは保証されていません。attachVideo()の前に適用されれば問題ありません。

結果: リモートビューは clip-to-fill レンダリングモードでバインドされます。aspectRatio と一致しない向きのコンテナは、レターボックス化せずに画像をトリミングします (たとえば、16:9 コンテナに配置された 9:16 縦長ストリームは、上下の大部分が失われます)。

// After startTravel() returns and before attachVideo(): size the container from aspectRatio first
val ratio: Float = when (travel.aspectRatio) {   // width / height
    "9:16" -> 9f / 16f                           // portrait (the Acting server-side default)
    "16:9" -> 16f / 9f                           // landscape
    else -> HOST_DEFAULT_RATIO                   // null or unrecognized: fall back to your own default orientation
}
// Apply `ratio` to the container, for example:
//   - Compose: Modifier.fillMaxWidth().aspectRatio(ratio)
//   - Views: put the container in a ConstraintLayout, full width, height driven by the ratio (height=0dp in XML)
binding.videoContainer.updateLayoutParams<ConstraintLayout.LayoutParams> {
    dimensionRatio = ratio.toString()
}

// Only once the container orientation is settled, mount the video View returned by the SDK
val videoView = HappyOyster.attachVideo()
binding.videoContainer.addView(videoView)

注記アカウントでアクティングが有効になっていない (または仕様がオフになっている) 場合、ワールドの作成と参加の両方がサーバーによって拒否されます。startTravel は 403007 で失敗し (Travelが作成される前に拒否されます)、SDKはこれをそのまま渡します。このコードをキャパシティフルとしてリトライせず、代わりに有効化を要求するプロンプトを表示してください。

14. 完全な例 (ViewModel + Activityのスニペット)

class TravelViewModel : ViewModel() {

    private val listener = object : HappyOysterListener {
        override fun onStatusChanged(status: TravelStatusValue) {
            _status.value = status
        }
        override fun onError(error: SDKError) {
            _error.value = error
        }
    }

    init { HappyOyster.addListener(listener) }

    fun start(ticket: String) = viewModelScope.launch {
        try {
            val travel = HappyOyster.startTravel(ticket)
            _travel.value = travel
        } catch (e: SDKError) {
            _error.value = e
        }
    }

    fun send(text: String) = viewModelScope.launch {
        runCatching { HappyOyster.sendInstruct(text) }
    }

    fun stop() = viewModelScope.launch { runCatching { HappyOyster.endTravel() } }

    override fun onCleared() {
        HappyOyster.removeListener(listener)
        viewModelScope.launch { runCatching { HappyOyster.endTravel() } }
    }
}

// Mount the video in the Activity
val videoView = HappyOyster.attachVideo()
binding.videoContainer.addView(videoView)
// On end
binding.videoContainer.removeAllViews()

15. DevOps トラブルシューティングガイド

統合中に問題が発生した場合 (Travelに入れない、ビデオが表示されない黒画面、セッション中のストリーム切断、一時停止/再開の異常など)、SDKログを確認することが問題を特定する最も早い方法です。この章では、標準的なトラブルシューティングフローを示します。これは、自己チェックだけでなく、1回のやり取りで十分な情報を提供するためにも役立ちます。

15.1 Enabling logs

トラブルシューティング中は、必要に応じてログ出力を選択してください(§8 ログを参照)。簡単な自己チェックにはlogcatEnabled = trueを設定してadb logcatで読み取り、診断時にはlogLevelをDEBUGまたはVERBOSEに上げます(デフォルトのINFOにはすでに完全なライフサイクルタイムラインが含まれています。エラーのみの場合はWARNに下げます)。独自のシステムに供給するには、logHandlerを使用してすべてのLogRecord(logLevelの影響を受けない)を受信し、ファイルまたはクラッシュプラットフォームに書き込みます。

15.2 セッション相関ID: travelId

SDKログ内の travelId (つまり StartTravelData.encryptedTravelId) は完全な形で出力されます。これは1つのセッションを通じて使用される一意の識別子であり、サーバーに送信されるセッションキーです。これは クライアントログ と サーバー側のセッション記録 を照合するための鍵です。問題を報告する際は、必ず失敗したセッションの travelId を含めてください。

15.3 ログの収集

問題を再現する際に、SDKログをファイルに保存します。

# Clear history, reproduce the issue, then capture SDK logs to a file
adb logcat -c
adb logcat -s HappyOysterSDK > happyoyster-sdk.log
# To also see your own app logs (replace MyApp with your tag):
adb logcat -s HappyOysterSDK MyApp > happyoyster-sdk.log

15.4 問題を報告する際に含めるべき情報

複数のやり取りを避けるため、以下のものを添付してください。

項目

Notes

SDK version

HappyOyster.VERSION

Session travelId

失敗したセッションの encryptedTravelId (15.2 を参照)

Time of occurrence

おおよその時間(分単位で可)

エラーコード

キャプチャされた SDKError.code (意味はAPIリファレンスのエラーコード表を参照)

Reproduction steps

操作パス + 期待される結果 + 実際の結果

Environment

デバイスモデル、Androidバージョン、ネットワーク(WiFi/モバイル通信)

Log file

15.3 の HappyOysterSDK ログ (DEBUG/VERBOSE を推奨)

15.5 マスキングとログの共有について

SDKログは安全に共有できるように設計されています。認証情報(Bearerトークン、Travelチケット、RTCトークン)、RTC内部識別子、およびメディアURLは書き込み前にマスキングされるため、平文で表示されることはありません。travelIdはセッション識別子(認証情報ではない)であり、相関関係の追跡のためにのみ完全な形で保持されます。それでも、ログファイルは管理されていないプラットフォームに公開して貼り付けるのではなく、信頼できるチャネル経由で転送することをお勧めします。