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

@mantequilla-soft/3speak-player

v0.3.2

Published

Framework-agnostic HLS video player SDK for 3Speak. Works with vanilla JS, React, Vue, Svelte, or any framework.

Readme

@mantequilla-soft/3speak-player

Framework-agnostic HLS video player SDK for 3Speak. Works with vanilla JavaScript, React, Vue, Svelte, or any framework.

Eliminates iframes entirely — plays 3Speak videos using native <video> elements with hls.js (Chrome/Firefox) or native HLS (Safari/iOS).

Why?

  • iPhone/Safari compatible — No iframes means no cross-origin blocking, no throttled media playback
  • Lightweight — ~60KB gzipped (hls.js), no Video.js, no iframe overhead
  • CDN fallback chain — Automatically falls back through 3Speak CDN nodes
  • iOS-optimized — Native HLS on Safari, manifest prefetching, single-player strategy
  • TypeScript — Full type definitions included

Install

npm install @mantequilla-soft/3speak-player

Quick Start

Vanilla JavaScript

<video id="player" playsinline></video>

<script type="module">
  import { Player } from '@mantequilla-soft/3speak-player';

  const player = new Player({ muted: true, loop: true });
  player.attach(document.getElementById('player'));
  player.load('author/permlink');

  player.on('ready', ({ isVertical }) => {
    console.log('Video is', isVertical ? 'vertical' : 'horizontal');
    player.play();
  });

  player.on('timeupdate', ({ currentTime, duration }) => {
    console.log(`${currentTime.toFixed(1)}s / ${duration.toFixed(1)}s`);
  });
</script>

React

import { usePlayer } from '@mantequilla-soft/3speak-player/react';

function VideoPlayer({ author, permlink }) {
  const { ref, state, togglePlay, setMuted } = usePlayer({
    autoLoad: `${author}/${permlink}`,
    autoPlay: true,
    muted: true,
    loop: true,
    onReady: ({ isVertical }) => console.log('vertical?', isVertical),
  });

  return (
    <div>
      <video ref={ref} playsInline style={{ width: '100%' }} />
      <button onClick={togglePlay}>{state.paused ? 'Play' : 'Pause'}</button>
      <button onClick={() => setMuted(!state.muted)}>
        {state.muted ? 'Unmute' : 'Mute'}
      </button>
    </div>
  );
}

Vue 3

<template>
  <video ref="videoEl" playsinline />
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { Player } from '@mantequilla-soft/3speak-player';

const videoEl = ref(null);
let player;

onMounted(() => {
  player = new Player({ muted: true, loop: true });
  player.attach(videoEl.value);
  player.load('author/permlink');
  player.on('ready', () => player.play());
});

onUnmounted(() => player?.destroy());
</script>

Svelte

<script>
  import { onMount, onDestroy } from 'svelte';
  import { Player } from '@mantequilla-soft/3speak-player';

  let videoEl;
  let player;

  onMount(() => {
    player = new Player({ muted: true, loop: true });
    player.attach(videoEl);
    player.load('author/permlink');
    player.on('ready', () => player.play());
  });

  onDestroy(() => player?.destroy());
</script>

<video bind:this={videoEl} playsinline />

API Reference

Player

Single video player instance.

const player = new Player(config?: PlayerConfig);

Config: | Option | Type | Default | Description | |--------|------|---------|-------------| | apiBase | string | 'https://play.3speak.tv' | 3Speak player API URL | | debug | boolean | false | Enable console logging | | muted | boolean | true | Start muted (needed for autoplay) | | loop | boolean | false | Loop playback | | hlsConfig | object | {} | hls.js config overrides | | autopause | boolean | false | Auto-pause when scrolled out of viewport | | resume | boolean | false | Resume playback from last position (localStorage) |

Methods:

player.attach(videoElement)          // Attach to a <video> element
player.load('author/permlink')       // Load by 3Speak ref (fetches HLS URL)
player.load({ url, fallbacks, poster }) // Load from direct source
player.play()                        // Play
player.pause()                       // Pause
player.togglePlay()                  // Toggle play/pause
player.seek(time)                    // Seek to time in seconds
player.setMuted(boolean)             // Set mute state
player.setVolume(0-1)                // Set volume
player.setLoop(boolean)              // Set loop mode
player.setPlaybackRate(rate)         // Set speed (0.5, 1, 2, etc.)
player.togglePip()                   // Toggle Picture-in-Picture
player.toggleFullscreen()            // Toggle fullscreen
player.getQualities()                // Get available quality levels (hls.js only)
player.setQuality(index)             // Set quality (-1 for auto, hls.js only)
player.getCurrentQuality()           // Get current quality index
player.setAudioOnly(boolean)         // Audio-only mode (hides video)
player.getThumbnailAt(time)          // Get thumbnail URL at time (stub)
player.enableAutopause()             // Enable auto-pause on scroll out
player.disableAutopause()            // Disable auto-pause
player.clearResumePosition(ref?)     // Clear saved resume position
player.getState()                    // Get current PlayerState
player.detach()                      // Detach from element
player.destroy()                     // Destroy and release resources

