npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

janus-svelte

v0.0.5

Published

Svelte wrapper around Janus Gateway JS API

Downloads

3

Readme

janus-svelte

A Svelte wrapper to interact with MeetEcho's Janus Gateway.

Fully typescript-compatible.

Installation

Available now at an npm near you:

npm i janus-svelte

or

yarn add janus-svelte

or

git clone [email protected]:emdienn/janus-svelte.git

Motivation

The javascript SDK for Janus, while comprehensive and powerful, can be very indimiating at first blush. This package seeks to svelte-ify and simplify the DX of interacting with Janus, hopefully without sacrificing too much flexibility and power in the process. Out of the gate it won't be feature-complete with all of Janus' plugins, but they'll hopefully get covered over time if there's interest and need.

janus-svelte is entirely unopinionated about markup and style; with exceptions of a few components like <J.Utils.Video />, components are purely functional in nature and don't contribute to the DOM - it's up to your to build your app how you want it.

Code Structure

Svelte Components as Wrappers

Each functional component in janus-svelte is backed by a corresponding set of helper functions in an adjacent ts/js file. The motivation here is that if you need the flexibility to ignore the <Component /> and just go directly for the code, you can do that: the components are simply svelte wrappers around those functions, exposing data via nested slots and event dispatches (as we'll see in the examples below.)

  • The server/ directory contains the core connection component.
  • The plugins/ directory contains a subdirectory for each implemented plugin, each containing one or more components depending on the plugin's complexity.
  • The utils/ directory contains components and scripts that are common to all components.

One-Stop Import

For ease of use, all components are exposed at the top level of the package, so you can just import everything from the library to make it available like so:

<script>
  import * as J from 'janus-svelte'
</script>

This does result in some long namespace chains, however, so for brevity, it's recommended that if you're drilling into plugin types and/or behaviours, to alias the plugin directly:

<script>
  import * as J from 'janus-svelte'
  import * as VR from 'janus-svelte/plugins/videoroom'

  // these both point to the same type
  let foo: J.Plugins.VideoRoom.Pub.VideoOffer
  let bar: VR.Pub.VideoOffer
</script>

The main plugin components, though, are explicitly exported at the top level:

<script>
  import * as J from 'janus-svelte'
</script>

<J.VideoRoom>
  <!-- your implementation here -->
</J.VideoRoom>

Slots and Events

Wherever possible, functional components expose properties both via slots and events. This enables you to handle those variables in the way best served by your application.

<script lang="ts">
  import * as J from 'janus-svelte'
  import type { JanusJS } from 'janus-gateway-ts'

  let janus: JanusJS.Janus
  let room: number
  let username: string
  let pin: string

  function handleAttach(e: CustomEvent<J.VideoRoomAttachEvent>) {
    // note how these properties here match
    // those being `let:` on the default component slot
    const { peers, publish } = e.detail
  }

  function handleError(e: CustomEvent) {
    // this error corresponds to the one exposed in the error slot
    const error = e.detail
  }
</script>

<J.VideoRoom {janus} {room} {username} {pin} let:publish let:peers on:attach="{handleAttach}" on:error="{handleError}">
  <div slot="error" let:error>
    <h1>Room Join Failed</h1>
    <pre>{JSON.stringify(error, null, 2)}</pre>
  </div>

  <h2>Joined room {room}</h2>
  <!-- you could do something with `publish` and `peers` here -->
</J.VideoRoom>

Core Component Reference

These are pretty copy-paste-able: obviously fill out things like your server and your room credentials, but for the most part this is all functional code.

Server

<script lang="ts">
  import * as J from 'janus-svelte
  import type { JanusJS } from 'janus-gateway-ts'

  // to whence are you connecting
  let server: string | string[]

  // how verbose should the console be
  let debug: boolean | "all" | JanusJS.DebugLevel[]

  function handleConnect(e: CustomEvent<JanusJS.Janus>) {
    const janus = e.detail

    // you now have code access to your janus connection,
    // if that's something you need
  }

  function handleError(e: CustomEvent) {
    console.error(e.detail)
  }
</script>

<J.Server {server} {debug} let:janus on:connect="{handleConnect}" on:error="{handleError}">
  <div slot="error" let:error>
    <h2>[ERROR {error.code}] {error.message}</h2>
  </div>

  <!-- init room components here with your `janus` variable -->
</J.Server>

TextRoom

<script lang="ts">
  import * as J from 'janus-svelte'

  // room ID, as known by Janus
  let room: number

  // room pin, if set in Janus
  let pin: string | undefined

  // username is NOT optional for textroom
  let username: string

  // display name, however, IS optional
  let display: string | undefined

  function handleAttach(e: CustomEvent<J.Plugins.TextRoom.AttachEvent>) {
    const { on, send, peers } = e.detail

    // peers is a Readable store of Participants
    peers.subscribe(peers => {
      console.log('peers update', peers)
    })

    // on is a listener-attach function for application functionality
    on('data', (handle, data) => {
      console.log('received data', data)
    })

    // send abstracts message sending
    // note that `to` and `tos` are mutually exclusive, and if neither
    // is specified, the message will be broadcast to the whole room
    send({
      text: 'a message to be read',
      // either use `to`...
      to: 'recipient-username',
      // ...OR `tos` - not both
      tos: [ 'recipientA', 'recipientB' ]
    })
  }

</script>

<J.Server server="my.janus.dev" debug="all" let:janus>
  <J.TextRoom {janus} {room} {username} {pin} let:on let:send let:peers
    on:attach={handleAttach}
  >
    <div slot="error" let:error>
      <h2>[ERROR {error.code}] {error.message}</h2>
    </div>

    <h2>Text Room {room} joined</h2>

    <button on:click={() => send('hello world!')}>Send to all</button>

  </J.TextRoom>
</J.Server>

VideoRoom

<script lang="ts">
  import * as J from 'janus-svelte'
  import * as VR from 'janus-svelte/plugins/videoroom'

  let room: number
  let username: string | undefined
  let pin: string | undefined

  let videoOffer: VR.Pub.VideoOffer
  let audioOffer: VR.Pub.AudioOffer

  async function handleRoomAttach(e: CustomEvent<VR.AttachEvent>) {
    const { peers, publish } = e.detail

    // publish is a function that accepts an opaqueId and
    // returns a promise of a PluginHandle
    const { plugin, handle } = await publish('random-string')

    // peers is a Readable store of Publishers
    peers.subscribe(peers => {
      console.log('peers update', peers)
    })
  }

  function handlePeerJoin(e: CustomEvent<VR.Publisher>) {
    // if you subscribe to the `peers` store, you don't really need this, but
    // it is included for completeness
    const publisher = e.detail
  }

  function handlePeerLeave(e: CustomEvent<number>) {
    // the ID of a departing peer - as with join, if you're subscribed to the
    // peer store you don't so much need this
    const peerId = e.detail
  }

  function handlePublishAttach(e: CustomEvent<string>) {
    // you'll want this for filtering out your own feed
    const localFeedId = e.detail
  }

  function handleLocalStream(e: CustomEvent<MediaStream>) {
    // any time the local media stream changes, this event will fire
    const localMediaStream = e.detail
  }

  function handleRemoteStream(e: CustomEvent<{ peer: VR.Sub.PeerModel }>) {
    const { peer } = e.detail
  }
</script>

<J.Server server="my.janus.dev" debug="all" let:janus>
  <J.VideoRoom
    {janus}
    {room}
    {username}
    {pin}
    let:publish
    let:peers
    on:attach="{handleRoomAttach}"
    on:error="{handleError}"
    on:join="{handlePeerJoin}"
    on:leave="{handlePeerLeave}">
    <div slot="error" let:error>
      <h2>[ERROR {error.code}] {error.message}</h2>
    </div>

    <h2>Video room {room} joined</h2>

    <VR.Publish
      {publish}
      {videoOffer}
      {audioOffer}
      let:stream
      on:attach="{handlePublishAttach}"
      on:localstream="{handleLocalStream}">
      <!-- this binds a <video /> element, you can see your face now! -->
      <J.Utils.Video {stream} />
    </VR.Publish>

    <!-- iterating and filtering $peers -->
    <!-- the Peer component will actively subscribe to the peer feed -->
    <VR.Peer peer="{$peers[n]}" let:stream let:meta on:remotestream="{handleRemoteStream}">
      <div slot="error" let:error>
        <pre>{JSON.stringify(error, null, 2)}</pre>
      </div>

      <!-- view the remote feed with a video element -->
      <J.Utils.Video {stream} />
      <!-- meta is a Readable store -->
    </VR.Peer>
    <!-- /iteration -->
  </J.VideoRoom>
</J.Server>

Code Walkthrough

0. Connect to Janus

<script lang="ts">
  import * as J from 'janus-svelte'

  // tell it where to connect
  const server = 'wss://my-janus-server.com'

  // show all debug messages in console; omit this for production
  const debug = 'all'

  // janus-svelte exposes errors both as named slots and as dispatched events,
  // so you can handle the effects either in code or in JSX...or both, it's your
  // call.
  function handleError(e) {
    // display the error in the console
    console.error(e)
  }
</script>

<J.Server {server} {debug} let:janus on:error="{handleError}">
  <!-- display the error on the page -->
  <div slot="error" let:error>
    <h1>Connection Failed</h1>
    <pre>{JSON.stringify(error, null, 2)}</pre>
  </div>

  <h1>Connected to {janus.getServer()}</h1>
</J.Server>

1. Join a VideoRoom

<script lang="ts">
  import * as J from 'janus-svelte'

  const server = 'wss://my-janus-server.com'
  const debug = 'all'

  // we need to specify the room we're joining
  const room = 1234
  const username = 'Mateo'

  // (the default demo room has no password)
  const pin: string = undefined

  function handleError(e) {
    console.error(e)
  }
</script>

<J.Server {server} {debug} let:janus on:error="{handleError}">
  <div slot="error" let:error>
    <h1>Connection Failed</h1>
    <pre>{JSON.stringify(error, null, 2)}</pre>
  </div>

  <h1>Connected to {janus.getServer()}</h1>

  <!-- The VideoRoom component will auto-join the room we specify -->
  <J.VideoRoom {janus} {room} {username} {pin} let:publish let:peers on:error="{handleError}">
    <div slot="error" let:error>
      <h1>Room Join Failed</h1>
      <pre>{JSON.stringify(error, null, 2)}</pre>
    </div>

    <h2>Joined room {room}</h2>
  </J.VideoRoom>
</J.Server>

2. Publish a Feed

<script lang="ts">
  import * as J from 'janus-svelte'

  const server = 'wss://my-janus-server.com'
  const debug = 'all'
  const room = 1234
  const username = 'Mateo'
  const pin: string = undefined

  // we specify that we want to send video and audio
  const videoOffer: J.VideoRoom.VideoOffer = 'stdres-16:9'
  const audioOffer: J.VideoRoom.AudioOffer = true

  // create a spot to put our local feed ID
  let localFeedId: number

  function handleError(e: CustomEvent<any>) {
    console.error(e)
  }

  // when our publisher attaches, we want to capture its feed ID
  function handlePublishAttach({ detail: feedId }: CustomEvent<number>) {
    localFeedId = feedId
  }
</script>

<J.Server {server} {debug} let:janus on:error="{handleError}">
  <div slot="error" let:error>
    <h1>Connection Failed</h1>
    <pre>{JSON.stringify(error, null, 2)}</pre>
  </div>

  <h1>Connected to {janus.getServer()}</h1>

  <J.VideoRoom {janus} {room} {username} {pin} let:publish let:peers on:error="{handleError}">
    <div slot="error" let:error>
      <h1>Room Join Failed</h1>
      <pre>{JSON.stringify(error, null, 2)}</pre>
    </div>

    <h2>Joined room {room}</h2>

    <J.VideoRoom.Publish {publish} {videoOffer} {audioOffer} let:stream on:attach="{handlePublishAttach}">
      <p>Publish {localFeedId}</p>

      <!-- show our local stream on the page so we can see ourselves -->
      <J.Video {stream} />
    </J.VideoRoom.Publish>
  </J.VideoRoom>
</J.Server>

3. Subscribe to Peers

<script lang="ts">
  import * as J from 'janus-svelte'
  import { get } from 'svelte/store'

  const server = 'wss://my-janus-server.com'
  const debug = 'all'
  const room = 1234
  const username = 'Mateo'
  const pin: string = undefined
  const videoOffer: J.VideoRoom.VideoOffer = 'stdres-16:9'
  const audioOffer: J.VideoRoom.AudioOffer = true
  let localFeedId: number

  function handleError(e: CustomEvent<any>) {
    console.error(e)
  }

  function peerFilter(peer: J.Plugins.VideoRoom.Sub.PeerModel) {
    return !peer.ended && get(peer.meta).display !== username
  }
</script>

<J.Server {server} {debug} let:janus on:error="{handleError}">
  <div slot="error" let:error>
    <h1>Connection Failed</h1>
    <pre>{JSON.stringify(error, null, 2)}</pre>
  </div>

  <h1>Connected to {janus.getServer()}</h1>

  <J.VideoRoom {janus} {room} {username} {pin} let:publish let:peers on:error="{handleError}">
    <div slot="error" let:error>
      <h1>Room Join Failed</h1>
      <pre>{JSON.stringify(error, null, 2)}</pre>
    </div>

    <h2>Joined room {room}</h2>

    <J.Plugins.VideoRoom.Publish {publish} {videoOffer} {audioOffer} let:stream>
      <p>Publish {localFeedId}</p>
      <J.Video {stream} />
    </J.Plugins.VideoRoom.Publish>

    <!-- filter out our own feed, otherwise we subscribe to ourselves -->
    {#each Object.entries(peers).filter(peerFilter) as peer}
    <!-- accept the connection with a Peer component -->
    <J.Plugins.VideoRoom.Peer {peer} let:stream let:meta>
      <p>Peer: {meta.display}</p>
      <!-- and show the feed on the page -->
      <J.Utils.Video {stream} />
    </J.Plugins.VideoRoom.Peer>
    {/each}
  </J.VideoRoom>
</J.Server>

Bugs and Issues

Please use Github Issues to report any bugs or problems.

Please note that this package depends heavily on both Janus Gateway (as the backend service) and the javascript/typescript generously provided by meetecho and notedit. Issues in those underpinning libraries and/or general Janus usage aren't supported, so please endeavour to ensure that you're submitting an issue for this package specifically, and not an issue in one of those.

License

MIT