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 🙏

© 2026 – Pkg Stats / Ryan Hefner

synxed-native-sdk

v0.1.2

Published

Lightweight licensed music playback SDK for Expo and React Native applications

Downloads

16

Readme

Synxed Native SDK

Lightweight licensed-music playback SDK for Expo and React Native applications. Choose between a drop-in ready music player UI or build your own custom controls — both backed by the same powerful playback engine.

  • Bundle size: ~35 KB (ESM), ~6 KB gzipped
  • Runtime deps: 1 (socket.io-client)
  • 60 fps record spinner driven entirely on the native thread
  • Zero-config playlists — queue management, track changes, and skip logic handled by the server

Two ways to integrate

| Approach | When to use | Lines of code | | ---------------------------------------- | ------------------------------------------------------------------ | ------------- | | Drop-in UI | You want a ready-made player — just pass your API key and playlist | ~5 | | Build your own | You need custom controls, layout, or a non-React environment | ~30 |


Drop-in UI

The SDK ships with a production-ready MusicPlayer component. Three display modes, automatic lifecycle handling, and full playlist support — no configuration needed beyond your credentials.

5-second integration

npm install synxed-native-sdk expo-audio react-native-svg
import { MusicPlayer } from "synxed-native-sdk";

export default function App() {
  return (
    <View style={{ flex: 1 }}>
      {/* Your app content */}
      <MusicPlayer
        apiKey="sk-..."
        serverUrl="https://api.synxed.com"
        playlistCode="chill-vibes"
        defaultImage={require("./fallback-art.png")}
      />
    </View>
  );
}

That's it. The player auto-connects, auto-plays the playlist, handles app backgrounding, and shows track info — all out of the box.

Display modes

// Mini — compact bar pinned to an edge (default)
<MusicPlayer
  apiKey="sk-..."
  serverUrl="https://api.synxed.com"
  playlistCode="chill-vibes"
  displayMode="mini"
  position="bottom"
/>

// Full — large RecordSpinner + progress bar + prev/play/skip controls
<MusicPlayer
  apiKey="sk-..."
  serverUrl="https://api.synxed.com"
  playlistCode="chill-vibes"
  displayMode="full"
/>

// Icon — floating circular play/pause button
<MusicPlayer
  apiKey="sk-..."
  serverUrl="https://api.synxed.com"
  playlistCode="chill-vibes"
  displayMode="icon"
/>

| Mode | Description | | -------------------- | ------------------------------------------------------------------------------------ | | "mini" (default) | Compact horizontal bar with artwork, title, progress, and play/pause | | "full" | Large RecordSpinner + track info + scrub-able progress bar + prev/play/skip controls | | "icon" | Floating circular play/pause button — smallest footprint |

MusicPlayer props

| Prop | Type | Default | Description | | -------------------- | ------------------------------------ | ------------ | ------------------------------------------- | | apiKey | string | required | Your Synxed API key | | serverUrl | string | required | Synxed server URL | | playlistCode | string | required | The playlist to stream | | displayMode | "full" \| "mini" \| "icon" | "mini" | Visual presentation mode | | position | "bottom" \| "top" | "bottom" | Edge to pin the player to | | defaultImage | ImageSourcePropType | — | Fallback when no album art from the server | | defaultArtwork | ImageSourcePropType | — | Alias for defaultImage (takes precedence) | | autoPlay | boolean | true | Auto-start the playlist on mount | | initialVolume | number | 0.3 | Volume on mount (0–1) | | showProgress | boolean | true | Show scrub-able progress bar | | backgroundColor | string | "#111" | Container background color | | accentColor | string | "#14772F" | Play button and progress fill color | | textColor | string | "#FFFFFF" | Track title color | | textSecondaryColor | string | "#999999" | Artist name and time label color | | edgeOffset | number | 0 | Distance from the pinned edge (pt) | | horizontalInset | number | 12 | Horizontal inset from screen edges (pt) | | style | StyleProp<ViewStyle> | — | Additional container styles | | onTrackChange | (track: TrackInfo \| null) => void | — | Fires when the current track changes | | onConnected | () => void | — | Fires when WebSocket connects | | onError | (error: Error) => void | — | Fires on playback errors |

RecordSpinner

A standalone vinyl-record spinner you can use anywhere — with or without MusicPlayer.

