Skip to main content
SDK iOS

Referensi API SDK iOS HappyOyster

Titik masuk SDK iOS HappyOyster adalah singleton tingkat proses HappyOysterEngine.shared. Metode bisnis semuanya bersifat async throws, melempar OysterSDKError saat gagal. Satu pengalaman dibawa oleh handle OysterTravel, yang mencakup tiga mode — adventure, directing, dan acting — pada UIKit maupun SwiftUI.

  • Dokumen ini adalah deskripsi fitur eksternal + referensi antarmuka untuk Happy Oyster iOS SDK: dokumen ini menjelaskan parameter, waktu, penggunaan, dan contoh singkat dari setiap tipe dan metode publik, satu per satu.
  • Untuk alur integrasi lengkap (pengaturan proyek, konfigurasi dependensi, koordinasi sisi server, pelaksanaan end-to-end), lihat dokumentasi proyek sampel; tidak diulang di sini.

1. Konsep Inti

Beberapa istilah terlebih dahulu, agar sisanya lebih mudah dibaca.

Konsep

Deskripsi

token

Token autentikasi HTTP (API Key sementara Bailian). Server Anda menukarnya melalui API Bailian dan mengirimkannya ke bawah; diinjeksikan melalui updateToken(_:), dan SDK membawanya sebagai HTTP Bearer saat meminta gateway. SDK hanya menyimpan yang terbaru, tidak menyimpan atau menyegarkannya; setelah kedaluwarsa, Anda menukar dan menginjeksikannya kembali.

ticket

Kredensial uji coba sekali pakai. Server Anda menukarnya melalui platform terbuka get-travel-credential (prefiks tk_, berlaku selama 30 menit, sekali pakai), digunakan sebagai argumen createTravel(ticket:); menjadi tidak valid setelah pengalaman berakhir atau kedaluwarsa, dan tidak dapat digunakan kembali.

World

World AI, berisi karakter dan adegan. Dibuat dan dikelola oleh server Anda; SDK tidak terlibat.

Travel

Pengalaman real-time tunggal, sesuai dengan satu handle OysterTravel. Sekali pakai; menjadi tidak valid setelah status terminal tercapai, memerlukan createTravel baru melalui mesin.

Status sesi

OysterTravelStatus: idle → prepare → running → pausing → paused (paused kembali ke prepare → running melalui reconnect), ditambah dua status terminal ended / failed (lihat §7).

Mode

adventure (perintah direction/view/action), directing (alur cerita berbasis teks), atau acting (pertunjukan karakter berbasis teks). mode yang dikembalikan oleh start() menentukan UI yang sesuai; lihat §2 untuk apa yang didukung oleh setiap mode.

2. Ikhtisar

Setelah mengintegrasikan SDK ini, aplikasi Anda dapat memasuki "world" yang dihasilkan secara real-time oleh AI, dan memiliki pengalaman video interaktif real-time dalam tiga mode — adventure, directing, atau acting: mulai pengalaman → pemutaran real-time → interaksi real-time → kontrol proses (pause/resume/rewind/end) → callback status dan error. Setiap mode mendukung serangkaian kapabilitas yang berbeda. Gerakkan UI Anda dari mode yang dikembalikan oleh start():

Kapabilitas

adventure

directing

acting

sendCommand (arah/tampilan/tindakan)

Ya

Tidak

Tidak

sendInstruct (instruksi teks)

Tidak

Ya

Ya

pause() / resume()

Tidak

Ya

Ya

rewind(toSec:)

Tidak

Ya

Tidak — sembunyikan titik masuk

end()

Ya

Ya

Ya

start(maxExperienceTimeSec:)

Diterapkan

Diabaikan

Diabaikan

World acting mengutamakan potret: start() mengembalikan aspectRatio (9:16 / 16:9) untuk sesi tersebut. Gunakan ini untuk memilih orientasi pemutar dan ukuran kontainer sebelum menarik aliran (lihat §8).
import HappyOysterSDK; titik masuk intinya terdiri dari dua tipe. HappyOysterEngine — titik masuk orkestrasi, singleton tingkat proses HappyOysterEngine.shared.

Method / Property

Deskripsi

initialize(config:)

Inisialisasi runtime dan daftarkan mesin real-time secara otomatis (sekali, sebelum createTravel)

updateToken(_:)

Injeksikan / perbarui token autentikasi HTTP

createTravel(ticket:)

Buat handle sesi dengan kredensial sekali pakai

cleanup()

Lepaskan sumber daya (dapat initialize lagi)

version

Versi SDK saat ini

OysterTravel — handle sesi untuk satu pengalaman, dibuat oleh createTravel(ticket:).

Method / Property

Deskripsi

videoView / OysterVideoView(travel:)

Tampilan pemutaran (UIKit / SwiftUI)

events / status

Aliran push perubahan status + error / status saat ini yang dapat diobservasi

start() / start(maxExperienceTimeSec:)

Hubungkan dan mainkan (opsional meminta durasi maksimum pengalaman adventure)

sendInstruct(content:)

Instruksi teks mode Directing

sendCommand(_:) / flushCommands()

Kontrol mode Adventure

pause() / resume()

Jeda / lanjutkan (directing dan acting)

rewind(toSec:)

Putar mundur (hanya saat dijeda)

end()

Akhiri (idempoten, pastikan untuk memanggilnya)

pauseLocalAudioCapture() / resumeLocalAudioCapture()

Lepaskan / pulihkan mikrofon sementara

