HappyOyster Referência da API do SDK do iOS
O HappyOyster O ponto de entrada do iOS SDK é o singleton em nível de processo HappyOysterEngine.shared . Os métodos de negócios são todos lançamentos assíncronos, lançando OysterSDKError em caso de falha. Uma única experiência é realizada por um identificador OysterTravel, abrangendo três modos – aventura, direção e atuação – no UIKit e no SwiftUI.
- Este documento é a descrição de recurso externo + referência de interface para o Happy Oyster iOS SDK: ele explica os parâmetros, tempo, uso e exemplos curtos de cada tipo e método público, um por um.
- Para o fluxo de integração completo (configuração do projeto, configuração de dependências, coordenação do lado do servidor, execução ponta a ponta), consulte o exemplo de documentação do projeto; não é repetido aqui.
1. Conceitos Básicos
Alguns termos primeiro, para facilitar a leitura do resto.
<table><colgroup><col style={{ width: "18.518519%" }} /><col style={{ width: "81.481481%" }} /></colgroup><thead><tr><th> Conceito </th><th> Descrição </th></tr></thead><tbody><tr><td> ficha </td><td> Token de autenticação HTTP (QwenCloud chave de API temporária). Seu servidor troca-o através do QwenCloud API e entrega-a; é injetado através <code>atualizarToken(_:)</code>, e o SDK o transporta como um portador HTTP ao solicitar o gateway. O SDK mantém apenas o mais recente, não o persiste nem o atualiza; depois de expirar, você troca novamente e injeta novamente. </td></tr><tr><td> bilhete </td><td> Credencial de teste único. Seu servidor troca através da plataforma aberta <code>obter credencial de viagem</code> (prefixo <code>tk_</code>, válido para 30 minutos, uso único), usado como <code>criarViagem(bilhete:)</code> argumento; torna-se inválido quando a experiência termina ou expira e não pode ser reutilizado. </td></tr><tr><td> Mundo </td><td> Um mundo de IA, contendo personagens e cenas. Criado e gerenciado pelo seu servidor; o SDK não está envolvido. </td></tr><tr><td> Viajar </td><td> Uma única experiência em tempo real, correspondente a um <code>OysterTravel</code> manusear. Uso único; invalidado quando um estado terminal é alcançado, exigindo um novo <code>criarViagem</code> através do motor. </td></tr><tr><td> Status da sessão </td><td> <code>OysterTravelStatus</code>: <code>inativo → preparar → em execução → pausando → pausado</code> (pausado retorna para <code>preparar → correr</code> via reconexão), mais dois estados terminais <code>terminou</code> / <code>falhou</code> (ver §7). </td></tr><tr><td> Modo </td><td> <code>aventura</code> (comandos de direção/visão/ação), <code>dirigir</code> (enredo baseado em texto), ou <code>atuando</code> (desempenho de personagem baseado em texto). O <code>modo</code> retornado por <code>início()</code> determina a IU de acordo; consulte §2 para saber o que cada modo suporta. </td></tr></tbody></table>
2. Visão geral
Depois de integrar este SDK, seu aplicativo pode entrar em "mundos" gerados em tempo real pela IA e ter uma experiência de vídeo interativa em tempo real em três modos — aventura, direção ou atuação: iniciar a experiência → reprodução em tempo real → interação em tempo real → controle de processo (pausar/retomar/retroceder/finalizar) → retornos de chamada de status e erro.
Cada modo oferece suporte a um conjunto diferente de recursos. Direcione sua IU a partir do mode retornado por start():
<table><colgroup><col style={{ width: "40%" }} /><col style={{ width: "20%" }} /><col style={{ width: "20%" }} /><col style={{ width: "20%" }} /></colgroup><thead><tr><th> Capacidade </th><th> <code>aventura</code> </th><th> <code>dirigir</code> </th><th> <code>atuando</code> </th></tr></thead><tbody><tr><td> <code>enviarComando</code> (direção/visão/ação) </td><td> Sim </td><td> Não </td><td> Não </td></tr><tr><td> <code>enviarInstruir</code> (instrução de texto) </td><td> Não </td><td> Sim </td><td> Sim </td></tr><tr><td> <code>pausa()</code> / <code>currículo()</code> </td><td> Não </td><td> Sim </td><td> Sim </td></tr><tr><td> <code>retroceder (para segundos:)</code> </td><td> Não </td><td> Sim </td><td> Não – oculte o ponto de entrada </td></tr><tr><td> <code>fim()</code> </td><td> Sim </td><td> Sim </td><td> Sim </td></tr><tr><td> <code>iniciar(maxExperienceTimeSec:)</code> </td><td> Aplicado </td><td> Ignorado </td><td> Ignorado </td></tr></tbody></table>
acting os mundos priorizam o retrato: start() retorna um aspectRatio (9:16 / 16:9) para a sessão. Use-o para escolher a orientação do player e o tamanho do contêiner antes de puxar o fluxo (consulte §8).
import HappyOysterSDK; os principais pontos de entrada são de dois tipos.
HappyOysterEngine — o ponto de entrada da orquestração, um singleton em nível de processo HappyOysterEngine.shared.
<table><colgroup><col style={{ width: "40%" }} /><col style={{ width: "60%" }} /></colgroup><thead><tr><th> Método/Propriedade </th><th> Descrição </th></tr></thead><tbody><tr><td> <code>inicializar(configuração:)</code> </td><td> Inicialize o tempo de execução e registre automaticamente o mecanismo em tempo real (uma vez, antes <code>criarViagem</code>) </td></tr><tr><td> <code>atualizarToken(_:)</code> </td><td> Injetar/atualizar o token de autenticação HTTP </td></tr><tr><td> <code>criarViagem(bilhete:)</code> </td><td> Crie um identificador de sessão com uma credencial única </td></tr><tr><td> <code>limpeza()</code> </td><td> Liberar recursos (pode <code>inicializar</code> novamente) </td></tr><tr><td> <code>versão</code> </td><td> A versão atual do SDK </td></tr></tbody></table>
OysterTravel — o identificador de sessão para uma experiência, criado por createTravel(ticket:).
<table><colgroup><col style={{ width: "40%" }} /><col style={{ width: "60%" }} /></colgroup><thead><tr><th> Método/Propriedade </th><th> Descrição </th></tr></thead><tbody><tr><td> <code>vídeoVer</code> / <code>OysterVideoView(viagem:)</code> </td><td> Visualização de reprodução (UIKit/SwiftUI) </td></tr><tr><td> <code>eventos</code> / <code>estado</code> </td><td> Mudança de status + fluxo de push de erro / status atual observável </td></tr><tr><td> <code>início()</code> / <code>iniciar(maxExperienceTimeSec:)</code> </td><td> Conecte-se e jogue (opcionalmente, solicite a duração máxima de uma experiência de aventura) </td></tr><tr><td> <code>enviarInstruct(conteúdo:)</code> </td><td> Instruções de texto no modo de direção </td></tr><tr><td> <code>enviarComando(_:)</code> / <code>flushCommands()</code> </td><td> Controle do modo aventura </td></tr><tr><td> <code>pausa()</code> / <code>currículo()</code> </td><td> Pausar/retomar (<code>dirigir</code> e <code>atuando</code>) </td></tr><tr><td> <code>retroceder (para segundos:)</code> </td><td> Retroceder (apenas pausado) </td></tr><tr><td> <code>fim()</code> </td><td> Fim (idempotente, certifique-se de chamá-lo) </td></tr><tr><td> <code>pausaLocalAudioCapture()</code> / <code>retomarLocalAudioCapture()</code> </td><td> Ceder/restaurar temporariamente o microfone </td></tr></tbody></table>
Existem também algumas exportações auxiliares: OysterLog por assumir o registro interno do SDK; HappyOysterEngine.version para ler a versão; OysterVideoView é a visualização de reprodução do SwiftUI (equivalente a videoView). O uso é descrito posteriormente.
Ciclo de vida: inicializar → injetar token → criar sessão → montar vídeo + inscrever-se em eventos → iniciar a reprodução → interagir → finalizar. O SDK não é responsável pela criação ou gerenciamento de mundos (feito pelo seu servidor), nem expõe detalhes de comunicação de baixo nível em tempo real.
3. Início rápido
Um fluxo completo da inicialização ao final, com comentários passo a passo. Os detalhes de cada API estão em §6.
import HappyOysterSDK
// 1) Initialize (as early as possible after app launch, once before createTravel)
let engine = HappyOysterEngine.shared
engine.initialize(config: OysterConfig(
apiHost: "maas.qwencloudapi.com",// API Host (maas.qwencloudapi.com)
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 (QwenCloud 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()
Para configuração do projeto, configuração de dependência (incluindo o adaptador AliRTC necessário e o binário do fornecedor), coordenação do lado do servidor e uma execução ponta a ponta, consulte a documentação do projeto de amostra.
4. Requisitos
<table><colgroup><col style={{ width: "25%" }} /><col style={{ width: "75%" }} /></colgroup><thead><tr><th> Artigo </th><th> Requisito </th></tr></thead><tbody><tr><td> SO mínimo </td><td> iOS 15.0+ (todos os tipos públicos estão marcados <code>@disponível(iOS 15.0, *)</code>) </td></tr><tr><td> Idioma </td><td> Rápido (<code>assíncrono/aguardar</code>) </td></tr><tr><td> Simultaneidade </td><td> Acesso ao thread principal (os tipos de entrada são marcados <code>@MainActor</code>) </td></tr><tr><td> Rede </td><td> É necessário acesso à rede pública </td></tr><tr><td> Permissões </td><td> <code>Informações.plist</code> deve incluir <code>NSMicrofoneUsoDescrição</code> (veja abaixo) </td></tr></tbody></table>
Permissão de microfone: uma experiência de vídeo interativa em tempo real precisa de um canal de áudio/vídeo bidirecional (uplink + downlink) em tempo real, para que o SDK ocupe o microfone local durante a execução — isso não é gravação. Info.plist deve fornecer NSMicrophoneUsageDescription, caso contrário, iniciar a captura em tempo real travará. Quando você precisar de acesso exclusivo ao microfone (por exemplo, reconhecimento de fala), use pauseLocalAudioCapture() / resumeLocalAudioCapture() ceder e restaurá-lo temporariamente (ver §6.2).
5. Integração e Autenticação
5.1 Integração e Dependências
import HappyOysterSDK é tudo que você precisa no nível do código. O SDK é distribuído como um binário pré-compilado (xcframework) por meio de subespecificações do CocoaPods, publicado no CocoaPods Trunk público — declare as dependências em seu Podfile:
# 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 é obrigatório sempre que você entra HappyOysterSDK/StreamAliRTC: se estiver faltando, o SDK volta silenciosamente para Loopback — ele pode se conectar e alcançar running, mas mostra uma tela preta sem erros.
A configuração do projeto e o fluxo de inicialização ponta a ponta são gerenciados pelo projeto de amostra; consulte o exemplo de documentação do projeto. Este documento se concentra na própria interface.
5.2 Modelo de autenticação
O SDK não obtém nem atualiza tokens, mantendo as coisas leves. A autenticação possui duas camadas e o integrador gerencia seus ciclos de vida:
- Token de autenticação HTTP (QwenCloud chave de API temporária): seu servidor a troca por meio do QwenCloud API e entrega-a; é injetado através
updateToken(_:). Alguns serviços internos do SDK chamam o QwenCloud gateway diretamente, carregando esse token para autenticação — portanto, deve ser uma chave API temporária emitida por QwenCloud, não um token do seu próprio serviço comercial. O SDK mantém apenas o mais recente, não o persiste nem o atualiza; depois de expirar, você troca novamente e injeta novamente. - Credencial de avaliação única
ticket: Seu servidor troca através da plataforma abertaget-travel-credential(prefixotk_, válido por 30 minutos, uso único), usado comocreateTravel(ticket:)argumento; torna-se inválido quando a experiência termina (normal ou anormalmente) ou expira e não pode ser reutilizado.
AK, chaves de assinatura e outras credenciais de alto privilégio existem apenas no seu servidor; o SDK do cliente nunca os toca. O que o cliente recebe é sempre uma chave de API temporária de curta duração.
Feature Gate (troca remota/atualização forçada): O servidor pode desabilitar remotamente o SDK ou definir uma versão mínima suportada. Enquanto desativadas, as chamadas bloqueadas (start / pause / resume / rewind / sendInstruct / sendCommand) são rejeitados com 108001 e qualquer experiência a bordo será encerrada pelo SDK (ver §9). OysterSDKError.raw carrega uma razão legível por humanos; solicitar que o usuário atualize quando a versão for muito baixa.
Tratamento da expiração do token: depois que o token de autenticação HTTP acima expirar, solicite imediatamente um novo token do seu servidor e injete-o via updateToken(_:). Existem dois locais onde você precisa determinar se o token expirou:
- Ao chamar APIs como
engine.createTraveloutravel.start, trate o erro e verifique os tipos de erro de token expirado/inválido (101001/101002); após injetar um novo token, chame novamente a API correspondente. - Ao ouvir o
.errorevento deOysterTravelEvent, verifique os tipos de erro de token expirado/inválido, solicite novamente o token e injete-o.
6. Referência da API
Existem dois tipos de entrada, ambos @MainActor e @available(iOS 15.0, *). Os métodos de negócios são async throws e jogue OysterSDKError em caso de falha; métodos com valores de retorno estão todos marcados @discardableResult.
6.1 HappyOysterEngine
Um singleton no nível do processo, o ponto de entrada da orquestração. init não é público - sempre use HappyOysterEngine.shared; não instancie você mesmo (o mecanismo de tempo real subjacente também é um processo singleton).
@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:)
- Objetivo: Inicializar o tempo de execução e registrar automaticamente o mecanismo em tempo real (nenhum registro manual necessário pelo host).
- Parâmetros:
config.apiHosté o QwenCloud URL do gateway, não do seu servidor comercial, e é obrigatório; em ambientes de pré-lançamento/teste, você deve passar explicitamente o gateway correspondente, caso contrário, as solicitações falharão (por exemplo,106001, o domínio não pode ser resolvido).config.modelé o nome do modelo com versão habilitado para sua conta e também é obrigatório sem padrão — o Happy Oyster é dividido em submodelos por modo, portanto, o SDK não pode inferir qual deles usar. Ambos devem pertencer à mesma conta do token injetado. Outros campos estão em §8OysterConfig. - Um modelo serve um
mode: cada modelo por modo é sua própria rota de gateway, então uma únicainitializesó serve mundos daquelemode. Se seu aplicativo oferece mundos em vários modos, basta ligarinitialize()novamente com o modelo correspondente antes de entrar em um mundo de modo diferente - enquanto ocioso, a configuração mais recente vence, nãocleanup()é necessário e o token injetado é mantido; enquanto uma viagem está em vôo, a chamada é ignorada, entãoend()primeiro. Um modelo que não corresponde ao mundomodeé rejeitado pelo gateway comAccessDenied, normalizado para106003. - Quando usar: Ligue uma vez antes
createTravel, o mais cedo possível após o lançamento do aplicativo. - Retornos:
Bool- quer oconfigvocê passou entrou em vigor. Éfalseem dois casos: a configuração é inválida (apiHost/modelem branco ou não formando um URL de gateway válido) ou uma viagem está em andamento, então a chamada foi ignorada. Em ambos os casos, o tempo de execução permanece inalterado. - Nota: Enquanto ocioso, chamá-lo novamente reconfigura o tempo de execução com a nova configuração (alternando
apiHost/modelnão precisacleanup(), e o token injetado é mantido); é um ambiente autônomo com um aviso apenas enquanto uma viagem está em vôo, entãoend()primeiro. Uma configuração inválida deixa o tempo de execução inalterado. Não useisReadypara dizer se um re-initializebem-sucedido — se foi rejeitado, a configuração anterior ainda está em vigor eisReadypermanecetrue;isReadyresponde "o mecanismo pode ser usado agora", o valor de retorno responde "a configuração que acabei de passar entrou em vigor".
OysterLog
- Objetivo: Configurar e assumir o logging interno do SDK, imprimindo-o em seu próprio módulo de logging. Fornece
setMinimumLevel(_:)para definir o nível esetHandler(_:)para saída personalizada (veja o exemplo §3).
updateToken(_:)
- Objetivo: Injetar/atualizar o token de autenticação HTTP (QwenCloud chave de API temporária, §5.2).
- Quando usar: Depois
initialize, exigível a qualquer momento; troque novamente e ligue novamente após o token expirar ou após receber um erro relacionado à autenticação (101001/101002). - Observação: Um ambiente autônomo com um aviso quando não
initialized.
createTravel(ticket:)
- Objetivo: Criar um identificador de sessão único com um único
ticket. - Parâmetro:
ticketé uma credencial única; uma vez criado, é considerado ocupado para esta experiência. - Quando usar: Ligue antes de cada nova experiência; o identificador retornado ainda não está conectado e você deve ligar
travel.start()depois. O vídeo é retirado do identificador retornado (ver §6.2). - Nota (síncrono
throws): arremessos100001se nãoinitialized; lança103004se for chamado novamente antes da viagem anterior terend()ed (cada mecanismo permite apenas uma viagem ativa por vez).
cleanup()
- Objetivo: Liberar recursos do SDK (terminar a viagem ativa, configuração de tempo de execução, token).
- Quando usar: ao sair totalmente do SDK ou precisar alterar o
config. - Nota:
async- internamente é determinísticaend()É primeiro o deslocamento ativo atual e, em seguida, reduz o tempo de execução, sem deixar o fogo e o esquecimento. Após a liberação você podeinitializenovamente.
6.2 OysterTravel
O identificador de sessão criado por createTravel; uso único, invalidado quando um estado terminal (end / fim do servidor / falha) é atingido, exigindo uma nova createTravel através do motor. É também um ObservableObject (@Published status, pode conduzir diretamente o 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
}
Visualização do vídeo videoView / OysterVideoView(travel:)
- Objetivo: A entrada de renderização para a imagem remota — "o SDK fornece a visualização, o host a coloca". Para UIKit, pegue
travel.videoView; para SwiftUI, useOysterVideoView(travel:). - Quando usar: Disponível assim que o handle é criado (o acesso repetido retorna a mesma visualização); monte-o em qualquer hierarquia e, quando o mecanismo estiver pronto, ele renderizará automaticamente. Montagem antes ou depois
start()ambos funcionam, sem tela preta. - Observação: Quando a sessão termina, o SDK libera automaticamente a vinculação de renderização; remova a visualização da hierarquia conforme necessário.
Eventos e status events / status / isEnded
- Objetivo:
eventsé o fluxo push de alterações de status + erros;statusé o estado externo atual observável;isEndedé um sinalizador legível de forma síncrona para o estado terminal. - Quando usar: Recomenda-se começar a consumir
eventsantesstart(), para evitar a perda de status iniciais. - Nota: Cada acesso a
eventsretorna um fluxo independente, com suporte para múltiplas assinaturas; cancelar inscrição = encerrando ofor awaititeração (ou destruindo o mantidoTask). No SwiftUI você pode observar diretamentestatuscom@StateObject/@ObservedObject(erros ainda aparecemevents). Veja §7.
start() / start(maxExperienceTimeSec:)
- Objetivo: Usar o
ticketcapturado no momento da criação para trocar pela configuração de viagem + RTC e conectar para jogar. Se for bem-sucedido, o SDK estabelece automaticamente a conexão em tempo real e inicia a pesquisa de status interna, revelando o status por meio deevents. - Parâmetro:
maxExperienceTimeSec(opcional) — solicita a duração máxima (em segundos) desta experiência de aventura. O valor é enviado ao servidor como está; os valores permitidos, a duração efetiva real e o tempo de término automático são todos decididos pelo servidor — o SDK não realiza nenhuma validação local. Passandonil(ou chamando o sem parâmetrosstart()) usa a duração padrão do servidor. O modo Direção ignora este parâmetro. Quando o tempo acabar, o servidor encerra a experiência e o host recebe oendedestado terminal viaevents(o mesmo que uma extremidade do lado do servidor, consulte §7). - Retornos:
OysterStartTravelData(mode/version/encryptedTravelId, etc., consulte §8), que determina a IU de interação. - Erros:
401010/401011(credencial inválida/usada),403002(o mundo não está pronto),403007(especificação de serviço não habilitada, por exemplo, atuação),429001/429002(limite de simultaneidade/capacidade esgotada),500001(falha de recurso/servidor),103004(início simultâneo). - O
ticketmundomodedeve corresponder aomodelpassou parainitialize(responsabilidade do chamador): com modelos por modo, cada modelo é sua própria rota de gateway estart()envia oticketpara a rota do modelo atualmente inicializado. O SDK não verifica e não pode verificar isso para você de antemão —modeé entregue pela resposta a esta mesmastart()ligar (OysterStartTravelData.mode); antes da chamada, o SDK contém apenas um opacotickete um nome de modelo, sem modo para comparar, e inferir o modo a partir do nome do modelo seria adivinhar um esquema de nomenclatura de propriedade do servidor, o que o SDK não faz. Então: **antes de entrar em um mundo de modo diferente, ligueinitialize()novamente com o modelo correspondente ** (enquanto inativo, a configuração mais recente vence - nãocleanup()necessário e o token é mantido; enquanto uma viagem está em vôo, a chamada é ignorada, entãoend()isso primeiro). Em uma incompatibilidade issostart()falha no gateway; ao diagnosticar, primeiro verifique se a correntemodele omodedoticketos mundos pertencem um ao outro, então consulte os códigos de erro de credencial acima. - Nota (término automático em caso de ausência de transmissão): O servidor envia um "tempo limite sem transmissão" (padrão ~30s). Se, após a conexão, nenhum fluxo for recebido dentro dessa duração (nunca chega
running), o SDK encerra automaticamente a experiência, faz a transição parafailede superfícies105006através do.errordeevents(fatal; controle retornando à tela de pré-início, sem necessidade de cronometrar você mesmo).
pause() / resume()
- Objetivo: Pausar/retomar a experiência (idempotente).
- Quando usar: Suportado por
directingeactingmundos; não suportado poradventure. Use omoderetornado porstart()para decidir antecipadamente se deseja mostrar um botão de pausa.pauseexige que o estado atual sejarunning;resumerequerpaused. - Erros:
103001(sem experiência ativa),103002(estado/versão não permitido),103003(incompatibilidade de modo).
rewind(toSec:)
- Objetivo: retroceder até os segundos especificados. Em caso de sucesso, o SDK se reúne automaticamente ao rtcConfig original e retorna à reprodução.
- Quando usar: Só pode ser iniciado no
pausedestado, e somente pordirectingmundos —actingeadventurenão suporta retrocesso, portanto oculte o ponto de entrada de retrocesso nesses modos e não o chame. - Erros:
103001,103002.
end()
- Objetivo: Encerrar a experiência (idempotente, pode ser chamado repetidamente). Após uma chamada bem-sucedida ou uma saída anormal, o SDK desconecta automaticamente a conexão em tempo real, interrompe a sondagem e libera todos os recursos da sessão; o
ticketé invalidado ao mesmo tempo e o identificador entra em um estado terminal. - Quando usar / Nota: Se o usuário sai ativamente ou a experiência termina passivamente (expiração do cronômetro, recebimento
.ended/.failed, destruição de página), certifique-se de que um únicoend()é alcançado, caso contrário, os recursos remotos poderão não ser liberados imediatamente. Recomenda-se canalizar todos os caminhos de saída para o mesmo método de limpeza idempotente.
sendInstruct(content:) (modo de direção)
- Objetivo: enviar uma instrução de texto para conduzir o enredo.
- Quando usar: Modo Direção; enviado diretamente quando
running, armazenado em cache quandopausede reenviar com o primeiro quadro depois de voltar pararunningvia reconectar. - Erros:
103001,103002,103003(chamado no modo aventura),403004(moderação de conteúdo),404000(viagem não encontrada).
sendCommand(_:) / flushCommands() (modo aventura)
sendCommand: Enviar comandos de controle de direção/visão/ação (ver §8OysterAdventureCommand; atire e esqueça, sem retorno, não jogue). Efetivo apenas no modo aventura quandorunning. A entrada externa pode ser de alta frequência em cada quadro, e o SDK acelera internamente (amostragem de vitórias mais recentes, fusão de quadros na taxa de linha RTC); o host não precisa se limitar. Observe que a própria resposta do servidor aos comandos tem latência, portanto o tempo efetivo real não é fixo.flushCommands: Chamado no momento de “liberação de chave/liberação de entrada”, reenviando imediatamente o último comando já aguardando na fila; um ambiente totalmente autônomo quando não há nenhum comando pendente, não gerando nenhum novo comando.- Erros (todos surgidos através do
.errordeevents, nãothrows): nenhuma experiência ativa103001; chamado em modo de direção103003; canal em tempo real não está pronto/falha no envio105004.
pauseLocalAudioCapture() / resumeLocalAudioCapture()
- Objetivo: Liberar/restaurar temporariamente a ocupação do SDK de captura de microfone local.
- Quando usar: quando algo como reconhecimento de fala precisa de acesso exclusivo ao microfone,
pauseprimeiro eresumedepois.
7. Eventos e status
Os eventos estão anexados a OysterTravel.events e são o canal push ativo do SDK para você, usados para revelar situações não acionadas por suas próprias chamadas (por exemplo, problemas com a conexão em tempo real gerenciada internamente ou pesquisa de 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)
}
Existem dois estilos de consumo, escolha um:
- SwiftUI:
OysterTravelé umObservableObject; usar diretamente@StateObject/@ObservedObjectobservarstatuse conduzir a IU; erros ainda vêm deevents. - Imperativo / UIKit: em um
Task,for await event in travel.events { ... }eswitchacabou.statusChanged/.error; cancelar o retidoTaskquando terminar.
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 estados de processo + 2 estados terminais):
<table><colgroup><col style={{ width: "16.666667%" }} /><col style={{ width: "50%" }} /><col style={{ width: "33.333333%" }} /></colgroup><thead><tr><th> Estado </th><th> Descrição </th><th> Manuseio típico </th></tr></thead><tbody><tr><td> <code>ocioso</code> </td><td> depois de criar, antes de começar </td><td> — </td></tr><tr><td> <code>preparar</code> </td><td> conectar/reconectar (conectar/reconectar internamente) </td><td> mostrar dica de conexão/reconexão </td></tr><tr><td> <code>correndo</code> </td><td> pronto para transmissão, interativo (reprodução interna) </td><td> mostrar imagem e controles </td></tr><tr><td> <code>pausando</code> </td><td> pausa aceita, aguardando confirmação do servidor </td><td> mostrar "pausando…" </td></tr><tr><td> <code>pausado</code> </td><td> pausado (confirmado) </td><td> mostrar estado de pausa (somente <code>dirigir</code> mostra a entrada de retrocesso) </td></tr><tr><td> <code>terminou</code> </td><td> finalizado (extremidade ativa ou extremidade do lado do servidor). Terminais </td><td> encerrar e fechar a página </td></tr><tr><td> <code>falhou</code> </td><td> falhou. Terminais </td><td> mostrar erro e finalizar </td></tr></tbody></table>
Depois de entrar ended / failed, a sessão foi encerrada e todas as operações da sessão (pause/resume/sendCommand…) não terão mais efeito. Os retornos de chamada podem ser acionados no thread principal, para que você possa atualizar a IU diretamente.
Durante a pesquisa de status interna, o SDK relata automaticamente as pulsações do estado de pull/reprodução do cliente (conectando/reproduzindo/pausado/reconectando/desconectado), para que o servidor possa distinguir os estados do lado do fluxo dos do lado do cliente — comportamento puramente interno do SDK, do qual o host não precisa estar ciente ou do qual participar.
8. Modelos de dados
Todos os tipos públicos estão marcados @available(iOS 15.0, *). Os valores/parâmetros de retorno abaixo são saídas do SDK, construídas internamente com tipos Swift nativos (Date / TimeInterval / OysterTravelStatus); eles não Codable e não exponha detalhes do fio (snake_case) — a decodificação do fio acontece dentro do SDK.
// Configuration
public struct OysterConfig: Sendable {
public let apiHost: String // QwenCloud 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)
}
- Nota: os valores brutos do comando são camelCase inferiores (por exemplo
front/mouseLeft/jump); os valores enum acima são oficiais. - Nota:
modeé externamenteadventure(wander) /directing(história) /acting(representação de papéis); oOysterModeValuedefinição é oficial. - Nota:
aspectRatioé uma string aberta (atualmente9:16/16:9, mais podem ser adicionados). Analise-o comowidth:heighte compare a proporção em vez de combinar valores conhecidos.
9. Códigos de erro
O SDK relata erros uniformemente como OysterSDKError, e o tipo é sempre distinguido por code — não julgue o tipo por "de qual caminho veio o erro": o mesmo code pode ser lançado por um método de negócios (async throws) ou apareceu através do .error de events. Para correspondência digitada, use error.kind (ver §8 OysterErrorKind).
Códigos de erro: servidor 4xxxxx/5xxxxx, cliente-local 1xxxxx.
Códigos de erro do servidor (comuns)
<table><colgroup><col style={{ width: "25%" }} /><col style={{ width: "50%" }} /><col style={{ width: "25%" }} /></colgroup><thead><tr><th> código </th><th> Significado </th><th> Manuseio sugerido </th></tr></thead><tbody><tr><td> <code>400000</code> </td><td> Parâmetros inválidos (valor enum inválido, etc.) </td><td> Verifique os parâmetros da solicitação ou a versão do SDK </td></tr><tr><td> <code>401010</code> </td><td> Credencial de experiência (<code>bilhete</code>) inválido ou expirado </td><td> Faça com que o servidor emita novamente a credencial </td></tr><tr><td> <code>401011</code> </td><td> Credencial de experiência (<code>bilhete</code>) já usado </td><td> Credencial de uso único; reeditar </td></tr><tr><td> <code>403001</code> </td><td> O mundo não existe, foi excluído ou não pertence ao desenvolvedor atual (incluindo um mundo excluído após a emissão da credencial) </td><td> Escolha um mundo válido novamente </td></tr><tr><td> <code>403002</code> </td><td> Estado mundial não está pronto </td><td> Espere o mundo estar pronto antes de começar </td></tr><tr><td> <code>403003</code> </td><td> A API permite apenas a chave de API primária </td><td> Uma chave temporária não pode ser usada para esta API </td></tr><tr><td> <code>403004</code> </td><td> Conteúdo de entrada rejeitado pela moderação de conteúdo; aplica-se a <code>enviarInstruir</code> instruções de texto </td><td> Altere a entrada e tente novamente </td></tr><tr><td> <code>403007</code> </td><td> A especificação de serviço solicitada não está habilitada </td><td> Não tente novamente como se a capacidade estivesse cheia; mudar para uma especificação habilitada (normalmente: a conta não tem especificação de atuação) </td></tr><tr><td> <code>403008</code> </td><td> Configuração de capacidade temporariamente indisponível </td><td> Tente novamente mais tarde </td></tr><tr><td> <code>404000</code> </td><td> O recurso não existe (mundo/wander propriedade ou nenhum artefato) </td><td> Verifique ID/status </td></tr><tr><td> <code>409000</code> </td><td> A solicitação está em conflito com o estado atual do recurso </td><td> Verifique o estado da experiência </td></tr><tr><td> <code>429001</code> </td><td> Limite de simultaneidade atingido para esta especificação </td><td> Tente novamente após o término de uma sessão existente (não confunda com <code>500001</code>) </td></tr><tr><td> <code>429002</code> </td><td> Capacidade disponível insuficiente </td><td> Tente novamente mais tarde </td></tr><tr><td> <code>500000</code> </td><td> Erro interno do sistema </td><td> Tente novamente mais tarde/denuncie </td></tr><tr><td> <code>500001</code> </td><td> Alocação de recursos de inferência ou falha de serviço interno </td><td> Tente novamente mais tarde </td></tr></tbody></table>
Códigos de erro locais do cliente
<table><colgroup><col style={{ width: "16.666667%" }} /><col style={{ width: "33.333333%" }} /><col style={{ width: "16.666667%" }} /><col style={{ width: "33.333333%" }} /></colgroup><thead><tr><th> código </th><th> Significado </th><th> SDK encerra sessão automaticamente </th><th> Manuseio sugerido </th></tr></thead><tbody><tr><td> <code>100001</code> </td><td> Chamado antes da inicialização do SDK; também abrange uma <code>inicializar</code> que não entrou em vigor porque <code>apiHost</code>/<code>modelo</code> estava em branco ou inválido </td><td> Não (lançado de forma síncrona, rejeitando esta chamada) </td><td> <code>inicializar</code> primeiro e verifique ambos <code>apiHost</code> e <code>modelo</code> estão configurados corretamente </td></tr><tr><td> <code>101001</code> </td><td> Token de autenticação HTTP não injetado </td><td> Não </td><td> tente novamente depois <code>atualizarToken</code> </td></tr><tr><td> <code>101002</code> </td><td> Token de autenticação HTTP inválido/rejeitado </td><td> Não </td><td> troque novamente o token e tente novamente <code>atualizarToken</code> </td></tr><tr><td> <code>103001</code> </td><td> Nenhuma experiência ativa atualmente </td><td> Não (rejeitando esta chamada) </td><td> <code>criarViagem</code> + <code>começar</code> primeiro </td></tr><tr><td> <code>103002</code> </td><td> O estado/versão atual não permite esta operação </td><td> Não (rejeitando esta chamada) </td><td> verifique o estado da experiência / <code>versão</code> </td></tr><tr><td> <code>103003</code> </td><td> Incompatibilidade de modo (por ex. <code>enviarComando</code> apelou a um não-<code>aventura</code> mundo) </td><td> Não (rejeitando esta chamada) </td><td> escolha a API certa <code>modo</code>, consulte a tabela de capacidade §2 </td></tr><tr><td> <code>103004</code> </td><td> Criação/início simultâneo de experiência </td><td> Não (lançado de forma síncrona) </td><td> serializar chamadas, <code>fim</code> a sessão antiga primeiro </td></tr><tr><td> <code>105001</code> </td><td> Falha na conexão em tempo real </td><td> Sim </td><td> terminar e reiniciar </td></tr><tr><td> <code>105002</code> </td><td> Tempo limite de adesão em tempo real </td><td> Sim </td><td> terminar e reiniciar </td></tr><tr><td> <code>105003</code> </td><td> Tempo limite aguardando o primeiro quadro de vídeo </td><td> Sim </td><td> terminar e reiniciar </td></tr><tr><td> <code>105004</code> </td><td> Canal em tempo real não pronto/falha no envio </td><td> depende (falha de envio ativo; pulsação é somente relatório) </td><td> enviar depois <code>correndo</code> </td></tr><tr><td> <code>105005</code> </td><td> Tempo limite de retorno de chamada (padrão 30s) </td><td> Não </td><td> tente novamente e aumente <code>callbackTimeoutMs</code> se necessário </td></tr><tr><td> <code>105006</code> </td><td> Nenhuma transmissão após entrar; O SDK encerra automaticamente a experiência </td><td> Sim </td><td> terminar e reiniciar </td></tr><tr><td> <code>106001</code> </td><td> Erro de rede local </td><td> Não </td><td> repetível </td></tr><tr><td> <code>106002</code> </td><td> Falha na análise da resposta </td><td> depende </td><td> atualizar o SDK/relatório </td></tr><tr><td> <code>106003</code> </td><td> Nenhum código de erro reconhecível/código de erro de string de proxy </td><td> Não </td><td> repetível </td></tr><tr><td> <code>108001</code> </td><td> Desativado remotamente pela opção de recurso do servidor (desligamento total ou versão muito baixa; motivo em <code>cru</code>) </td><td> Sim </td><td> Acompanhe o motivo em <code>cru</code>; orientar o usuário a atualizar quando a versão for muito baixa </td></tr></tbody></table>
Como julgar a fatalidade: a fatalidade não é mais exposta como um campo booleano (OysterSDKError não tem isFatal). Semanticamente, "fatal" significa especificamente se o SDK encerra ativamente a sessão (desconectar o RTC, liberar a sessão inteira) —
-
Erros que encerram automaticamente a sessão (por exemplo
105001/105002/105003/105006/108001): o host percebe isso a partir do estado terminal da máquina de estado (status → failed, surgiu através do.statusChangeddeevents) e retorna à tela antes de "iniciar a experiência" de acordo, sem necessidade de julgar a fatalidade em si. -
Erros de rejeição de chamada (
100001/103001/103002/103003/103004): lançada de forma síncrona/a chamada é rejeitada quando você liga ativamente; eles não encerram a sessão. -
Outros erros não fatais (por exemplo
101001/101002/105005/106001/106003): eles não encerram a sessão; tente novamente conforme sugerido ou continue após reinjetar o token.106001pode ter duas causas: um erro de rede local ou um erroapiHost. Se a nova tentativa não recuperar a solicitação, verifique seapiHostestá configurado corretamente.106003aparecendo depois de você definirmodelgeralmente significa que o nome/versão do modelo está errado ou não habilitado para sua conta: verifique o modelo junto com oapiHoste token, que devem pertencer todos à mesma conta — o gateway rejeita uma incompatibilidade comAccessDenied, normalizado para este código.