@mantequilla-soft/3speak-player 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/dist/index.cjs +837 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +400 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +826 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +935 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +446 -0
- package/dist/react.d.ts +446 -0
- package/dist/react.js +928 -0
- package/dist/react.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 3Speak / Mantequilla Soft
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# @mantequilla-soft/3speak-player
|
|
2
|
+
|
|
3
|
+
Framework-agnostic HLS video player SDK for [3Speak](https://3speak.tv). Works with vanilla JavaScript, React, Vue, Svelte, or any framework.
|
|
4
|
+
|
|
5
|
+
Eliminates iframes entirely — plays 3Speak videos using native `<video>` elements with [hls.js](https://github.com/video-dev/hls.js) (Chrome/Firefox) or native HLS (Safari/iOS).
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
- **iPhone/Safari compatible** — No iframes means no cross-origin blocking, no throttled media playback
|
|
10
|
+
- **Lightweight** — ~60KB gzipped (hls.js), no Video.js, no iframe overhead
|
|
11
|
+
- **CDN fallback chain** — Automatically falls back through 3Speak CDN nodes
|
|
12
|
+
- **iOS-optimized** — Native HLS on Safari, manifest prefetching, single-player strategy
|
|
13
|
+
- **TypeScript** — Full type definitions included
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @mantequilla-soft/3speak-player
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Vanilla JavaScript
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<video id="player" playsinline></video>
|
|
27
|
+
|
|
28
|
+
<script type="module">
|
|
29
|
+
import { Player } from '@mantequilla-soft/3speak-player';
|
|
30
|
+
|
|
31
|
+
const player = new Player({ muted: true, loop: true });
|
|
32
|
+
player.attach(document.getElementById('player'));
|
|
33
|
+
player.load('author/permlink');
|
|
34
|
+
|
|
35
|
+
player.on('ready', ({ isVertical }) => {
|
|
36
|
+
console.log('Video is', isVertical ? 'vertical' : 'horizontal');
|
|
37
|
+
player.play();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
player.on('timeupdate', ({ currentTime, duration }) => {
|
|
41
|
+
console.log(`${currentTime.toFixed(1)}s / ${duration.toFixed(1)}s`);
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### React
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { usePlayer } from '@mantequilla-soft/3speak-player/react';
|
|
50
|
+
|
|
51
|
+
function VideoPlayer({ author, permlink }) {
|
|
52
|
+
const { ref, state, togglePlay, setMuted } = usePlayer({
|
|
53
|
+
autoLoad: `${author}/${permlink}`,
|
|
54
|
+
autoPlay: true,
|
|
55
|
+
muted: true,
|
|
56
|
+
loop: true,
|
|
57
|
+
onReady: ({ isVertical }) => console.log('vertical?', isVertical),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
<video ref={ref} playsInline style={{ width: '100%' }} />
|
|
63
|
+
<button onClick={togglePlay}>{state.paused ? 'Play' : 'Pause'}</button>
|
|
64
|
+
<button onClick={() => setMuted(!state.muted)}>
|
|
65
|
+
{state.muted ? 'Unmute' : 'Mute'}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Vue 3
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<template>
|
|
76
|
+
<video ref="videoEl" playsinline />
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup>
|
|
80
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
81
|
+
import { Player } from '@mantequilla-soft/3speak-player';
|
|
82
|
+
|
|
83
|
+
const videoEl = ref(null);
|
|
84
|
+
let player;
|
|
85
|
+
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
player = new Player({ muted: true, loop: true });
|
|
88
|
+
player.attach(videoEl.value);
|
|
89
|
+
player.load('author/permlink');
|
|
90
|
+
player.on('ready', () => player.play());
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
onUnmounted(() => player?.destroy());
|
|
94
|
+
</script>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Svelte
|
|
98
|
+
|
|
99
|
+
```svelte
|
|
100
|
+
<script>
|
|
101
|
+
import { onMount, onDestroy } from 'svelte';
|
|
102
|
+
import { Player } from '@mantequilla-soft/3speak-player';
|
|
103
|
+
|
|
104
|
+
let videoEl;
|
|
105
|
+
let player;
|
|
106
|
+
|
|
107
|
+
onMount(() => {
|
|
108
|
+
player = new Player({ muted: true, loop: true });
|
|
109
|
+
player.attach(videoEl);
|
|
110
|
+
player.load('author/permlink');
|
|
111
|
+
player.on('ready', () => player.play());
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
onDestroy(() => player?.destroy());
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<video bind:this={videoEl} playsinline />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## API Reference
|
|
121
|
+
|
|
122
|
+
### `Player`
|
|
123
|
+
|
|
124
|
+
Single video player instance.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const player = new Player(config?: PlayerConfig);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Config:**
|
|
131
|
+
| Option | Type | Default | Description |
|
|
132
|
+
|--------|------|---------|-------------|
|
|
133
|
+
| `apiBase` | `string` | `'https://play.3speak.tv'` | 3Speak player API URL |
|
|
134
|
+
| `debug` | `boolean` | `false` | Enable console logging |
|
|
135
|
+
| `muted` | `boolean` | `true` | Start muted (needed for autoplay) |
|
|
136
|
+
| `loop` | `boolean` | `false` | Loop playback |
|
|
137
|
+
| `hlsConfig` | `object` | `{}` | hls.js config overrides |
|
|
138
|
+
| `autopause` | `boolean` | `false` | Auto-pause when scrolled out of viewport |
|
|
139
|
+
| `resume` | `boolean` | `false` | Resume playback from last position (localStorage) |
|
|
140
|
+
|
|
141
|
+
**Methods:**
|
|
142
|
+
```ts
|
|
143
|
+
player.attach(videoElement) // Attach to a <video> element
|
|
144
|
+
player.load('author/permlink') // Load by 3Speak ref (fetches HLS URL)
|
|
145
|
+
player.load({ url, fallbacks, poster }) // Load from direct source
|
|
146
|
+
player.play() // Play
|
|
147
|
+
player.pause() // Pause
|
|
148
|
+
player.togglePlay() // Toggle play/pause
|
|
149
|
+
player.seek(time) // Seek to time in seconds
|
|
150
|
+
player.setMuted(boolean) // Set mute state
|
|
151
|
+
player.setVolume(0-1) // Set volume
|
|
152
|
+
player.setLoop(boolean) // Set loop mode
|
|
153
|
+
player.setPlaybackRate(rate) // Set speed (0.5, 1, 2, etc.)
|
|
154
|
+
player.togglePip() // Toggle Picture-in-Picture
|
|
155
|
+
player.toggleFullscreen() // Toggle fullscreen
|
|
156
|
+
player.getQualities() // Get available quality levels (hls.js only)
|
|
157
|
+
player.setQuality(index) // Set quality (-1 for auto, hls.js only)
|
|
158
|
+
player.getCurrentQuality() // Get current quality index
|
|
159
|
+
player.setAudioOnly(boolean) // Audio-only mode (hides video)
|
|
160
|
+
player.getThumbnailAt(time) // Get thumbnail URL at time (stub)
|
|
161
|
+
player.enableAutopause() // Enable auto-pause on scroll out
|
|
162
|
+
player.disableAutopause() // Disable auto-pause
|
|
163
|
+
player.clearResumePosition(ref?) // Clear saved resume position
|
|
164
|
+
player.getState() // Get current PlayerState
|
|
165
|
+
player.detach() // Detach from element
|
|
166
|
+
player.destroy() // Destroy and release resources
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Events:**
|
|
170
|
+
```ts
|
|
171
|
+
player.on('ready', ({ isVertical, width, height }) => {})
|
|
172
|
+
player.on('play', () => {})
|
|
173
|
+
player.on('pause', () => {})
|
|
174
|
+
player.on('ended', () => {})
|
|
175
|
+
player.on('timeupdate', ({ currentTime, duration, paused }) => {})
|
|
176
|
+
player.on('error', ({ message, fatal }) => {})
|
|
177
|
+
player.on('fallback', ({ url, index }) => {})
|
|
178
|
+
player.on('loading', (isLoading) => {})
|
|
179
|
+
player.on('resize', ({ width, height, isVertical }) => {})
|
|
180
|
+
player.on('buffered', (progress) => {})
|
|
181
|
+
player.on('pip', (active) => {})
|
|
182
|
+
player.on('fullscreen', (active) => {})
|
|
183
|
+
player.on('qualitychange', ({ index, height, width, bitrate }) => {})
|
|
184
|
+
player.on('visibility', (visible) => {})
|
|
185
|
+
player.on('resume', ({ time, ref }) => {})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `PlayerPool`
|
|
189
|
+
|
|
190
|
+
Manage multiple players for feed/shorts UIs.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const pool = new PlayerPool(config?: PlayerConfig);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
pool.add(id, videoElement, source?) // Add a player
|
|
198
|
+
pool.addByRef(id, el, author, perm) // Add + load by 3Speak ref
|
|
199
|
+
pool.get(id) // Get player by id
|
|
200
|
+
pool.remove(id) // Remove + destroy player
|
|
201
|
+
pool.activate(id) // Play this, pause all others
|
|
202
|
+
pool.pauseAll() // Pause all
|
|
203
|
+
pool.setAllMuted(boolean) // Mute/unmute all
|
|
204
|
+
pool.setAllLoop(boolean) // Set loop on all
|
|
205
|
+
pool.retainOnly(ids) // Keep only these, destroy rest
|
|
206
|
+
pool.prefetch(hlsUrl) // Prefetch manifest (CDN warm)
|
|
207
|
+
pool.prefetchByRef(author, permlink) // Prefetch by 3Speak ref
|
|
208
|
+
pool.destroy() // Destroy everything
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `ThreeSpeakApi`
|
|
212
|
+
|
|
213
|
+
Direct API access.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { ThreeSpeakApi } from '@mantequilla-soft/3speak-player';
|
|
217
|
+
|
|
218
|
+
const api = new ThreeSpeakApi('https://play.3speak.tv');
|
|
219
|
+
const meta = await api.fetchVideoMetadata('author', 'permlink');
|
|
220
|
+
const source = await api.fetchSource('author', 'permlink');
|
|
221
|
+
await api.prefetchManifest(source.url);
|
|
222
|
+
await api.recordView('author', 'permlink');
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `detectPlatform()`
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { detectPlatform } from '@mantequilla-soft/3speak-player';
|
|
229
|
+
|
|
230
|
+
const platform = detectPlatform();
|
|
231
|
+
// { isIOS, isSafari, supportsNativeHLS, supportsMSE, supportsHlsJs }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `canAutoplay()`
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import { canAutoplay } from '@mantequilla-soft/3speak-player';
|
|
238
|
+
|
|
239
|
+
// Test muted autoplay (default)
|
|
240
|
+
const canMuted = await canAutoplay();
|
|
241
|
+
|
|
242
|
+
// Test unmuted autoplay
|
|
243
|
+
const canUnmuted = await canAutoplay(false);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Results are cached — safe to call multiple times.
|
|
247
|
+
|
|
248
|
+
### React Hooks
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import { usePlayer, usePlayerPool } from '@mantequilla-soft/3speak-player/react';
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**`usePlayer(options)`** — Single player hook (see Quick Start above)
|
|
255
|
+
|
|
256
|
+
**`usePlayerPool(options)`** — Pool hook for shorts/feeds:
|
|
257
|
+
```tsx
|
|
258
|
+
function ShortsFeed({ videos }) {
|
|
259
|
+
const { pool, add, activate, setAllMuted, retainOnly } = usePlayerPool({
|
|
260
|
+
muted: true,
|
|
261
|
+
loop: true,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// In your scroll handler:
|
|
265
|
+
// add(video.id, videoElement, source)
|
|
266
|
+
// activate(currentVideoId)
|
|
267
|
+
// retainOnly(visibleVideoIds)
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## How It Works
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
┌─────────────────────────────────────────────┐
|
|
275
|
+
│ Your App │
|
|
276
|
+
│ (React / Vue / Svelte / Vanilla JS) │
|
|
277
|
+
├─────────────────────────────────────────────┤
|
|
278
|
+
│ @mantequilla-soft/3speak-player │
|
|
279
|
+
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
|
|
280
|
+
│ │ Player │ │ PlayerPool│ │ API │ │
|
|
281
|
+
│ └─────┬────┘ └─────┬─────┘ └────┬─────┘ │
|
|
282
|
+
│ │ │ │ │
|
|
283
|
+
│ ┌─────▼──────────────▼──────┐ ┌────▼─────┐ │
|
|
284
|
+
│ │ HLS Engine │ │ 3Speak │ │
|
|
285
|
+
│ │ ┌─────────┐ ┌─────────┐ │ │ embed │ │
|
|
286
|
+
│ │ │ hls.js │ │ Native │ │ │ API │ │
|
|
287
|
+
│ │ │(Chrome) │ │ (Safari)│ │ │ │ │
|
|
288
|
+
│ │ └────┬────┘ └────┬────┘ │ └──────────┘ │
|
|
289
|
+
│ └───────┼───────────┼──────┘ │
|
|
290
|
+
├──────────┼───────────┼──────────────────────┤
|
|
291
|
+
│ <video> <video> │
|
|
292
|
+
│ elements elements │
|
|
293
|
+
└─────────────────────────────────────────────┘
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
- **Safari/iOS**: Uses native HLS (just sets `video.src = m3u8`). Zero JavaScript HLS overhead.
|
|
297
|
+
- **Chrome/Firefox/Edge**: Uses hls.js (MediaSource Extensions) to play HLS streams.
|
|
298
|
+
- **CDN fallback**: If the primary CDN fails, automatically tries fallback nodes.
|
|
299
|
+
- **iOS single-player**: iOS only allows one active video — the pool handles this transparently.
|
|
300
|
+
|
|
301
|
+
## Migrating from iframes
|
|
302
|
+
|
|
303
|
+
If you're currently using `<iframe src="https://play.3speak.tv/embed?v=...">`, replace with:
|
|
304
|
+
|
|
305
|
+
```diff
|
|
306
|
+
- <iframe src="https://play.3speak.tv/embed?v=author/permlink&controls=0" />
|
|
307
|
+
+ <video ref={videoRef} playsinline />
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
```diff
|
|
311
|
+
- // postMessage to control playback
|
|
312
|
+
- iframe.contentWindow.postMessage({ type: 'play' }, '*');
|
|
313
|
+
+ // Direct control
|
|
314
|
+
+ player.play();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
No more cross-origin restrictions, no more postMessage timing issues, no more iOS iframe throttling.
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
MIT
|