Events:

player.on('ready', ({ isVertical, width, height }) => {})
player.on('play', () => {})
player.on('pause', () => {})
player.on('ended', () => {})
player.on('timeupdate', ({ currentTime, duration, paused }) => {})
player.on('error', ({ message, fatal }) => {})
player.on('fallback', ({ url, index }) => {})
player.on('loading', (isLoading) => {})
player.on('resize', ({ width, height, isVertical }) => {})
player.on('buffered', (progress) => {})
player.on('pip', (active) => {})
player.on('fullscreen', (active) => {})
player.on('qualitychange', ({ index, height, width, bitrate }) => {})
player.on('visibility', (visible) => {})
player.on('resume', ({ time, ref }) => {})

PlayerPool

Manage multiple players for feed/shorts UIs.

const pool = new PlayerPool(config?: PlayerConfig);
pool.add(id, videoElement, source?)   // Add a player
pool.addByRef(id, el, author, perm)   // Add + load by 3Speak ref
pool.get(id)                          // Get player by id
pool.remove(id)                       // Remove + destroy player
pool.activate(id)                     // Play this, pause all others
pool.pauseAll()                       // Pause all
pool.setAllMuted(boolean)             // Mute/unmute all
pool.setAllLoop(boolean)              // Set loop on all
pool.retainOnly(ids)                  // Keep only these, destroy rest
pool.prefetch(hlsUrl)                 // Prefetch manifest (CDN warm)
pool.prefetchByRef(author, permlink)  // Prefetch by 3Speak ref
pool.destroy()                        // Destroy everything

ThreeSpeakApi

Direct API access.

import { ThreeSpeakApi } from '@mantequilla-soft/3speak-player';

const api = new ThreeSpeakApi('https://play.3speak.tv');
const meta = await api.fetchVideoMetadata('author', 'permlink');
const source = await api.fetchSource('author', 'permlink');
await api.prefetchManifest(source.url);
await api.recordView('author', 'permlink');

detectPlatform()

import { detectPlatform } from '@mantequilla-soft/3speak-player';

const platform = detectPlatform();
// { isIOS, isSafari, supportsNativeHLS, supportsMSE, supportsHlsJs }

canAutoplay()

import { canAutoplay } from '@mantequilla-soft/3speak-player';

// Test muted autoplay (default)
const canMuted = await canAutoplay();

// Test unmuted autoplay
const canUnmuted = await canAutoplay(false);

Results are cached — safe to call multiple times.

React Hooks

import { usePlayer, usePlayerPool } from '@mantequilla-soft/3speak-player/react';

usePlayer(options) — Single player hook (see Quick Start above)

usePlayerPool(options) — Pool hook for shorts/feeds:

function ShortsFeed({ videos }) {
  const { pool, add, activate, setAllMuted, retainOnly } = usePlayerPool({
    muted: true,
    loop: true,
  });

  // In your scroll handler:
  // add(video.id, videoElement, source)
  // activate(currentVideoId)
  // retainOnly(visibleVideoIds)
}

How It Works

┌─────────────────────────────────────────────┐
│                Your App                      │
│  (React / Vue / Svelte / Vanilla JS)         │
├─────────────────────────────────────────────┤
│            @mantequilla-soft/3speak-player                │
│  ┌──────────┐  ┌───────────┐  ┌──────────┐ │
│  │  Player   │  │ PlayerPool│  │   API    │ │
│  └─────┬────┘  └─────┬─────┘  └────┬─────┘ │
│        │              │              │       │
│  ┌─────▼──────────────▼──────┐ ┌────▼─────┐ │
│  │   HLS Engine              │ │ 3Speak   │ │
│  │  ┌─────────┐ ┌─────────┐ │ │  embed   │ │
│  │  │ hls.js  │ │ Native  │ │ │   API    │ │
│  │  │(Chrome) │ │ (Safari)│ │ │          │ │
│  │  └────┬────┘ └────┬────┘ │ └──────────┘ │
│  └───────┼───────────┼──────┘              │
├──────────┼───────────┼──────────────────────┤
│      <video>     <video>                     │
│     elements     elements                    │
└─────────────────────────────────────────────┘
  • Safari/iOS: Uses native HLS (just sets video.src = m3u8). Zero JavaScript HLS overhead.
  • Chrome/Firefox/Edge: Uses hls.js (MediaSource Extensions) to play HLS streams.
  • CDN fallback: If the primary CDN fails, automatically tries fallback nodes.
  • iOS single-player: iOS only allows one active video — the pool handles this transparently.

Migrating from iframes

If you're currently using <iframe src="https://play.3speak.tv/embed?v=...">, replace with:

- <iframe src="https://play.3speak.tv/embed?v=author/permlink&controls=0" />
+ <video ref={videoRef} playsinline />
- // postMessage to control playback
- iframe.contentWindow.postMessage({ type: 'play' }, '*');
+ // Direct control
+ player.play();

No more cross-origin restrictions, no more postMessage timing issues, no more iOS iframe throttling.

License

MIT