@scalemule/gallop 0.0.1
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 +201 -0
- package/dist/EventEmitter-CiUv3YL_.d.cts +12 -0
- package/dist/EventEmitter-CkfpgRij.d.ts +12 -0
- package/dist/chunk-2JQGJ7NX.cjs +40 -0
- package/dist/chunk-PKRNWEEX.cjs +265 -0
- package/dist/chunk-QTV4W7FA.js +2886 -0
- package/dist/chunk-SQPWH6EI.js +38 -0
- package/dist/chunk-UFFGSURS.js +263 -0
- package/dist/chunk-VCNMR5AB.cjs +2893 -0
- package/dist/element.cjs +342 -0
- package/dist/element.d.cts +38 -0
- package/dist/element.d.ts +38 -0
- package/dist/element.js +340 -0
- package/dist/gallop.embed.global.js +568 -0
- package/dist/gallop.umd.global.js +568 -0
- package/dist/iframe.cjs +11 -0
- package/dist/iframe.d.cts +50 -0
- package/dist/iframe.d.ts +50 -0
- package/dist/iframe.js +2 -0
- package/dist/index.cjs +11 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +2 -0
- package/dist/react.cjs +77 -0
- package/dist/react.d.cts +34 -0
- package/dist/react.d.ts +34 -0
- package/dist/react.js +74 -0
- package/dist/types-D9Oqcpr1.d.cts +235 -0
- package/dist/types-D9Oqcpr1.d.ts +235 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScaleMule Inc.
|
|
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,201 @@
|
|
|
1
|
+
# Gallop Web SDK
|
|
2
|
+
|
|
3
|
+
**ScaleMule's Video Player for Web Applications**
|
|
4
|
+
|
|
5
|
+
*TypeScript • MSE • Zero Dependencies*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
The Gallop Web SDK is a high-performance video player built with TypeScript and native browser APIs. It provides ScaleMule customers with a premium streaming experience including social features, deep analytics, and optimized playback.
|
|
12
|
+
|
|
13
|
+
### Key Features
|
|
14
|
+
|
|
15
|
+
- **Instant Start** - Pre-warming system for TikTok-like instant playback
|
|
16
|
+
- **50% Bandwidth Savings** - VP9/HEVC codec support
|
|
17
|
+
- **Timeline Comments** - Social video features via scalemule-chat
|
|
18
|
+
- **Deep Analytics** - Heatmaps, engagement tracking, TTFF metrics
|
|
19
|
+
- **Zero Dependencies** - Custom engine (Phase 3+), no HLS.js in production
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @scalemule/gallop
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### React
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { GallopPlayer } from '@scalemule/gallop/react';
|
|
37
|
+
|
|
38
|
+
function VideoPage() {
|
|
39
|
+
return (
|
|
40
|
+
<GallopPlayer
|
|
41
|
+
videoId="your-video-id"
|
|
42
|
+
apiKey="sm_live_xxx"
|
|
43
|
+
onPlay={() => console.log('Playing')}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Playback Analytics + Debugging
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { GallopPlayer } from '@scalemule/gallop/react';
|
|
53
|
+
|
|
54
|
+
<GallopPlayer
|
|
55
|
+
videoId="your-video-id"
|
|
56
|
+
apiKey="sm_live_xxx"
|
|
57
|
+
analytics={{
|
|
58
|
+
enabled: true,
|
|
59
|
+
progressIntervalSeconds: 10,
|
|
60
|
+
includeNetworkInfo: true,
|
|
61
|
+
includeDeviceInfo: true,
|
|
62
|
+
debug: false,
|
|
63
|
+
}}
|
|
64
|
+
onEngineStats={({ stats }) => {
|
|
65
|
+
console.log('HLS stats', stats);
|
|
66
|
+
}}
|
|
67
|
+
/>;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
When `analytics.enabled` is on, the player sends playback telemetry to `POST /v1/videos/{id}/track` with:
|
|
71
|
+
- Core events: `play`, `pause`, `seek`, `complete`, `buffer`, `error`
|
|
72
|
+
- Session ID and playback position
|
|
73
|
+
- QoE metadata (TTFF, rebuffer ratio, quality switches)
|
|
74
|
+
- Network/device context (when available)
|
|
75
|
+
|
|
76
|
+
### Web Component
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<script type="module">
|
|
80
|
+
import '@scalemule/gallop/element';
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<gallop-player
|
|
84
|
+
video-id="your-video-id"
|
|
85
|
+
api-key="sm_live_xxx"
|
|
86
|
+
></gallop-player>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Pre-warming (Instant Start)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { gallop } from '@scalemule/gallop';
|
|
93
|
+
|
|
94
|
+
// Prewarm on hover for instant playback
|
|
95
|
+
thumbnail.addEventListener('mouseenter', () => {
|
|
96
|
+
gallop.prewarm('video-id');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Or use IntersectionObserver for feeds
|
|
100
|
+
const observer = new IntersectionObserver((entries) => {
|
|
101
|
+
entries.forEach(entry => {
|
|
102
|
+
if (entry.isIntersecting) {
|
|
103
|
+
gallop.prewarm(entry.target.dataset.videoId);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}, { rootMargin: '200px' });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Architecture
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
115
|
+
│ Gallop Web │
|
|
116
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
117
|
+
│ UI Layer │
|
|
118
|
+
│ ├── React Components / Web Components │
|
|
119
|
+
│ ├── Controls (play, seek, volume, quality) │
|
|
120
|
+
│ └── Social Features (timeline comments, share) │
|
|
121
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
122
|
+
│ Engine Abstraction │
|
|
123
|
+
│ ├── HLSJSEngine (Phase 1-2, temporary) │
|
|
124
|
+
│ ├── GallopEngine (Phase 3+, custom) │
|
|
125
|
+
│ └── NativeHLSEngine (iOS Safari fallback) │
|
|
126
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
127
|
+
│ Performance Layer │
|
|
128
|
+
│ ├── Pre-warming System │
|
|
129
|
+
│ ├── Low-Latency Startup Profile │
|
|
130
|
+
│ └── Hot-Swap Fallback │
|
|
131
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
132
|
+
│ Browser APIs │
|
|
133
|
+
│ ├── Media Source Extensions (MSE) │
|
|
134
|
+
│ ├── Encrypted Media Extensions (EME) │
|
|
135
|
+
│ └── Web Workers │
|
|
136
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Browser Support
|
|
142
|
+
|
|
143
|
+
| Browser | Version | MSE | DRM |
|
|
144
|
+
|---------|---------|-----|-----|
|
|
145
|
+
| Chrome | 70+ | ✅ | Widevine |
|
|
146
|
+
| Firefox | 78+ | ✅ | Widevine |
|
|
147
|
+
| Safari | 14+ | ✅ | FairPlay |
|
|
148
|
+
| Edge | 79+ | ✅ | Widevine |
|
|
149
|
+
| iOS Safari | 14+ | Native HLS | FairPlay |
|
|
150
|
+
| Android Chrome | 70+ | ✅ | Widevine |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
### Technology Stack
|
|
157
|
+
|
|
158
|
+
| Tool | Purpose |
|
|
159
|
+
|------|---------|
|
|
160
|
+
| TypeScript | Primary language (strict mode) |
|
|
161
|
+
| tsup | Build pipeline (ESM + CJS + UMD) |
|
|
162
|
+
| Vitest | Unit testing |
|
|
163
|
+
| Playwright | E2E testing |
|
|
164
|
+
|
|
165
|
+
### Scripts
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Development
|
|
169
|
+
npm run dev # Start dev server with hot reload
|
|
170
|
+
npm run build # Production build
|
|
171
|
+
npm run test # Run unit tests
|
|
172
|
+
npm run test:e2e # Run Playwright tests
|
|
173
|
+
|
|
174
|
+
# Quality
|
|
175
|
+
npm run lint # ESLint
|
|
176
|
+
npm run typecheck # TypeScript type checking
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Documentation
|
|
182
|
+
|
|
183
|
+
| Document | Description |
|
|
184
|
+
|----------|-------------|
|
|
185
|
+
| [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) | Development phases, technical details |
|
|
186
|
+
| [Core Architecture](../scalemule-gallop/docs/ARCHITECTURE.md) | Shared architecture decisions |
|
|
187
|
+
| [Cross-Platform](../scalemule-gallop/docs/CROSS_PLATFORM.md) | Platform comparison |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Related Repos
|
|
192
|
+
|
|
193
|
+
- **[scalemule-gallop](../scalemule-gallop)** - Core documentation (shared architecture)
|
|
194
|
+
- **[scalemule-gallop-ios](../scalemule-gallop-ios)** - iOS SDK (Swift)
|
|
195
|
+
- **[scalemule-gallop-android](../scalemule-gallop-android)** - Android SDK (Kotlin)
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
Proprietary - ScaleMule, Inc.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { d as GallopEventMap, c as GallopEventCallback } from './types-D9Oqcpr1.cjs';
|
|
2
|
+
|
|
3
|
+
declare class EventEmitter {
|
|
4
|
+
private listeners;
|
|
5
|
+
on<K extends keyof GallopEventMap>(event: K, callback: GallopEventCallback<GallopEventMap[K]>): void;
|
|
6
|
+
off<K extends keyof GallopEventMap>(event: K, callback: GallopEventCallback<GallopEventMap[K]>): void;
|
|
7
|
+
once<K extends keyof GallopEventMap>(event: K, callback: GallopEventCallback<GallopEventMap[K]>): void;
|
|
8
|
+
protected emit<K extends keyof GallopEventMap>(event: K, ...args: GallopEventMap[K] extends void ? [] : [GallopEventMap[K]]): void;
|
|
9
|
+
removeAllListeners(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { EventEmitter as E };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { d as GallopEventMap, c as GallopEventCallback } from './types-D9Oqcpr1.js';
|
|
2
|
+
|
|
3
|
+
declare class EventEmitter {
|
|
4
|
+
private listeners;
|
|
5
|
+
on<K extends keyof GallopEventMap>(event: K, callback: GallopEventCallback<GallopEventMap[K]>): void;
|
|
6
|
+
off<K extends keyof GallopEventMap>(event: K, callback: GallopEventCallback<GallopEventMap[K]>): void;
|
|
7
|
+
once<K extends keyof GallopEventMap>(event: K, callback: GallopEventCallback<GallopEventMap[K]>): void;
|
|
8
|
+
protected emit<K extends keyof GallopEventMap>(event: K, ...args: GallopEventMap[K] extends void ? [] : [GallopEventMap[K]]): void;
|
|
9
|
+
removeAllListeners(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { EventEmitter as E };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/EventEmitter.ts
|
|
4
|
+
var EventEmitter = class {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
7
|
+
}
|
|
8
|
+
on(event, callback) {
|
|
9
|
+
if (!this.listeners.has(event)) {
|
|
10
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
11
|
+
}
|
|
12
|
+
this.listeners.get(event).add(callback);
|
|
13
|
+
}
|
|
14
|
+
off(event, callback) {
|
|
15
|
+
this.listeners.get(event)?.delete(callback);
|
|
16
|
+
}
|
|
17
|
+
once(event, callback) {
|
|
18
|
+
const wrapper = ((...args) => {
|
|
19
|
+
this.off(event, wrapper);
|
|
20
|
+
callback(...args);
|
|
21
|
+
});
|
|
22
|
+
this.on(event, wrapper);
|
|
23
|
+
}
|
|
24
|
+
emit(event, ...args) {
|
|
25
|
+
const set = this.listeners.get(event);
|
|
26
|
+
if (!set) return;
|
|
27
|
+
for (const listener of set) {
|
|
28
|
+
try {
|
|
29
|
+
listener(...args);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`[Gallop] Error in ${event} listener:`, err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
removeAllListeners() {
|
|
36
|
+
this.listeners.clear();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
exports.EventEmitter = EventEmitter;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk2JQGJ7NX_cjs = require('./chunk-2JQGJ7NX.cjs');
|
|
4
|
+
|
|
5
|
+
// src/iframe/GallopIframeController.ts
|
|
6
|
+
var HASH_CONFIG_ALLOWLIST = [
|
|
7
|
+
"autoplay",
|
|
8
|
+
"muted",
|
|
9
|
+
"loop",
|
|
10
|
+
"controls",
|
|
11
|
+
"startTime",
|
|
12
|
+
"preferredQuality",
|
|
13
|
+
"aspectRatio",
|
|
14
|
+
"theme",
|
|
15
|
+
"debug",
|
|
16
|
+
"doNotTrack",
|
|
17
|
+
"analytics",
|
|
18
|
+
"pageUrl"
|
|
19
|
+
];
|
|
20
|
+
var MAX_HASH_CONFIG_BYTES = 4096;
|
|
21
|
+
function sanitizeConfigForHash(config) {
|
|
22
|
+
const safe = {};
|
|
23
|
+
for (const key of HASH_CONFIG_ALLOWLIST) {
|
|
24
|
+
if (config[key] !== void 0) {
|
|
25
|
+
safe[key] = config[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return safe;
|
|
29
|
+
}
|
|
30
|
+
var GallopIframeController = class extends chunk2JQGJ7NX_cjs.EventEmitter {
|
|
31
|
+
constructor(container, config) {
|
|
32
|
+
super();
|
|
33
|
+
this.container = container;
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.targetOrigin = null;
|
|
36
|
+
this.isConnected = false;
|
|
37
|
+
this.pendingCalls = /* @__PURE__ */ new Map();
|
|
38
|
+
this.stateCache = {};
|
|
39
|
+
this.cachedQualityLevels = [];
|
|
40
|
+
this.cachedCurrentQuality = -1;
|
|
41
|
+
this.cachedDiagnostics = {};
|
|
42
|
+
this.sessionId = Math.random().toString(36).substring(7);
|
|
43
|
+
this.handshakeInterval = null;
|
|
44
|
+
this.iframe = this.createIframe();
|
|
45
|
+
this.container.appendChild(this.iframe);
|
|
46
|
+
this.boundHandler = this.handleMessage.bind(this);
|
|
47
|
+
window.addEventListener("message", this.boundHandler);
|
|
48
|
+
this.startHandshake();
|
|
49
|
+
}
|
|
50
|
+
createIframe() {
|
|
51
|
+
const iframe = document.createElement("iframe");
|
|
52
|
+
const videoId = this.config.videoId || "unknown";
|
|
53
|
+
const baseUrl = this.config.apiBaseUrl ? `${this.config.apiBaseUrl.replace(/\/$/, "")}/v1/videos/embed` : "https://api.scalemule.com/v1/videos/embed";
|
|
54
|
+
const url = new URL(`${baseUrl}/${videoId}`);
|
|
55
|
+
if (this.config.embedToken) {
|
|
56
|
+
url.searchParams.set("token", this.config.embedToken);
|
|
57
|
+
}
|
|
58
|
+
const safeConfig = sanitizeConfigForHash(this.config);
|
|
59
|
+
if (!safeConfig.pageUrl && typeof window !== "undefined") {
|
|
60
|
+
safeConfig.pageUrl = window.location.href;
|
|
61
|
+
}
|
|
62
|
+
const json = JSON.stringify(safeConfig);
|
|
63
|
+
const encoded = btoa(json).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
64
|
+
if (encoded.length > MAX_HASH_CONFIG_BYTES) {
|
|
65
|
+
console.warn("[Gallop] Config too large for hash, using defaults");
|
|
66
|
+
} else {
|
|
67
|
+
url.hash = `config=${encoded}`;
|
|
68
|
+
}
|
|
69
|
+
iframe.src = url.toString();
|
|
70
|
+
iframe.style.width = "100%";
|
|
71
|
+
iframe.style.height = "100%";
|
|
72
|
+
iframe.style.border = "0";
|
|
73
|
+
iframe.allow = "autoplay; fullscreen; picture-in-picture; encrypted-media";
|
|
74
|
+
iframe.title = "Gallop Video Player";
|
|
75
|
+
return iframe;
|
|
76
|
+
}
|
|
77
|
+
startHandshake() {
|
|
78
|
+
let attempts = 0;
|
|
79
|
+
this.handshakeInterval = setInterval(() => {
|
|
80
|
+
if (this.isConnected || attempts >= 3) {
|
|
81
|
+
if (this.handshakeInterval) clearInterval(this.handshakeInterval);
|
|
82
|
+
this.handshakeInterval = null;
|
|
83
|
+
if (!this.isConnected && attempts >= 3) {
|
|
84
|
+
this.emit("error", { code: "CONNECT_TIMEOUT", message: "iframe handshake timed out" });
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.sendMessage("gallop:ping", { sessionId: this.sessionId }, "*");
|
|
89
|
+
attempts++;
|
|
90
|
+
}, 1e3);
|
|
91
|
+
}
|
|
92
|
+
handleMessage(event) {
|
|
93
|
+
const data = event.data;
|
|
94
|
+
if (!data || typeof data.type !== "string" || !data.type.startsWith("gallop:")) return;
|
|
95
|
+
if (event.source !== this.iframe.contentWindow) return;
|
|
96
|
+
if (!this.isConnected) {
|
|
97
|
+
if (data.type === "gallop:hello") {
|
|
98
|
+
this.sendMessage("gallop:ping", { sessionId: this.sessionId }, "*");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (data.type === "gallop:pong") {
|
|
102
|
+
this.targetOrigin = event.origin;
|
|
103
|
+
this.isConnected = true;
|
|
104
|
+
if (this.handshakeInterval) {
|
|
105
|
+
clearInterval(this.handshakeInterval);
|
|
106
|
+
this.handshakeInterval = null;
|
|
107
|
+
}
|
|
108
|
+
if (data.diagnostics) {
|
|
109
|
+
this.cachedDiagnostics = data.diagnostics;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (data.type === "gallop:error") {
|
|
114
|
+
this.emit("error", { code: data.code || "EMBED_LOAD_FAILED", message: data.message || "Embed error" });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (event.origin !== this.targetOrigin) return;
|
|
120
|
+
switch (data.type) {
|
|
121
|
+
case "gallop:event":
|
|
122
|
+
if (data.event === "qualitylevels" && data.data?.levels) {
|
|
123
|
+
this.cachedQualityLevels = data.data.levels;
|
|
124
|
+
}
|
|
125
|
+
if (data.event === "qualitychange" && data.data?.level) {
|
|
126
|
+
this.cachedCurrentQuality = data.data.level.index ?? -1;
|
|
127
|
+
}
|
|
128
|
+
this.emit(data.event, data.data);
|
|
129
|
+
break;
|
|
130
|
+
case "gallop:state":
|
|
131
|
+
this.stateCache = data.state;
|
|
132
|
+
break;
|
|
133
|
+
case "gallop:response":
|
|
134
|
+
this.handleResponse(data);
|
|
135
|
+
break;
|
|
136
|
+
case "gallop:error":
|
|
137
|
+
this.emit("error", { code: data.code || "UNKNOWN", message: data.message || "Unknown error" });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
handleResponse(data) {
|
|
142
|
+
const call = this.pendingCalls.get(data.callId);
|
|
143
|
+
if (call) {
|
|
144
|
+
clearTimeout(call.timeout);
|
|
145
|
+
if (data.error) {
|
|
146
|
+
call.reject(data.error);
|
|
147
|
+
} else {
|
|
148
|
+
call.resolve(data.result);
|
|
149
|
+
}
|
|
150
|
+
this.pendingCalls.delete(data.callId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
sendMessage(type, payload = {}, overrideOrigin) {
|
|
154
|
+
const target = overrideOrigin || this.targetOrigin;
|
|
155
|
+
if (!target) return;
|
|
156
|
+
this.iframe.contentWindow?.postMessage({
|
|
157
|
+
type,
|
|
158
|
+
...payload,
|
|
159
|
+
version: 1
|
|
160
|
+
}, target);
|
|
161
|
+
}
|
|
162
|
+
callMethod(method, ...args) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
if (!this.isConnected && method !== "destroy") {
|
|
165
|
+
reject(new Error("Player not connected"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (this.pendingCalls.size >= 20) {
|
|
169
|
+
reject(new Error("Too many pending calls"));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const callId = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2);
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
this.pendingCalls.delete(callId);
|
|
175
|
+
reject(new Error(`Method ${method} timed out`));
|
|
176
|
+
}, 5e3);
|
|
177
|
+
this.pendingCalls.set(callId, { resolve, reject, timeout });
|
|
178
|
+
this.sendMessage("gallop:method", { method, args, callId });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// --- GallopPlayer Implementation ---
|
|
182
|
+
get currentTime() {
|
|
183
|
+
return this.stateCache.currentTime ?? 0;
|
|
184
|
+
}
|
|
185
|
+
get duration() {
|
|
186
|
+
return this.stateCache.duration ?? 0;
|
|
187
|
+
}
|
|
188
|
+
get paused() {
|
|
189
|
+
return this.stateCache.paused ?? true;
|
|
190
|
+
}
|
|
191
|
+
get status() {
|
|
192
|
+
return this.stateCache.status ?? "loading";
|
|
193
|
+
}
|
|
194
|
+
get isFullscreen() {
|
|
195
|
+
return this.stateCache.isFullscreen ?? false;
|
|
196
|
+
}
|
|
197
|
+
get volume() {
|
|
198
|
+
return this.stateCache.volume ?? 1;
|
|
199
|
+
}
|
|
200
|
+
set volume(v) {
|
|
201
|
+
void this.callMethod("setVolume", v);
|
|
202
|
+
}
|
|
203
|
+
get muted() {
|
|
204
|
+
return this.stateCache.muted ?? false;
|
|
205
|
+
}
|
|
206
|
+
set muted(m) {
|
|
207
|
+
void this.callMethod("setMuted", m);
|
|
208
|
+
}
|
|
209
|
+
get playbackRate() {
|
|
210
|
+
return this.stateCache.playbackRate ?? 1;
|
|
211
|
+
}
|
|
212
|
+
set playbackRate(r) {
|
|
213
|
+
void this.callMethod("setPlaybackRate", r);
|
|
214
|
+
}
|
|
215
|
+
play() {
|
|
216
|
+
return this.callMethod("play");
|
|
217
|
+
}
|
|
218
|
+
pause() {
|
|
219
|
+
return this.callMethod("pause");
|
|
220
|
+
}
|
|
221
|
+
seek(time) {
|
|
222
|
+
return this.callMethod("seek", time);
|
|
223
|
+
}
|
|
224
|
+
setQualityLevel(index) {
|
|
225
|
+
return this.callMethod("setQualityLevel", index);
|
|
226
|
+
}
|
|
227
|
+
setAutoQuality() {
|
|
228
|
+
return this.callMethod("setAutoQuality");
|
|
229
|
+
}
|
|
230
|
+
toggleFullscreen() {
|
|
231
|
+
return this.callMethod("toggleFullscreen");
|
|
232
|
+
}
|
|
233
|
+
getQualityLevels() {
|
|
234
|
+
return this.cachedQualityLevels;
|
|
235
|
+
}
|
|
236
|
+
getCurrentQuality() {
|
|
237
|
+
return this.cachedCurrentQuality;
|
|
238
|
+
}
|
|
239
|
+
getDiagnostics() {
|
|
240
|
+
return this.cachedDiagnostics;
|
|
241
|
+
}
|
|
242
|
+
query(key) {
|
|
243
|
+
return this.callMethod("query", key);
|
|
244
|
+
}
|
|
245
|
+
get connected() {
|
|
246
|
+
return this.isConnected;
|
|
247
|
+
}
|
|
248
|
+
destroy() {
|
|
249
|
+
if (this.handshakeInterval) {
|
|
250
|
+
clearInterval(this.handshakeInterval);
|
|
251
|
+
this.handshakeInterval = null;
|
|
252
|
+
}
|
|
253
|
+
for (const [, call] of this.pendingCalls) {
|
|
254
|
+
clearTimeout(call.timeout);
|
|
255
|
+
call.reject(new Error("Player destroyed"));
|
|
256
|
+
}
|
|
257
|
+
this.pendingCalls.clear();
|
|
258
|
+
window.removeEventListener("message", this.boundHandler);
|
|
259
|
+
this.iframe.remove();
|
|
260
|
+
this.emit("destroy");
|
|
261
|
+
this.removeAllListeners();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
exports.GallopIframeController = GallopIframeController;
|