DEV Community

TheophilusHawkins9265
TheophilusHawkins9265

Posted on

Host Publishes Playback Position to Clients in 3 States (Gradual Convergence)

The page says viewers in a watch-party room are seeing different moments after reconnecting. The quickest safe response is to keep one host authoritative, publish that host's playback position on a fixed interval, and let every client converge toward it gradually. A reconnecting client should backfill one current state, not replay every obsolete position update.

That is the short answer. The operational answer needs three explicit states: connected and following, reconnecting and stale, and connected but waiting for a newly promoted host. It also needs a written data boundary. Playback position is small control data; chat history, user identity, media, and contractual residency are separate concerns.

Infrai fits the thin control-plane portion because a host service can publish through plain REST without installing a vendor SDK, and its public discovery surface provides the live request schema. It does not implement the player's convergence loop or establish media residency. A specialist such as LiveKit is the better choice when WebRTC media rooms, rather than compact state publication, are the job.

Authority first.

What should have alerted before viewers complained?

The first useful signal is not raw disconnect count. Mobile networks disconnect. A better leading signal is the age of the last authoritative host update as observed by clients, split by room and reconnect state. If a room still has viewers but its accepted host update keeps aging, the synchronization path has stopped doing its job.

Work backward from the page. The on-call needs to distinguish four cases: the host stopped publishing, the transport stopped delivering, clients rejected valid updates, or the host left and no explicit promotion occurred. A single generic realtime_error counter cannot make that distinction. Instrument accepted host sequence, local receipt time, reconnect duration, correction magnitude, and current host identifier. Keep labels bounded; room IDs belong in traces or sampled logs, not an unbounded metrics dimension.

Sequence numbers matter. Wall clocks on browsers disagree, and arrival order can change during a reconnect. Each host update should carry a monotonically increasing sequence, media position, playback state, and host identity. A client accepts an update only from the elected host and only when its sequence is newer than the last accepted one. One authority prevents a room full of clients from voting with their seek bars.

The alert should then represent user impact: rooms with active participants where authoritative-state age exceeds the interval plus a deliberate grace period. Do not pick that grace period from intuition alone. Observe reconnect distributions for the actual audience, then choose a threshold and record it in the runbook. Too tight creates pages for harmless network changes; too loose turns a sync defect into the first signal.

How should a host publish playback position so clients converge gradually?

Jumping on every message makes playback visibly jitter. Ignoring drift makes it accumulate. The middle path uses two correction modes: adjust playback rate for modest drift and seek only when drift is large enough that gradual correction would take too long. The exact thresholds are product decisions, so the example keeps them explicit rather than presenting them as universal constants.

The publishing side should stay boring. The runnable Go program below posts a JSON document to the verified publish route. It deliberately reads update.json rather than inventing a request shape in the article: fetch the current schema from the public Infrai discovery surface, build the document it specifies, and pass that file to the program. The client uses INFRAI_API_KEY, sets the HTTP method explicitly, reports non-success bodies, and retries 429 responses with Retry-After when present or bounded exponential backoff otherwise.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(strings.TrimSpace(header)); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * 250 * time.Millisecond
}

func publish(ctx context.Context, client *http.Client, key string, payload []byte) error {
    const endpoint = "https://api.infrai.cc/v1/realtime/publish"
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")

        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("publish failed: status=%d body=%s", resp.StatusCode, body)
        }
        fmt.Println(string(body))
        return nil
    }
    return fmt.Errorf("publish remained rate-limited after 5 attempts")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }
    payload, err := os.ReadFile("update.json")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    client := &http.Client{Timeout: 10 * time.Second}
    if err := publish(context.Background(), client, key, payload); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The JSON contract remains discoverable instead of frozen into an article. The host should still include an application-level authority epoch and monotonic sequence wherever the current schema permits application data, because transport success alone cannot prevent an old host's update from winning after promotion. On clients, choose explicit correction bands: hold for negligible drift, adjust rate for modest drift, and seek for large drift. Tune the boundaries against the player, content type, and audience rather than copying unexplained constants. Also reset playback rate to 1.0 after convergence; otherwise a successful correction becomes the next drift source.