import { RecordSpinner } from "synxed-native-sdk";

<RecordSpinner
  imageSource={{ uri: currentTrack?.albumArt }}
  size={280}
  spinning={isPlaying}
  onPress={() => (isPlaying ? pause() : resume())}
/>;

| Prop | Type | Default | Description | | ------------- | ---------------------- | ------------ | ------------------------------------ | | imageSource | ImageSourcePropType | required | Album art or avatar — center label | | size | number | 300 | Diameter in points | | spinning | boolean | false | Drives rotation (tie to isPlaying) | | visible | boolean | true | Show/hide the component | | onPress | () => void | — | Tap handler | | style | StyleProp<ViewStyle> | — | Additional container styles |

The rotation runs on the native thread via useNativeDriver: true — 60 fps, zero JS thread impact.

ProgressBar

A scrub-able progress bar with time labels. Works with any currentTime / duration pair.

import { ProgressBar } from "synxed-native-sdk";

<ProgressBar
  currentTime={currentTime}
  duration={duration}
  onSeek={(ms) => seek(ms)}
  accentColor="#1DB954"
  labelColor="#999"
/>;

| Prop | Type | Default | Description | | ------------- | ---------------------- | ------------ | ------------------------------ | | currentTime | number | required | Current playback position (ms) | | duration | number | required | Total track duration (ms) | | onSeek | (ms: number) => void | required | Called when the user scrubs | | accentColor | string | "#1DB954" | Fill and thumb color | | labelColor | string | "#999" | Time label color | | barHeight | number | 4 | Track height in points | | style | StyleProp<ViewStyle> | — | Additional container styles |

Icon components

Standalone SVG icons rendered via react-native-svg. Use them to build your own controls.

import { PlayIcon, PauseIcon, SkipNextIcon, SkipPreviousIcon, MusicNoteIcon } from "synxed-native-sdk";

<PlayIcon size={24} color="#1DB954" />
<PauseIcon size={24} color="#ffffff" />
<SkipNextIcon size={20} color="#999" />
<SkipPreviousIcon size={20} color="#999" />
<MusicNoteIcon size={40} color="#14772F" />

| Component | Source | Description | | ------------------ | -------------------------- | ------------------------------- | | PlayIcon | public/play-*-icon.svg | Right-pointing play triangle | | PauseIcon | public/puase-*-icons.svg | Two vertical pause bars | | SkipNextIcon | Custom SVG | Bar + triangle — skip forward | | SkipPreviousIcon | Custom SVG | Triangle + bar — skip backward | | MusicNoteIcon | Custom SVG | Musical note — fallback artwork |

All accept size (number) and color (hex string) props. Default size varies per icon, default color is "#ffffff".


Build your own UI

If you want full control over the player UI, use the useSynxedPlayer hook (React) or the SynxedPlayer class (any environment).

React hook

import { useSynxedPlayer } from "synxed-native-sdk";

function CustomPlayer() {
  const {
    player, // raw SynxedPlayer instance
    state, // PlayerState { status, currentTrack, currentTime, duration, volume }
    isPlaying, // boolean
    isConnected, // boolean — WebSocket state
    currentTrack, // TrackInfo | null
    currentTime, // ms
    duration, // ms
    playSong, // (options: PlaySongOptions) => Promise
    playPlaylist, // (options: PlayPlaylistOptions) => Promise
    pause, // () => Promise
    resume, // () => Promise
    stop, // () => Promise
    skip, // () => Promise
    previous, // () => Promise
    skipTo, // (index: number) => void
    seek, // (ms: number) => Promise
    setVolume, // (volume: 0-1) => Promise
    connect, // () => Promise
  } = useSynxedPlayer({
    apiKey: "sk-...",
    serverUrl: "https://api.synxed.com",
    autoConnect: true, // default: true
  });

  // Build your own UI using these values
  return (
    <View>
      <Text>{currentTrack?.title ?? "Nothing playing"}</Text>
      <Text>{currentTrack?.artist}</Text>
      <RecordSpinner
        imageSource={{ uri: currentTrack?.albumArt }}
        spinning={isPlaying}
      />
      <ProgressBar
        currentTime={currentTime}
        duration={duration}
        onSeek={seek}
      />
      <View style={{ flexDirection: "row", gap: 16 }}>
        <Button title="⏮" onPress={previous} />
        <Button
          title={isPlaying ? "⏸" : "▶"}
          onPress={() => (isPlaying ? pause() : resume())}
        />
        <Button title="⏭" onPress={skip} />
      </View>
      <Button
        title="Load Playlist"
        onPress={() => playPlaylist({ playlistCode: "chill-vibes" })}
      />
    </View>
  );
}

