# Integration guide This guide takes your player app from nothing to a working Open Watch Party (OWP) room. It covers the TypeScript, Dart, Kotlin and Swift SDKs, and most steps show the code in all four languages. Your app writes three parts: a player adapter that lets the SDK drive your player, a resolver that finds the user's own copy of the media others share, and the UI. The rules behind every step are in [the SDK spec](../spec/SDK.md). Install the SDK as the README for your language describes: [TypeScript](../sdk/typescript/README.md), [Dart](../sdk/dart/README.md), [Kotlin](../sdk/kotlin/README.md) or [Swift](../sdk/swift/README.md). A complete small app is in [the web example](../examples/web/README.md). Steps 1 to 4 set up every party. Steps 5 and 6 are the host's flow and the guest's flow. Steps 7 to 11 cover controls, leaving, events and errors, relays, and the public relay. ## How a room works The relay runs the room. It holds the room's phase, position and rate, which participant is the host, and the room's settings. The phase is one of `idle`, `paused`, `waiting` or `playing`. Each client plays its own copy of the media. Room positions are in the timeline of the host's reference file, which is the file whose duration the shared media descriptor carries. ## 1. Create the party The party is the SDK object your app talks to. Its constructor takes four things: - A `Relays` object says which relays to use, as step 10 describes. Create one per app and pass it to every party. - A true `client` name gives your app's published name and version, as step 11 describes. - A `name` is the user's display name. - Your resolver from step 3 matches the media that other participants share. If `client` or `name` is bad, the constructor throws `invalid_message`. Subscribe before you call `create` or `join`, because the party replays nothing. Then `create` opens a new room and returns its room code, which `formatCode` formats for display. To enter an existing room, call `join(code)` instead. TypeScript: ```ts import { Relays, WatchParty, formatCode } from "openwatchparty"; const relays = Relays.public(); // one per app, passed to every party const party = new WatchParty({ relays, client: "Example/1.0", // your app's published name and version name: "Anna", // the user's display name resolver: libraryResolver(library), // step 3; `library` is your app's library }); party.on("state", render); // subscribe before create or join: nothing is replayed party.on("event", onEvent); const code = await party.create(); // or: await party.join(typedCode) showCode(formatCode(code)); // "K7M-Q2X" ``` Dart: ```dart final relays = Relays.public(); // one per app, passed to every party final party = WatchParty( relays: relays, client: 'Example/1.0', // your app's published name and version name: 'Anna', // the user's display name resolver: LibraryResolver(library), ); party.states.listen(render); // subscribe before create or join: nothing is replayed party.events.listen(onEvent); final code = await party.create(); // or: await party.join(typedCode) showCode(formatCode(code)); // "K7M-Q2X" ``` Kotlin: ```kotlin val http = HttpClient(OkHttp) { install(WebSockets) } // your Ktor client and engine val relays = Relays.public(http) // one per app, passed to every party val party = WatchParty( relays = relays, client = "Example/1.0", // your app's published name and version name = "Anna", // the user's display name resolver = LibraryResolver(library), scope = mainScope, // single-threaded; on Android a Main scope ) mainScope.launch { party.state.collect { render(it) } } mainScope.launch { party.events.collect { onEvent(it) } } // events are not replayed val code = party.create() // or: party.join(typedCode) showCode(formatCode(code)) // "K7M-Q2X" ``` Swift: ```swift import OpenWatchParty let relays = Relays.public() // one per app, passed to every party let party = try WatchParty( relays: relays, client: "Example/1.0", // your app's published name and version name: "Anna", // the user's display name resolver: libraryResolver(library) // step 3; `library` is your app's library ) // Subscribe before create or join: nothing is replayed. Each call is a stream of its own that gets every change. let states = party.states() let events = party.events() Task { for await state in states { render(state) } } Task { for await event in events { onEvent(event) } } let code = try await party.create() // or: try await party.join(typedCode) try showCode(formatCode(code)) // "K7M-Q2X" ``` ### Kotlin and Swift In Kotlin, the party calls your adapter and resolver on the scope you pass. The sample's scope is single-threaded, which on Android means a Main scope. In Swift, the party is `@MainActor`, so it calls your adapter and resolver on the main actor. `states()` and `events()` each return a new `AsyncStream` that gets every change. The constructor and `formatCode` throw, so you call them with `try`. The SDK spec has the details on the [relay list](../spec/SDK.md#3-relay-list) and the [per-language idioms](../spec/SDK.md#168-per-language-idioms). ### Room codes `join` takes the code as the user typed it. It removes separators and upper-cases the rest, so that it can route the code by its first symbol. If the result isn't 6 symbols from `23456789ABCDEFGHJKMNPQRSTUVWXYZ`, the call fails with `invalid_code` before any network use. The relay applies the same normalization to the `join` and `resume` codes it receives, as [room codes](../spec/SPEC.md#51-room-codes) describes. This matters only to apps that speak the wire protocol directly. ### Coming back to a code A room ends when its last participant leaves, when the host ends it, or when it reaches its maximum age. Its code then names no room, and `join` fails with `room_not_found`. To bring a group back to a code it used before, such as one from your app's recent rooms, join with `create`. The call joins the room when it exists. Otherwise it makes a new room with that code, and the user is its host. ```ts await party.join(code, { create: true }); ``` In Kotlin the call is `party.join(code, create = true)`, in Dart `party.join(code, create: true)` and in Swift `try await party.join(code, create: true)`. Keep these points in mind: - Whoever arrives first makes the room and is its host. The host can hand the room over with `transferHost`. - A code carries no ownership. While no room has it, anyone who knows it can make a room with it. - A relay that predates `create` answers `room_not_found`. Offer to create a new room then. The rules are in [create and join](../spec/SDK.md#72-create-and-join) and [the relay's `join`](../spec/SPEC.md#72-join). ## 2. Write the player adapter The player adapter is how the SDK reads your player's state and sends it commands. Each kind of member has one rule: - Reads, such as `positionMs` and `paused`, must be cheap and synchronous. - Commands, such as `play` and `seek`, must either finish or throw. Two optional members tune how the SDK follows the room: - `setRate` changes the player's speed. Without it, the SDK only seeks. In Dart, Kotlin and Swift, also return `true` from `canSetRate`. - `seekAccuracyMs` says how far from its target a seek may land, and defaults to 0. For a player that seeks only to keyframes, use the keyframe distance. For one that can't seek at all, use a very large value. ### When the player refuses to start When the player refuses to start, `play` must throw. While the player then stays paused, the SDK reports it as `loading` and emits a play-blocked event, which step 9 handles. The event is `playBlocked` in TypeScript, `PlayBlockedEvent` in Dart and Swift, and `PartyEvent.PlayBlocked` in Kotlin. If `play` swallows the refusal instead, the SDK reports the player as `ready` while it sits paused. The rules are in [player state](../spec/SDK.md#102-player-state). ### The adapter in code The TypeScript sample also calls `playerChanged` when the player changes. That is optional, but it gets changes to the room sooner. TypeScript: ```ts import type { PlayerAdapter } from "openwatchparty"; const adapter: PlayerAdapter = { get positionMs() { return player.positionMs; }, // in your own file get durationMs() { return player.durationMs; }, // null when unknown get paused() { return player.paused; }, get buffering() { return player.stalled; }, // stalled while trying to play get firstFrameShown() { return player.frameShown; }, play: () => player.play(), pause: () => player.pause(), seek: (positionMs) => player.seek(positionMs), // resolves once the seek has landed setRate: (rate) => player.setSpeed(rate), // optional: without it the SDK only seeks }; player.onChange(() => party.playerChanged()); // optional: changes reach the room sooner ``` Dart: ```dart final class ExampleAdapter extends PlayerAdapter { ExampleAdapter(this.player); final ExamplePlayer player; @override int get positionMs => player.positionMs; // in your own file @override int? get durationMs => player.durationMs; // null when unknown @override bool get paused => player.paused; @override bool get buffering => player.stalled; // stalled while trying to play @override bool get firstFrameShown => player.frameShown; @override bool get canSetRate => true; // optional, with setRate: without it the SDK only seeks @override Future play() => player.play(); @override Future pause() => player.pause(); @override Future seek(int positionMs) => player.seek(positionMs); // completes once the seek has landed @override Future setRate(double rate) => player.setSpeed(rate); } ``` Kotlin: ```kotlin class ExampleAdapter(private val player: ExamplePlayer) : PlayerAdapter { override val positionMs: Long get() = player.positionMs // in your own file override val durationMs: Long? get() = player.durationMs // null when unknown override val paused: Boolean get() = player.paused override val buffering: Boolean get() = player.stalled // stalled while trying to play override val firstFrameShown: Boolean get() = player.frameShown override val canSetRate: Boolean get() = true // optional, with setRate: without it the SDK only seeks override suspend fun play() = player.play() override suspend fun pause() = player.pause() override suspend fun seek(positionMs: Long) = player.seek(positionMs) // returns once the seek has landed override suspend fun setRate(rate: Double) = player.setSpeed(rate) } ``` Swift: ```swift @MainActor final class ExampleAdapter: PlayerAdapter { private let player: ExamplePlayer init(_ player: ExamplePlayer) { self.player = player } var positionMs: Int64 { player.positionMs } // in your own file var durationMs: Int64? { player.durationMs } // nil when unknown var paused: Bool { player.paused } var buffering: Bool { player.stalled } // stalled while trying to play var firstFrameShown: Bool { player.frameShown } var canSetRate: Bool { true } // optional, with setRate: without it the SDK only seeks func play() async throws { try await player.play() } // throws when the player refuses to start func pause() async throws { try await player.pause() } func seek(to positionMs: Int64) async throws { try await player.seek(positionMs) } // returns once the seek has landed func setRate(_ rate: Double) async throws { try await player.setSpeed(rate) } } ``` ### On the web On the web, `attachVideo(party, video, ref)` wraps an `HTMLMediaElement` and routes the element's controls to the party. Call it once the element has the chosen file open, as in step 4. It returns a detach function, which you await before you dispose of the element. For pages your app doesn't own, `videoAdapter(video)` makes the adapter alone. ```ts import { attachVideo } from "openwatchparty"; const video = document.querySelector("video")!; const detach = attachVideo(party, video, party.state.choice!.ref); // once the element has the chosen file open (step 4) // later: await detach(); // before disposing of the element ``` ### Players controlled over IPC If your app controls the player over IPC, the adapter still answers reads without waiting: - Reads return values cached from the player's property-change events. They never wait on the socket. - Commands await the player's reply. A reply can arrive before its property event, so set the cached `paused` after the reply. - A seek's reply also arrives before the seek lands. So `seek` waits for the report that it landed, under a timeout. - A refused command rejects, so `play` throws as described above. - Call `party.playerChanged()` on any change other than position. ### Browser extensions A browser extension runs on pages your app doesn't own, so it uses `videoAdapter`: - Put the adapter on the page's main `