Ada juga beberapa helper exports: OysterLog untuk mengambil alih pencatatan internal SDK; HappyOysterEngine.version untuk membaca versi; OysterVideoView adalah tampilan pemutaran SwiftUI (setara dengan videoView). Penggunaan dijelaskan selanjutnya. Siklus hidup: inisialisasi → injeksi token → buat sesi → mount video + berlangganan event → mulai pemutaran → interaksi → akhiri. SDK tidak bertanggung jawab untuk membuat atau mengelola world (dilakukan oleh server Anda), dan tidak mengekspos detail komunikasi real-time tingkat rendah.

3. Quick Start

Alur lengkap dari inisialisasi hingga pengakhiran, dengan komentar langkah demi langkah. Detail setiap API ada di §6.
import HappyOysterSDK

// 1) Initialize (as early as possible after app launch, once before createTravel)
let engine = HappyOysterEngine.shared
engine.initialize(config: OysterConfig(
    apiHost: "[workspace-id].[region].maas.aliyuncs.com",// For the API host, refer to the Bailian documentation https://www.alibabacloud.com/help/en/model-studio/base-url
    model: "happyoyster-1.0-adventure"                  // Versioned model name enabled for your account, required; see the Happy Oyster model documentation
))

// 2) (Optional) Take over logging: set level + custom output
OysterLog.setMinimumLevel(.info)
OysterLog.setHandler { level, tag, message in
    print("[Oyster][\(level)][\(tag)] \(message)")
}

// 3) Inject the HTTP auth token (Bailian temporary API Key, delivered by your server)
engine.updateToken(temporaryApiKey)

// 4) Create a session handle with a one-time ticket (not yet connected)
let travel = try engine.createTravel(ticket: ticket)

Task { @MainActor in
    // 5) Mount the video (UIKit; for SwiftUI use OysterVideoView(travel:))
    containerView.addSubview(travel.videoView)

    // 6) Subscribe to events before start, to avoid missing early statuses
    let eventTask = Task {
        for await event in travel.events {
            switch event {
            case .statusChanged(let status): render(status)   // §7 status table
            case .error(let error):          handle(error)     // §9 error.code / error.kind
            }
        }
    }

    do {
        // 7) Connect and play
        let data = try await travel.start()

        // 8) Interact based on mode
        if data.mode == .directing {
            _ = try await travel.sendInstruct(content: "Suddenly it starts to pour")
        } else {
            travel.sendCommand(OysterAdventureCommand(translation: .front))
        }
    } catch let error as OysterSDKError {
        handle(error)
    }

    // 9) Funnel every exit path into a single end()
    _ = try? await travel.end()
    eventTask.cancel()
}

// Exit the SDK / switch gateway: await engine.cleanup()
Untuk pengaturan proyek, konfigurasi dependensi (termasuk adaptor AliRTC dan biner vendor yang diperlukan), koordinasi sisi server, dan pelaksanaan end-to-end, lihat dokumentasi proyek sampel.

4. Persyaratan

Item

Requirement

OS Minimum

iOS 15.0+ (semua tipe publik ditandai dengan @available(iOS 15.0, *))

Language

Swift (async/await)

Konkurensi

Akses main-thread (tipe entri ditandai dengan @MainActor)

Network

Akses jaringan publik diperlukan

Izin

Info.plist harus menyertakan NSMicrophoneUsageDescription (lihat di bawah)

Izin mikrofon: Pengalaman video interaktif real-time memerlukan saluran audio/video real-time dua arah (uplink + downlink), sehingga SDK menggunakan mikrofon lokal saat berjalan — ini bukan perekaman. Info.plist harus menyediakan NSMicrophoneUsageDescription, jika tidak, memulai penangkapan real-time akan crash. Saat Anda memerlukan akses mikrofon eksklusif (misalnya pengenalan suara), gunakan pauseLocalAudioCapture() / resumeLocalAudioCapture() untuk melepaskannya dan memulihkannya sementara waktu (lihat §6.2).

5. Integrasi dan Autentikasi

5.1 Integrasi dan Dependensi

import HappyOysterSDK adalah semua yang Anda butuhkan di tingkat kode. SDK didistribusikan sebagai biner yang telah dikompilasi sebelumnya (xcframework) melalui subspecs CocoaPods, dipublikasikan ke CocoaPods Trunk publik — deklarasikan dependensi di Podfile Anda:
# HappyOysterSDK / AliVCSDK_ARTC are both published on the public CocoaPods source.
pod 'HappyOysterSDK', '1.0.3'              # Aggregate entry point (Core + World)
pod 'HappyOysterSDK/UI', '1.0.3'           # Optional default UI components (video view, control HUD)
pod 'HappyOysterSDK/StreamAliRTC', '1.0.3' # Video stream + AliRTC engine adapter (already depends on Stream)

# RTC vendor binary: weak-linked by the SDK, not redistributed with it — bring your own.
pod 'AliVCSDK_ARTC', '7.11.0'
AliVCSDK_ARTC diperlukan setiap kali Anda menarik HappyOysterSDK/StreamAliRTC: jika tidak ada, SDK secara diam-diam kembali ke Loopback — SDK dapat terhubung dan mencapai running, tetapi menampilkan layar hitam tanpa error.
Pengaturan proyek dan alur inisialisasi end-to-end ditangani oleh proyek sampel; lihat dokumentasi proyek sampel. Dokumen ini berfokus pada antarmuka itu sendiri.

5.2 Model Autentikasi