The hook manages the player lifecycle automatically — creates on mount, destroys on unmount. All playback methods are useCallback-wrapped for stable references.

Bare player (no React)

import { SynxedPlayer } from "synxed-native-sdk";

const player = new SynxedPlayer({
  apiKey: "sk-...",
  serverUrl: "https://api.synxed.com",
});

// Subscribe to events
player.on("stateChange", (state) => {
  /* ... */
});
player.on("timeUpdate", ({ currentTime, duration }) => {
  /* ... */
});
player.on("trackChange", (track) => {
  /* ... */
});
player.on("queueUpdated", (tracks) => {
  /* ... */
});
player.on("connected", () => {
  /* ... */
});
player.on("disconnected", (reason) => {
  /* ... */
});
player.on("error", (err) => {
  /* ... */
});

// Playback
await player.playPlaylist({ playlistCode: "chill-vibes" });
await player.playSong({ catalogTrackId: "track-abc-123" });
await player.pause();
await player.resume();
await player.skip();
await player.previous();
await player.seek(30_000);
await player.setVolume(0.5);

// Cleanup
await player.destroy();

Playlist streaming

The SDK handles the full playlist lifecycle — queue population, track advancement, skip/previous logic, and server synchronization.

Starting a playlist

// Hook
await playPlaylist({ playlistCode: "chill-vibes" });

// Bare player
await player.playPlaylist({ playlistCode: "chill-vibes" });

What happens under the hood:

  1. SDK connects to the Synxed server via WebSocket
  2. Sends a playlist init request with your playlistCode
  3. Server responds with the playback URL and a content summary (full track queue as JSON)
  4. SDK populates the internal PlaylistManager with all tracks and begins playback

Skip and previous

For multi-track playlists, skip and previous are server-authoritative — the server sends back the updated queue and playback URL. For single-track sessions, the SDK falls back to client-side logic.

await player.skip(); // Server-side for playlists
await player.previous(); // Server-side for playlists
player.skipTo(3); // Client-side — jump to index 3

Auto-advance

When a track ends, the SDK automatically advances to the next track. When the last track finishes, the player goes to "idle" status.

Queue events

player.on("queueUpdated", (tracks: TrackInfo[]) => {
  // Full track list — render a queue browser
  tracks.forEach((t, i) => console.log(`${i + 1}. ${t.title}`));
});

player.on("trackChange", (track: TrackInfo) => {
  // Fires on skip, previous, auto-advance, or initial play
  updateNowPlaying(track);
});

With the hook, currentTrack stays reactive automatically.

Checking what's next

const { player } = useSynxedPlayer({ ... });
const canSkip = player.playlist?.hasNext ?? false;
const canGoBack = player.playlist?.hasPrevious ?? false;

App lifecycle

Handle backgrounding/foregrounding to pause and resume correctly:

import { AppState } from "react-native";

function App() {
  const { player } = useSynxedPlayer({ apiKey: "...", serverUrl: "..." });

  useEffect(() => {
    const sub = AppState.addEventListener("change", (state) => {
      if (state === "active") player.handleAppForeground();
      else player.handleAppBackground();
    });
    return () => sub.remove();
  }, [player]);
}

MusicPlayer handles this automatically — you only need this if building your own UI.


API reference

Core types

interface SynxedConfig {
  apiKey: string; // Your Synxed API key
  serverUrl: string; // Synxed server URL
  autoConnect?: boolean; // Auto-connect on construction (default: true)
}

interface PlaySongOptions {
  catalogTrackId?: string;
  internalTrackId?: string;
  listenerId?: string;
}

interface PlayPlaylistOptions {
  playlistCode: string;
  listenerId?: string;
}

interface TrackInfo {
  id: string;
  kind: "catalog" | "internal";
  title?: string;
  artist?: string;
  duration?: number; // ms
  albumArt?: string; // URL
  metadata?: TrackMetadata;
}

