@purplecallio/vue

Official Vue 3 composable and components for PurpleCallio, a developer-focused real-time communication infrastructure platform for audio, video, screen sharing and WebRTC applications.
206
Install
npm install @purplecallio/vue
Latest Version:0.2.0
Vue:3
License:MIT
Last Updated:Oct 23, 2026
Links: npm
Maintainer: purplecallio

@purplecallio/vue

Official Vue 3 (Composition API) composable and components for PurpleCallio, a developer-focused real-time communication infrastructure platform for audio, video, screen sharing and WebRTC applications.

This package is a thin, framework-idiomatic adapter around @purplecallio/sdk's headless PurpleCallioMeeting engine — it does not reimplement any WebRTC/signaling logic itself. If you're building with React, React Native or Angular instead, see @purplecallio/react, @purplecallio/react-native and @purplecallio/angular.

Install

npm install @purplecallio/vue @purplecallio/sdk vue

vue (>=3.2.0) is a peer dependency — bring your own Vue install rather than have one bundled with this package, so your app only ever has a single Vue instance (required for reactivity/lifecycle hooks to work correctly).

Security: where does token come from?

usePurpleCallio({ token, ... }) takes a short-lived participant token, not a PurpleCallio API key.

  • A PurpleCallio API key is a server-only secret. It authenticates privileged, account-level operations (creating calls, minting tokens) via @purplecallio/sdk's PurpleCallioClient and must never be embedded in, shipped to, or generated by client-side code — including this package.
  • A participant token is a narrowly-scoped, short-lived credential for one participant in one call. Your own backend calls PurpleCallioClient server-side (using your API key) to mint one, then hands it to your frontend (e.g. as the response to your app's own "join this call" API endpoint). That token is what you pass into usePurpleCallio().

This mirrors exactly how @purplecallio/react, @purplecallio/react-native and @purplecallio/angular all handle it — there is no client-side path to a token that doesn't go through your backend first.

Usage

<script setup lang="ts">
import { usePurpleCallio, PurpleCallioVideo } from '@purplecallio/vue';

const props = defineProps<{ callId: string; token: string }>();
// `props.token` was fetched from YOUR OWN backend before this component
// mounted (see examples/App.vue + examples/CallView.vue for the full flow,
// including why the fetch happens in a parent component, not here).

const {
  connectionState,   // Ref<ConnectionState>
  participants,      // Ref<Participant[]>
  media,             // Ref<{ camera, microphone, screenShare }>
  remoteStream,      // ShallowRef<MediaStream | null>
  localStream,       // ShallowRef<MediaStream | null>
  participantId,     // Ref<string | null>
  join,
  leave,
  camera,            // { enable(), disable(), toggle(), isEnabled() }
  microphone,        // { enable(), disable(), toggle(), isEnabled() }
  screenShare,       // { start(), stop(), isActive() }
} = usePurpleCallio({
  token: props.token,
  callId: props.callId,
  // Your PurpleCallio host: Socket.IO origin, and the REST API under /api.
  signalUrl: 'https://<your-purplecallio-host>',
  apiUrl: 'https://<your-purplecallio-host>/api',
});
</script>

<template>
  <div>
    <p>{{ connectionState }} — {{ participants.length }} participant(s)</p>

    <PurpleCallioVideo :stream="localStream" :muted="true" />
    <PurpleCallioVideo :stream="remoteStream" :muted="false" />

    <button @click="join">Join</button>
    <button @click="camera.toggle()">Toggle camera</button>
    <button @click="microphone.toggle()">Toggle microphone</button>
    <button @click="leave">Leave</button>
  </div>
</template>

See examples/App.vue and examples/CallView.vue for a complete, realistic walkthrough: fetch a token from your own backend → configure/join the call → render local + remote video → toggle camera/mic → leave → cleanup.

Automatic cleanup

When usePurpleCallio() is called inside a component's setup(), it registers an onUnmounted hook that calls leave() as a last-resort safety net if the component unmounts while the call is still connected (mirroring the intent of @purplecallio/angular's PurpleCallioService.ngOnDestroy safety net). This is not a substitute for calling leave() explicitly at the right point in your own app flow (e.g. a "Leave call" button) — relying on unmount alone means the call stays connected for as long as the component happens to be mounted, which is rarely what you actually want for an intentional hang-up.

usePurpleCallio() also works outside a component's setup() (e.g. from a Pinia store, or composed by another composable-of-composables) — getCurrentInstance() is used internally to detect that case and skip registering the unmount hook, since there is no component instance to attach it to. In that mode, cleanup is entirely your own responsibility: call leave() yourself when you're done, since nothing will do it for you.