SDK tidak memperoleh atau menyegarkan token, menjaga agar tetap ringan. Autentikasi memiliki dua lapisan, dan integrator mengelola siklus hidupnya:
  1. Token autentikasi HTTP (API Key sementara Bailian): Server Anda menukarnya melalui API Bailian dan mengirimkannya ke bawah; diinjeksikan melalui updateToken(_:). Beberapa layanan internal SDK memanggil gateway Bailian secara langsung, membawa token ini untuk autentikasi — oleh karena itu harus berupa API Key sementara yang dikeluarkan oleh Bailian, bukan token dari layanan bisnis Anda sendiri. SDK hanya menyimpan yang terbaru, tidak menyimpan atau menyegarkannya; setelah kedaluwarsa, Anda menukar dan menginjeksikannya kembali.
  2. Kredensial uji coba sekali pakai ticket: Server Anda menukarnya melalui platform terbuka get-travel-credential (prefiks tk_, berlaku selama 30 menit, sekali pakai), digunakan sebagai argumen createTravel(ticket:); menjadi tidak valid setelah pengalaman berakhir (secara normal atau tidak normal) atau kedaluwarsa, dan tidak dapat digunakan kembali.
AK, kunci penandatanganan, dan kredensial hak istimewa tinggi lainnya hanya ada di server Anda; SDK klien tidak pernah menyentuhnya. Apa yang diterima klien selalu berupa API Key sementara yang berumur pendek.
Feature Gate (sakelar jarak jauh / peningkatan paksa): Server dapat menonaktifkan SDK dari jarak jauh atau menetapkan versi minimum yang didukung. Saat dinonaktifkan, panggilan yang dibatasi (start / pause / resume / rewind / sendInstruct / sendCommand) ditolak dengan 108001 dan pengalaman yang sedang berjalan dihentikan oleh SDK (lihat §9). OysterSDKError.raw membawa alasan yang dapat dibaca manusia; minta pengguna untuk melakukan upgrade saat versinya terlalu rendah.
Menangani kedaluwarsa token: Setelah token autentikasi HTTP di atas kedaluwarsa, segera minta token baru dari server Anda dan injeksikan melalui updateToken(_:). Ada dua tempat di mana Anda perlu menentukan apakah token telah kedaluwarsa:
  1. Saat memanggil API seperti engine.createTravel atau travel.start, tangani error dan periksa tipe error token kedaluwarsa/tidak valid (101001 / 101002); setelah menyuntikkan token baru, panggil ulang API yang sesuai.
  2. Saat mendengarkan event .error dari OysterTravelEvent, periksa tipe error token kedaluwarsa/tidak valid, minta ulang token, dan suntikkan.

6. Referensi API

Ada dua tipe entri, keduanya @MainActor dan @available(iOS 15.0, *). Metode bisnis bersifat async throws dan melempar OysterSDKError saat gagal; metode dengan nilai kembalian semuanya ditandai dengan @discardableResult.

6.1 HappyOysterEngine

Singleton tingkat proses, titik masuk orkestrasi. init tidak bersifat publik — selalu gunakan HappyOysterEngine.shared; jangan menginstansiasinya sendiri (mesin real-time yang mendasarinya juga merupakan singleton proses).
@MainActor @available(iOS 15.0, *)
public final class HappyOysterEngine {
    public static let shared: HappyOysterEngine            // the unique process-level instance
    public static let version: String                      // current SDK version

    @discardableResult
    public func initialize(config: OysterConfig) -> Bool  // initialize (once before createTravel)
    public func updateToken(_ token: String)               // inject/update the HTTP auth token
    public func createTravel(ticket: String) throws -> OysterTravel  // create a session handle with a one-time credential
    public func cleanup() async                            // release resources (can initialize again)
}
initialize(config:)
  • Tujuan: Menginisialisasi runtime dan mendaftarkan mesin real-time secara otomatis (tidak perlu pendaftaran manual oleh host).
  • Parameter: config.apiHost adalah URL gateway Bailian, bukan server bisnis Anda, dan wajib diisi; di lingkungan pra-rilis/ujicoba, Anda harus secara eksplisit meneruskan gateway yang sesuai, jika tidak, permintaan akan gagal (misalnya 106001, domain tidak dapat diselesaikan). config.model adalah nama model berversi yang diaktifkan untuk akun Anda dan juga wajib diisi tanpa nilai default — Happy Oyster dibagi menjadi sub-model per-mode, sehingga SDK tidak dapat menyimpulkan mana yang harus digunakan. Keduanya harus termasuk dalam akun dan wilayah yang sama dengan token yang diinjeksi. Bidang lainnya ada di §8 OysterConfig.
  • Satu model melayani satu mode: setiap model per-mode memiliki rute gateway-nya sendiri, sehingga satu initialize hanya melayani dunia dengan satu mode tersebut. Jika aplikasi Anda menawarkan dunia dalam beberapa mode, cukup panggil initialize() lagi dengan model yang sesuai sebelum memasuki dunia dengan mode yang berbeda — saat idle, konfigurasi terbaru yang akan digunakan, tidak perlu cleanup() dan token yang diinjeksi tetap dipertahankan; saat Travel sedang berlangsung, panggilan akan diabaikan, jadi end() terlebih dahulu. Model yang tidak sesuai dengan mode dunia akan ditolak oleh gateway dengan AccessDenied, dinormalisasi menjadi 106003.
  • Kapan digunakan: Panggil sekali sebelum createTravel, sedini mungkin setelah peluncuran aplikasi.
  • Mengembalikan: Bool — apakah config yang Anda berikan berlaku. Bernilai false dalam dua kasus: konfigurasi tidak valid (apiHost / model kosong atau tidak membentuk URL gateway yang valid), atau Travel sedang berjalan sehingga panggilan diabaikan. Dalam kedua kasus tersebut, runtime dibiarkan tidak berubah.
  • Catatan: Saat idle, memanggilnya kembali akan mengonfigurasi ulang runtime dengan konfigurasi baru (mengubah apiHost / model tidak memerlukan cleanup(), dan token yang diinjeksi tetap dipertahankan); ini menjadi no-op dengan peringatan hanya saat Travel sedang berlangsung, jadi end() terlebih dahulu. Konfigurasi yang tidak valid membiarkan runtime tidak berubah. Jangan gunakan isReady untuk mengetahui apakah re-initialize berhasil — jika ditolak, konfigurasi sebelumnya masih berlaku dan isReady tetap true; isReady menjawab "apakah engine dapat digunakan sekarang", nilai pengembalian menjawab "apakah konfigurasi yang baru saja saya berikan telah berlaku".