interface PlayerState {
  status: "idle" | "loading" | "playing" | "paused" | "error";
  currentTrack: TrackInfo | null;
  currentTime: number; // ms
  duration: number; // ms
  volume: number; // 0–1
}

interface TimeUpdate {
  currentTime: number; // ms
  duration: number; // ms
}

interface SynxedEvents {
  stateChange: (state: PlayerState) => void;
  timeUpdate: (time: TimeUpdate) => void;
  trackChange: (track: TrackInfo) => void;
  queueUpdated: (tracks: TrackInfo[]) => void;
  error: (error: Error) => void;
  connected: () => void;
  disconnected: (reason: string) => void;
}

SynxedPlayer methods

| Method | Description | | ----------------------- | ------------------------------------------ | | connect() | Open WebSocket connection | | disconnect() | Close WebSocket connection | | playSong(opts) | Play a single track | | playPlaylist(opts) | Play a playlist | | pause() | Pause playback | | resume() | Resume playback | | stop() | Stop and reset to idle | | skip() | Next track (server-side for playlists) | | previous() | Previous track (server-side for playlists) | | skipTo(index) | Jump to queue index | | seek(ms) | Seek to position in milliseconds | | setVolume(v) | Set volume (clamped 0–1) | | destroy() | Full teardown | | handleAppBackground() | Pause-on-background handler | | handleAppForeground() | Auto-resume handler |

Getters

| Getter | Type | Description | | --------------------- | --------------------------------------------------------- | -------------------------- | | player.status | "idle" \| "loading" \| "playing" \| "paused" \| "error" | Current playback status | | player.volume | number | Current volume | | player.isConnected | boolean | WebSocket connection state | | player.currentTrack | TrackInfo \| null | Currently active track |

Error types

import {
  SynxedError, // Base error
  SynxedConnectionError, // Socket / network / timeout failure
  SynxedPlaybackError, // Audio engine failure
  SynxedProtocolError, // Protobuf decode failure
} from "synxed-native-sdk";

player.on("error", (err) => {
  if (err instanceof SynxedConnectionError) {
    showToast("Connection lost. Retrying...");
  } else if (err instanceof SynxedPlaybackError) {
    showToast("Playback failed.");
  } else {
    showToast(err.message);
  }
});

Advanced exports

import {
  EventEmitter, // Typed event system
  AudioEngine, // expo-audio wrapper
  PlaylistManager, // Queue manager
  TransportManager, // WebSocket transport
  encodeClientEnvelope, // Protobuf encoder
  decodeServerEnvelope, // Protobuf decoder
} from "synxed-native-sdk";

Requirements

| Dependency | Version | Notes | | ------------------ | -------- | ---------------------------------------------------------------------------- | | react | ≥ 18.0.0 | Optional — only needed for useSynxedPlayer, RecordSpinner, MusicPlayer | | react-native | ≥ 0.70.0 | Optional — Animated, View, Image; Hermes TextEncoder support | | expo-audio | ≥ 0.1.0 | Audio playback engine (replaces deprecated expo-av) | | react-native-svg | ≥ 15.0.0 | Optional — SVG icon rendering for MusicPlayer and icon components | | socket.io-client | ^4.7.0 | Bundled runtime dependency |


Troubleshooting

expo-audio not found

npx expo install expo-audio

react-native-svg not found

npx expo install react-native-svg

socket.io-client transport errors

The SDK uses WebSocket only. Ensure your dev server, proxy, or firewall allows ws:// or wss:// connections.

Hermes engine — TextEncoder not found

The SDK uses TextEncoder/TextDecoder for the protobuf layer. Hermes has supported these since React Native 0.70. Upgrade if needed:

npm install react-native@latest

RecordSpinner doesn't spin

  • Verify spinning={true} is passed
  • Ensure the component is mounted (visible={true})
  • Rotation runs on the native thread — JS thread congestion won't affect it

Playlist queue not populating

  • Confirm your playlistCode is valid
  • Listen to queueUpdated to verify:
player.on("queueUpdated", (tracks) => {
  if (tracks.length === 0) console.warn("Empty queue");
});

Skip/previous not advancing

  • Multi-track playlists use server-authoritative skip/previous — the server must handle the control messages
  • Single-track sessions use client-side fallback — ensure a PlaylistManager has been populated

License

MIT