Each call to usePurpleCallio() creates its own new engine instance and its own independent reactive state — it is not a shared singleton across calls, the same way @purplecallio/react's <MeetingProvider> and @purplecallio/react-native's <PurpleCallioProvider> each own one engine per mounted provider.

<PurpleCallioVideo>

<PurpleCallioVideo :stream="remoteStream" :muted="false" />

Binds a MediaStream (local or remote) to a <video> element's srcObject, re-attaching whenever the stream prop changes and clearing it on unmount — the same concept as @purplecallio/angular's purplecallioVideo directive, expressed as a Vue component instead. Props: stream (MediaStream | null | undefined), muted (default true — flip to false for a remote participant's video so you actually hear them), autoplay (default true), playsInline (default true, required on iOS Safari to avoid full-screen takeover).

Build tooling

This package ships one real .vue Single-File Component (PurpleCallioVideo.vue). SFCs are not valid input to plain tsc — the <template>/<script setup> blocks need @vue/compiler-sfc to be turned into plain JS/TS first, which is exactly what @vitejs/plugin-vue does. So the build is:

  • vite build (library mode, vite.config.ts) — @vitejs/plugin-vue compiles the .vue file, Vite/Rollup bundles src/index.ts into a single ESM output, vue and @purplecallio/sdk are kept external (never bundled — both must resolve to the consuming app's own install).
  • vite-plugin-dts generates the .d.ts declarations for the whole package, including a real, accurate declaration for PurpleCallioVideo.vue (via @vue/language-core, the same engine that powers vue-tsc — so PurpleCallioVideoProps isn't hand-rolled or any-typed in the output).

Output is ESM-only (dist/index.mjs + dist/index.d.ts), which is standard for a Vue 3 library — Vue itself, and the Vue ecosystem's build tooling, are ESM-first. package.json's exports map only advertises an "import" condition (no "require"); a CJS consumer would need their own bundler/interop, same as most modern Vue libraries.

PurpleCallioVideoProps is factored into a plain PurpleCallioVideo.types.ts file (imported by the SFC via defineProps<PurpleCallioVideoProps>()) rather than declared inline in <script setup>, purely so it can be re-exported cleanly from src/index.ts without depending on a .vue file's own generated declaration shape.

Testing

vitest + @vue/test-utils (jsdom environment). @purplecallio/sdk is mocked with vi.mock(...) in every test — no real WebRTC/socket code ever runs. See vitest.config.ts for why the @purplecallio/sdk module specifier is also aliased straight at its built CJS entry point: the SDK's published exports map declares an "import" condition pointing at dist/index.mjs, a file that doesn't actually exist (only the CJS dist/index.js is built) — a pre-existing mismatch in packages/sdk itself that this package does not modify, but that trips up Vite's module resolution before vi.mock's replacement even gets a chance to apply.

npm test

Covers: usePurpleCallio's reactive refs updating on every engine event, join()/leave() delegating to the engine correctly, the unmount safety net (calls leave() only when still connected, never twice, stops forwarding events after unmount), a regression guard pinning camera.toggle() / microphone.toggle() to engine.*.toggle() (not .enable()/.disable() — see "Known limitations" below), and <PurpleCallioVideo> binding/unbinding srcObject on mount, prop change, and unmount.

Known limitations

  • @purplecallio/react's context.tsx has a pre-existing bug where its toggleCamera/toggleMicrophone call engine.camera.enable() / engine.microphone.disable() instead of the engine's own .toggle(). @purplecallio/vue does not repeat this: camera.toggle() and microphone.toggle() both call the engine's .toggle() directly — see tests/composable.spec.ts's "toggle semantics (regression guard)" suite.
  • Like the local-stream handling in every sibling adapter, localStream is only populated once join() resolves (the engine has no dedicated "local stream ready" event to subscribe to) — there is no local preview before joining.
  • This package does not add device-enumeration (enumerateDevices) or network-state helpers the way @purplecallio/react and @purplecallio/react-native do — those are convenience layers over browser/platform APIs unrelated to the core engine, and were left out to keep this first version focused. They can be added as additional composables (useDevices(), etc.) later without changing usePurpleCallio's shape.

Related Packages

nuxt-livekit

LiveKit integration for Nuxt: realtime video, audio, and data with client compos...

639 3
briefing-plugin-sdk

Build plugins for Briefing, the secure video chat: plugin API, Vite preset and t...

409 1
@pouchy_ai/companion-sdk

Embed the Pouchy companion — chat, voice, tools, memory, live world-state, insta...

14,543
@millicast/vue-viewer-plugin

Vue.js plugin to embed viewer of a Millicast stream. This plugin allows develope...

3,851 7

Version History

Version Released Vue License
0.2.0 >=3.2.0 MIT
0.1.0 >=3.2.0 MIT