OysterLog
  • Tujuan: Mengonfigurasi dan mengambil alih pencatatan internal SDK, mencetaknya ke dalam modul pencatatan Anda sendiri. Menyediakan setMinimumLevel(_:) untuk mengatur level dan setHandler(_:) untuk output kustom (lihat contoh §3).
updateToken(_:)
  • Tujuan: Menyuntikkan / memperbarui token autentikasi HTTP (API Key sementara Bailian, §5.2).
  • Kapan digunakan: Setelah initialize, dapat dipanggil kapan saja; tukar ulang dan panggil lagi setelah token kedaluwarsa atau setelah menerima error terkait autentikasi (101001 / 101002).
  • Catatan: Tidak ada operasi dan menampilkan peringatan jika belum di-initialize.
createTravel(ticket:)
  • Tujuan: Membuat handle sesi tunggal dengan ticket sekali pakai.
  • Parameter: ticket adalah kredensial sekali pakai; setelah dibuat, dianggap terpakai untuk pengalaman ini.
  • Kapan digunakan: Panggil sebelum setiap pengalaman baru; handle yang dikembalikan belum terhubung dan Anda harus memanggil travel.start() setelahnya. Video diambil dari handle yang dikembalikan (lihat §6.2).
  • Catatan (throws sinkron): melempar 100001 jika belum di-initialize; melempar 103004 jika dipanggil lagi sebelum Travel sebelumnya di-end() (setiap mesin hanya mengizinkan satu Travel aktif pada satu waktu).
cleanup()
  • Tujuan: Melepaskan sumber daya SDK (mengakhiri travel aktif, konfigurasi runtime, token).
  • Kapan digunakan: Saat keluar sepenuhnya dari SDK atau perlu mengubah config.
  • Catatan: async — secara internal ia secara deterministik mengakhiri travel aktif saat ini dengan end() terlebih dahulu, kemudian meruntuhkan runtime, tanpa meninggalkan fire-and-forget. Setelah dilepaskan, Anda dapat melakukan initialize lagi.

6.2 OysterTravel

Handle sesi yang dibuat oleh createTravel; sekali pakai, menjadi tidak valid setelah status terminal (end / akhir sisi server / kegagalan) tercapai, memerlukan createTravel baru melalui engine. Ini juga merupakan ObservableObject (@Published status, dapat langsung menggerakkan SwiftUI).
@MainActor @available(iOS 15.0, *)
public final class OysterTravel: ObservableObject {
    @Published public private(set) var status: OysterTravelStatus   // current external status (observable)
    public var isEnded: Bool { get }                                // whether a terminal state is reached (synchronously readable)
    public var videoView: UIView { get }                            // UIKit playback view; for SwiftUI use OysterVideoView(travel:)
    public var events: AsyncStream<OysterTravelEvent> { get }       // status change + error, multi-subscribe

    @discardableResult public func start() async throws -> OysterStartTravelData   // connect and play
    @discardableResult public func start(maxExperienceTimeSec: Int?) async throws -> OysterStartTravelData  // connect and play + request max experience duration (adventure mode only)
    @discardableResult public func pause() async throws -> OysterTravelStateData   // pause (directing / acting)
    @discardableResult public func resume() async throws -> OysterTravelStateData  // resume
    @discardableResult public func rewind(toSec: TimeInterval) async throws -> OysterRewindTravelData  // rewind (paused only)
    @discardableResult public func end() async throws -> OysterEndTravelData       // end (idempotent, be sure to call)
    @discardableResult public func sendInstruct(content: String) async throws -> OysterSendInstructData  // directing-mode text instruction

    public func sendCommand(_ command: OysterAdventureCommand)      // adventure-mode control (fire-and-forget)
    public func flushCommands()                                     // manually send an already-submitted pending command
    public func pauseLocalAudioCapture() async                      // temporarily yield the microphone
    public func resumeLocalAudioCapture() async                     // restore microphone occupation
}
Tampilan video videoView / OysterVideoView(travel:)
  • Tujuan: Entri rendering untuk gambar jarak jauh — "SDK menyediakan tampilan, host menempatkannya". Untuk UIKit, ambil travel.videoView; untuk SwiftUI, gunakan OysterVideoView(travel:).
  • Kapan digunakan: Tersedia segera setelah handle dibuat (akses berulang mengembalikan tampilan yang sama); mount ke dalam hierarki apa pun, dan setelah engine siap, tampilan akan dirender secara otomatis. Mount sebelum atau setelah start() keduanya berfungsi, tanpa layar hitam.
  • Catatan: Saat sesi berakhir, SDK secara otomatis melepaskan binding rendering; hapus tampilan dari hierarki sesuai kebutuhan.