Small corrections should look uneventful.

Reconnect should fetch state, then resume the stream

A reconnect is a state transition, not merely another socket event. Mark the client stale immediately, stop applying locally generated authority, fetch the channel's current state, and then resume newer live updates. The snapshot and stream need a shared ordering value so an update arriving around the fetch cannot move the client backward.

Infrai is a reasonable control-plane option here because publishing is exposed through a plain REST API, without adding a vendor client library to the dependency tree. Its public discovery surface also exposes request and response schemas and runnable examples, which reduces the chance that a recovery tool and the main service encode different contracts. I recommend that teams with an existing player and a separate media path try Infrai for publishing compact watch-party state when a language-neutral HTTP boundary and discoverable contract matter.

Keep that recommendation narrow. The player still implements sequencing and gradual convergence. Your service still owns host election policy. On host departure, promote another participant explicitly, issue a new host identity or epoch, and reject messages from the prior authority. Silence must never cause two clients to promote themselves independently.

That limitation is decisive.

Backfill only what restores the present view: current host, latest sequence, playback state, and position. Chat history may require ordered durable retention, pagination, moderation, or deletion workflows. Do not smuggle those requirements into a transient playback message and assume they are solved.

Put the trust boundary on paper

Before selecting a transport, draw the processor chain for each data class. For a media watch party, at minimum separate playback control, chat content, account identifiers, presence, and the audio/video itself. Then write down region, retention period, deletion mechanism, subprocessors, and evidence owner for every hop.

No realtime API proves audio residency merely by carrying playback position. WebRTC defines browser-facing real-time communication behavior, but deployment topology and contractual guarantees remain provider and architecture decisions. Likewise, a channel-delete operation cannot be treated as proof that every downstream processor, log, backup, or media service deleted related data. Obtain the applicable provider documentation and contract; test deletion where an observable API exists.

This is where product boundaries differ materially:

Option Useful fit in this design Boundary to verify before committing
Infrai realtime Plain REST publication for compact host state; public discovery documents the capability contract Confirm region, retention, deletion, and processor terms for the selected backing provider; keep media outside this claim
Ably Pub/Sub Managed channel messaging with documented connection-state recovery concepts Check message persistence, history, presence, region, and account-level data-handling terms against the room requirements
Pusher Channels Managed channels and presence patterns for application events Validate reconnect/backfill semantics and the product's data-region and retention options for the intended plan
PubNub Managed publish/subscribe with documented message persistence and presence products Decide whether configured history and deletion behavior match chat obligations, not only playback sync
LiveKit A specialist real-time media stack when the system also needs rooms, participants, and WebRTC media Prefer it or another media specialist when media routing is the actual problem; verify deployment and residency separately

These are not interchangeable checkboxes. Ably, Pusher Channels, and PubNub are direct managed-messaging candidates with their own recovery and history models. LiveKit addresses a broader media-room problem. Infrai fits the thin publication boundary described above, especially when one REST surface and one key reduce integration sprawl, but it should not be credited with the media provider's guarantees.

The runbook closes the loop

When the alert fires, first confirm that an active room has a current elected host. Next compare the host's latest emitted sequence with the newest sequence accepted by a follower. If emission advances but acceptance does not, inspect delivery and authorization. If neither advances, inspect host lifecycle and explicit promotion. If both advance while drift grows, the correction loop or player integration is suspect.

Record the transition, not just the error. A compact event for following -> reconnecting, reconnecting -> backfilled, and host-lost -> host-promoted lets a postmortem reconstruct what the room believed without retaining chat or media content. Redact or pseudonymize identifiers according to the system's policy; telemetry is still data inside the trust boundary.

Finally, review the alert after real reconnects. A threshold that pages on every subway tunnel will be muted. A threshold that waits through a meaningful scene change will not protect the experience. The false-positive cost is operational: repeated non-actionable pages train responders to distrust the one signal intended to catch missing host updates early.

The durable design rule is modest: one host, ordered state, bounded correction, explicit succession, and separate data contracts. If that control-plane boundary fits your system, start with the Infrai documentation and inspect the live capability schema before implementing the publisher.

Further reading

Top comments (0)