Event dan status events / status / isEnded
  • Tujuan: events adalah aliran push perubahan status + error; status adalah status eksternal saat ini yang dapat diobservasi; isEnded adalah flag yang dapat dibaca secara sinkron untuk status terminal.
  • Kapan digunakan: Disarankan untuk mulai mengonsumsi events sebelum start(), untuk menghindari terlewatnya status awal.
  • Catatan: Setiap akses ke events mengembalikan stream independen, mendukung multi-subscription; berhenti berlangganan = mengakhiri iterasi for await (atau menghancurkan Task yang ditahan). Di SwiftUI Anda dapat mengamati status secara langsung dengan @StateObject/@ObservedObject (error tetap masuk melalui events). Lihat §7.
start() / start(maxExperienceTimeSec:)
  • Tujuan: Gunakan ticket yang diambil pada saat pembuatan untuk ditukar dengan konfigurasi travel + join RTC dan terhubung untuk bermain. Jika berhasil, SDK secara otomatis membuat koneksi real-time dan memulai polling status internal, memunculkan status melalui events.
  • Parameter: maxExperienceTimeSec (opsional) — meminta durasi maksimum (dalam detik) untuk pengalaman petualangan ini. Nilai dikirim ke server apa adanya; nilai yang diizinkan, durasi efektif aktual, dan waktu berakhir otomatis semuanya ditentukan oleh server — SDK tidak melakukan validasi lokal. Meneruskan nil (atau memanggil start() tanpa parameter) menggunakan durasi default server. Mode Directing mengabaikan parameter ini. Ketika waktu habis, server mengakhiri pengalaman dan host menerima status terminal ended melalui events (sama seperti akhir sisi server, lihat §7).
  • Mengembalikan: OysterStartTravelData (mode / version / encryptedTravelId, dll., lihat §8), yang menentukan UI interaksi.
  • Error: 401010/401011 (kredensial tidak valid/terpakai), 403002 (world belum siap), 403007 (spesifikasi layanan tidak diaktifkan, misal acting), 429001/429002 (batas konkurensi / kapasitas habis), 500001 (kegagalan resource/server), 103004 (start konkuren).
  • mode dunia ticket harus cocok dengan model yang diteruskan ke initialize (tanggung jawab pemanggil): dengan model per-mode, setiap model memiliki rute gateway-nya sendiri, dan start() mengirim ticket ke rute model yang saat ini diinisialisasi. SDK tidak dan tidak dapat memverifikasi ini untuk Anda sebelumnya — mode dikirimkan oleh respons dari panggilan start() ini sendiri (OysterStartTravelData.mode); sebelum panggilan, SDK hanya menyimpan ticket yang buram dan nama model, tanpa mode untuk dibandingkan, dan menyimpulkan mode dari nama model berarti menebak skema penamaan milik server, yang tidak dilakukan oleh SDK. Jadi: sebelum memasuki dunia dengan mode yang berbeda, panggil initialize() lagi dengan model yang sesuai (saat idle, konfigurasi terbaru yang digunakan — tidak perlu cleanup() dan token dipertahankan; saat Travel sedang berlangsung, panggilan diabaikan, jadi end() terlebih dahulu). Jika tidak cocok, start() ini akan gagal di gateway; saat mendiagnosis, pertama-tama periksa apakah model saat ini dan mode dari dunia ticket saling terkait, lalu lihat kode kesalahan kredensial di atas.
  • Catatan (auto-end saat tidak ada stream): Server mengirimkan "no-stream timeout" (default ~30s). Jika, setelah terhubung, tidak ada stream yang diterima dalam durasi tersebut (tidak pernah mencapai running), SDK secara otomatis mengakhiri pengalaman, beralih ke failed, dan memunculkan 105006 melalui .error dari events (fatal; tangani dengan kembali ke layar pra-start, tidak perlu menghitung waktunya sendiri).
pause() / resume()
  • Tujuan: Menjeda / melanjutkan pengalaman (idempoten).
  • Kapan digunakan: Didukung oleh world directing dan acting; tidak didukung oleh adventure. Gunakan mode yang dikembalikan oleh start() untuk memutuskan terlebih dahulu apakah akan menampilkan tombol pause. pause mengharuskan status saat ini berupa running; resume mengharuskan paused.
  • Error: 103001 (tidak ada pengalaman aktif), 103002 (status/versi tidak diizinkan), 103003 (ketidakcocokan mode).
rewind(toSec:)
  • Tujuan: Memutar mundur ke detik yang ditentukan. Jika berhasil, SDK secara otomatis bergabung kembali dengan rtcConfig asli dan kembali ke pemutaran.
  • Kapan digunakan: Hanya dapat dimulai dalam status paused, dan hanya oleh world directing — acting dan adventure tidak mendukung putar mundur, jadi sembunyikan titik masuk putar mundur dalam mode tersebut dan jangan memanggilnya.
  • Error: 103001, 103002.
end()
  • Tujuan: Mengakhiri pengalaman (idempoten, dapat dipanggil berulang kali). Setelah panggilan berhasil atau keluar secara tidak normal, SDK secara otomatis memutuskan koneksi real-time, menghentikan polling, dan melepaskan semua resource sesi; ticket menjadi tidak valid pada saat yang sama, dan handle memasuki status terminal.
  • Kapan digunakan / Catatan: Baik pengguna keluar secara aktif maupun pengalaman berakhir secara pasif (kedaluwarsa timer, menerima .ended/.failed, penghancuran halaman), pastikan satu end() tercapai, jika tidak, resource jarak jauh mungkin tidak dilepaskan tepat waktu. Disarankan untuk menyalurkan semua jalur keluar ke metode pembersihan idempoten yang sama.
sendInstruct(content:) (mode directing)
  • Tujuan: Mengirim instruksi teks untuk menggerakkan alur cerita.
  • Kapan digunakan: Mode directing; dikirim langsung saat running, di-cache saat paused dan dikirim ulang dengan frame pertama setelah dilanjutkan kembali ke running melalui reconnect.
  • Error: 103001, 103002, 103003 (dipanggil dalam mode adventure), 403004 (moderasi konten), 404000 (travel tidak ditemukan).
sendCommand(_:) / flushCommands() (mode adventure)
  • sendCommand: Kirim perintah kontrol direction/view/action (lihat §8 OysterAdventureCommand; fire-and-forget, tidak ada pengembalian, tidak melempar error). Efektif hanya dalam mode adventure saat running. Input eksternal dapat berfrekuensi tinggi setiap frame, dan SDK melakukan throttle secara internal (sampling latest-wins, penggabungan frame pada line rate RTC); host tidak perlu melakukan throttle sendiri. Perhatikan bahwa respons server terhadap perintah itu sendiri memiliki latensi, sehingga waktu efektif sebenarnya tidak tetap.
  • flushCommands: Dipanggil pada saat "pelepasan tombol / pelepasan input", segera mengirim ulang perintah terakhir yang sudah menunggu dalam antrean; murni tidak melakukan apa pun ketika tidak ada perintah yang tertunda, tidak menghasilkan perintah baru.
  • Error (semua dimunculkan melalui .error dari events, bukan throws): tidak ada pengalaman aktif 103001; dipanggil dalam mode directing 103003; saluran real-time belum siap / gagal mengirim 105004.
pauseLocalAudioCapture() / resumeLocalAudioCapture()
  • Tujuan: Melepaskan / memulihkan sementara pendudukan SDK pada penangkapan mikrofon lokal.
  • Kapan digunakan: Ketika sesuatu seperti pengenalan suara memerlukan akses mikrofon eksklusif, pause terlebih dahulu dan resume setelahnya.

7. Event dan Status

Event dilampirkan pada OysterTravel.events dan merupakan saluran push aktif SDK kepada Anda, digunakan untuk memunculkan situasi yang tidak dipicu oleh panggilan Anda sendiri (misalnya masalah dengan koneksi real-time yang dikelola secara internal atau polling status).
var events: AsyncStream<OysterTravelEvent> { get }

@available(iOS 15.0, *)
public enum OysterTravelEvent {
    case statusChanged(OysterTravelStatus)
    // The SDK's internal flow errored, e.g. an internal API request error or a streaming error; note you must check for token expiration here and re-request the token
    case error(OysterSDKError)
}
Ada dua gaya konsumsi, pilih salah satu:
  • SwiftUI: OysterTravel adalah ObservableObject; gunakan @StateObject/@ObservedObject secara langsung untuk mengobservasi status dan menggerakkan UI; error tetap berasal dari events.
  • Imperatif / UIKit: dalam Task, for await event in travel.events { ... }, dan switch atas .statusChanged / .error; batalkan Task yang ditahan setelah selesai.
let task = Task {
    for await event in travel.events {
        switch event {
        case .statusChanged(let status): render(status)   // see table below
        case .error(let error):          handle(error)     // error.code / error.kind, see §9
        }
    }
}
// When done: task.cancel()
OysterTravelStatus (5 status proses + 2 status terminal):

Status

Deskripsi

Penanganan umum

idle

setelah create, sebelum start

—

prepare

menghubungkan / menghubungkan ulang (menghubungkan / menghubungkan ulang internal)

tampilkan petunjuk koneksi / koneksi ulang

running

stream siap, interaktif (pemutaran internal)

tampilkan gambar dan kontrol

pausing

jeda diterima, menunggu konfirmasi server

tampilkan "pausing…"

paused

dijeda (dikonfirmasi)

tampilkan status dijeda (hanya directing yang menampilkan entri rewind)

ended

berakhir (diakhiri aktif atau diakhiri sisi server). Terminal

akhiri dan tutup halaman

failed

gagal. Terminal

tampilkan kesalahan dan akhiri

Setelah memasuki ended / failed, sesi telah berakhir, dan semua operasi sesi (pause/resume/sendCommand…) tidak lagi berlaku. Callback dapat dipicu pada main thread, sehingga Anda dapat memperbarui UI secara langsung.
Selama polling status internal, SDK secara otomatis melaporkan heartbeat status pull/playback klien (connecting / playing / paused / reconnecting / disconnected), sehingga server dapat membedakan status sisi stream dari sisi klien — murni perilaku internal SDK, yang tidak perlu disadari atau diikutsertakan oleh host.

8. Model Data

Semua tipe publik ditandai dengan @available(iOS 15.0, *). Nilai/parameter pengembalian di bawah ini adalah output SDK, dibangun di tempat secara internal dengan tipe Swift native (Date / TimeInterval / OysterTravelStatus); semuanya bukan Codable dan tidak mengekspos detail wire (snake_case) — decoding wire terjadi di dalam SDK.
// Configuration
public struct OysterConfig: Sendable {
    public let apiHost: String             // Bailian gateway URL (not a business server), required, passed explicitly by the host
    public let model: String               // Versioned model name (e.g. "happyoyster-1.0-adventure"), required, no default
    public let logLevel: OysterLogLevel?   // .debug/.info/.warning/.error; defaults to .warning when nil
    public let callbackTimeoutMs: Int      // global callback timeout, default 30000; surfaced as 105005 on timeout
    public init(apiHost: String, model: String, logLevel: OysterLogLevel? = nil, callbackTimeoutMs: Int = 30_000)
}

public enum OysterLogLevel: Int, Comparable, CaseIterable, Sendable {
    case debug, info, warning, error
}

// External session status (§7)
public enum OysterTravelStatus: String, Equatable, Sendable, CustomStringConvertible {
    case idle      // after create, before start
    case prepare   // connecting / reconnecting
    case running   // stream ready, interactive
    case pausing   // pause accepted, awaiting server confirmation
    case paused    // paused (confirmed)
    case ended     // terminal: active end or server-side end
    case failed    // terminal: failed
}

// Open string value (preserves unknown values for server-side extension; encoded/decoded as a bare JSON string "running")
public struct OysterRawValue: RawRepresentable, Equatable, Hashable, Codable, Sendable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }
}
public typealias OysterModeValue = OysterRawValue
public extension OysterModeValue {
    static let adventure = OysterModeValue(rawValue: "adventure")
    static let directing  = OysterModeValue(rawValue: "directing")
    static let acting     = OysterModeValue(rawValue: "acting")
}

// start() return (SDK output, not Codable)
public struct OysterStartTravelData: Equatable, Sendable {
    public let encryptedTravelId: String  // identifier for this experience
    public let encryptedWorldId: String
    public let mode: OysterModeValue       // adventure / directing / acting
    public let playUrl: String?            // currently always returned as null by the server
    public let firstFrame: String?         // first-frame image URL, may be null (produced asynchronously)
    public let version: String             // world version identifier (diagnostics; drive interaction UI from `mode`)
    public let aspectRatio: String?        // "9:16" / "16:9"; only set for acting worlds, nil otherwise
}

// Control API returns (SDK output, not Codable; status is the external enum OysterTravelStatus)
public struct OysterTravelStateData: Equatable, Sendable { public let encryptedTravelId: String; public let status: OysterTravelStatus }
public struct OysterRewindTravelData: Equatable, Sendable { public let encryptedTravelId: String; public let status: OysterTravelStatus; public let resumedAtSec: TimeInterval }
public struct OysterEndTravelData: Equatable, Sendable { public let encryptedTravelId: String; public let status: OysterTravelStatus; public let endedAt: Date; public let duration: TimeInterval }
public struct OysterSendInstructData: Equatable, Sendable { public let encryptedTravelId: String; public let content: String; public let accepted: Bool }

// Adventure-mode control command (strongly typed enums; values aligned with the internal WorldControlParams)
public struct OysterAdventureCommand: Equatable, Sendable {
    public enum Translation: String { case none, front, back, left, right, frontLeft, frontRight, backLeft, backRight }
    public enum Rotation: String { case none, mouseUp, mouseDown, mouseLeft, mouseRight, mouseUpLeft, mouseUpRight, mouseDownLeft, mouseDownRight }
    public enum Interaction: String { case none, jump, attack, crouch, sprint }
    public let translation: Translation   // movement for this command; submit every frame while held, then stop
    public let rotation: Rotation          // view rotation for this command; submit every frame while held, then stop
    public let interaction: Interaction    // one-shot action for this command; submit once
    public init(translation: Translation = .none, rotation: Rotation = .none, interaction: Interaction = .none)
    public init(_ params: WorldControlParams)   // convenient bridge from the default control WorldControlParams (consistent rawValue)
}

// Unified error (§9)
public struct OysterSDKError: Error {
    public let code: Int
    public let raw: Any?                  // the SDK's internal raw error info; structure is not guaranteed stable, for logging/diagnostics only
    public var kind: OysterErrorKind      // typed view of the numeric code, for exhaustive switch (computed property)
    public init(code: Int, raw: Any? = nil)
    // Error code constants are in the nested OysterSDKError.Code (e.g. .notInitialized = 100001)
}

// Typed semantic view of error codes: local codes are named cases; server-side 4xxxxx/5xxxxx converge to .server(code:)
public enum OysterErrorKind: Equatable, Sendable {
    case notInitialized, tokenMissing, tokenInvalid, noActiveTravel, invalidState, sendCommandInDirecting
    case concurrentTravel, realtimeConnectFailed, realtimeJoinTimeout, firstFrameTimeout
    case channelNotReady, callbackTimeout, localNetwork, responseDecodeFailed
    case streamAutoEnd, proxyOrUnrecognized, featureGateDisabled
    case server(code: Int), unknown(code: Int)
}
  • Catatan: rawValues perintah menggunakan lower camelCase (misalnya front / mouseLeft / jump); nilai enum di atas adalah yang otoritatif.
  • Catatan: mode secara eksternal adalah adventure (wander) / directing (cerita) / acting (bermain peran); definisi OysterModeValue adalah yang otoritatif.
  • Catatan: aspectRatio adalah string terbuka (saat ini 9:16 / 16:9, lebih banyak mungkin ditambahkan). Uraikan sebagai width:height dan bandingkan rasionya alih-alih mencocokkan nilai yang diketahui.

9. Kode Error

SDK melaporkan error secara seragam sebagai OysterSDKError, dan tipenya selalu dibedakan oleh code — jangan menilai tipe dari "jalur mana error berasal": code yang sama dapat dilempar oleh metode bisnis (async throws) atau dimunculkan melalui .error dari events. Untuk pencocokan bertipe, gunakan error.kind (lihat §8 OysterErrorKind). Kode error: server 4xxxxx/5xxxxx, client-local 1xxxxx.

Kode Error Server (umum)

code

Arti

Penanganan yang disarankan

400000

Parameter tidak valid (nilai enum tidak valid, dll.)

Periksa parameter permintaan atau versi SDK

401010

Kredensial pengalaman (ticket) tidak valid atau kedaluwarsa

Minta server menerbitkan ulang kredensial

401011

Kredensial pengalaman (ticket) sudah digunakan

Kredensial sekali pakai; terbitkan ulang

403001

World tidak ada, telah dihapus, atau bukan milik pengembang saat ini (termasuk world yang dihapus setelah kredensial diterbitkan)

Pilih World yang valid lagi

403002

Status World belum siap

Tunggu hingga world siap sebelum memulai

403003

API hanya mengizinkan API Key utama

Key sementara tidak dapat digunakan untuk API ini

403004

Konten input ditolak oleh moderasi konten; berlaku untuk instruksi teks sendInstruct

Ubah input dan coba lagi

403007

Spesifikasi layanan yang diminta tidak diaktifkan

Jangan mencoba lagi seolah-olah kapasitas penuh; beralih ke spesifikasi yang diaktifkan (biasanya: akun tidak memiliki spesifikasi acting)

403008

Konfigurasi kapasitas sementara tidak tersedia

Coba lagi nanti

404000

Sumber daya tidak ada (kepemilikan world/wander atau tidak ada artefak)

Verifikasi ID / status

409000

Permintaan bertentangan dengan status sumber daya saat ini

Periksa status experience

429001

Batas konkurensi tercapai untuk spesifikasi ini

Coba lagi setelah sesi yang ada berakhir (jangan bingung dengan 500001)

429002

Kapasitas tersedia tidak mencukupi

Coba lagi nanti

500000

Kesalahan sistem internal

Coba lagi nanti / laporkan

500001

Alokasi sumber daya inferensi atau kegagalan layanan internal

Coba lagi nanti

Kode Error Client-Local

code

Arti

SDK mengakhiri sesi secara otomatis

Penanganan yang disarankan

100001

Dipanggil sebelum inisialisasi SDK; juga mencakup initialize yang tidak berlaku karena apiHost/model kosong atau tidak valid

Tidak (dilontarkan secara sinkron, menolak panggilan ini)

initialize terlebih dahulu, dan verifikasi bahwa apiHost dan model telah diatur dengan benar

101001

Token autentikasi HTTP tidak diinjeksikan

Tidak

coba lagi setelah updateToken

101002

Token autentikasi HTTP tidak valid / ditolak

Tidak

tukar ulang token lalu coba lagi updateToken

103001

Tidak ada experience aktif saat ini

Tidak (menolak panggilan ini)

createTravel + start terlebih dahulu

103002

Status/versi saat ini tidak mengizinkan operasi ini

Tidak (menolak panggilan ini)

periksa status experience / version

103003

Ketidakcocokan mode (misalnya sendCommand dipanggil pada world non-adventure)

Tidak (menolak panggilan ini)

pilih API yang tepat berdasarkan mode, lihat tabel kapabilitas §2

103004

Create/start experience secara konkuren

Tidak (dilontarkan secara sinkron)

serialisasikan panggilan, end sesi lama terlebih dahulu

105001

Koneksi real-time gagal

Ya

akhiri dan mulai ulang

105002

Batas waktu bergabung real-time

Ya

akhiri dan mulai ulang

105003

Batas waktu menunggu frame video pertama

Ya

akhiri dan mulai ulang

105004

Channel real-time belum siap / pengiriman gagal

tergantung (kegagalan pengiriman aktif; heartbeat hanya untuk pelaporan)

kirim setelah running

105005

Batas waktu callback (default 30s)

Tidak

coba lagi, dan tingkatkan callbackTimeoutMs jika diperlukan

105006

Tidak ada stream setelah bergabung; SDK mengakhiri experience secara otomatis

Ya

akhiri dan mulai ulang

106001

Kesalahan jaringan lokal

Tidak

dapat dicoba ulang

106002

Gagal mengurai respons

tergantung

tingkatkan versi SDK / laporkan

106003

Tidak ada kode error yang dapat dikenali / kode error string proxy

Tidak

dapat dicoba ulang

108001

Dinonaktifkan dari jarak jauh oleh sakelar fitur server (penonaktifan penuh atau versi terlalu rendah; alasan di raw)

Ya

Ikuti alasan di raw; pandu pengguna untuk meningkatkan saat versi terlalu rendah

Cara menilai fatalitas: Fatalitas tidak lagi diekspos sebagai field boolean (OysterSDKError tidak memiliki isFatal). Secara semantik, "fatal" secara khusus berarti apakah SDK secara aktif mengakhiri sesi (memutuskan RTC, melepaskan seluruh sesi) —
  • Error yang mengakhiri sesi secara otomatis (misalnya 105001/105002/105003/105006/108001): host menyadari hal ini dari status terminal state-machine (status → failed, dimunculkan melalui .statusChanged dari events), dan kembali ke layar sebelum "mulai pengalaman" sesuai dengan itu, tanpa perlu menilai fatalitasnya sendiri.
  • Error penolakan panggilan (100001/103001/103002/103003/103004): dilempar secara sinkron / panggilan ditolak saat Anda secara aktif memanggil; error ini tidak mengakhiri sesi.
  • Error non-fatal lainnya (misalnya 101001/101002/105005/106001/106003): error ini tidak mengakhiri sesi; coba lagi seperti yang disarankan atau lanjutkan setelah menyuntikkan ulang token.
106001 mungkin memiliki dua penyebab: error jaringan lokal, atau apiHost yang salah. Jika mencoba lagi tidak memulihkan permintaan, periksa apakah apiHost dikonfigurasi dengan benar.
106003 yang muncul setelah Anda mengatur model biasanya berarti nama/versi model salah atau tidak diaktifkan untuk akun Anda: periksa model bersama dengan apiHost dan token, yang semuanya harus berasal dari akun dan wilayah yang sama — gateway menolak ketidakcocokan dengan AccessDenied, dinormalisasi ke kode ini.
Pembuatan Gambar
  • FAQ
Video Generation
Model Dunia
Audio
  • Pembuatan audio
Realtime API
Penyematan Teks
TokenPlan
Model production