ts-video-player 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-video-player",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "A modern, framework-agnostic video player with deep stx integration",
5
5
  "author": "Chris Breuer <chris@stacksjs.org>",
6
6
  "license": "MIT",
@@ -8,9 +8,7 @@
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
11
- "sideEffects": [
12
- "./dist/cdn.min.js"
13
- ],
11
+ "sideEffects": true,
14
12
  "exports": {
15
13
  ".": {
16
14
  "types": "./dist/index.d.ts",
@@ -62,7 +60,7 @@
62
60
  },
63
61
  "devDependencies": {
64
62
  "@types/bun": "latest",
65
- "typescript": "^5.7.0"
63
+ "typescript": "^7.0.2"
66
64
  },
67
65
  "peerDependencies": {
68
66
  "stx": ">=0.65.0"
@@ -65,7 +65,7 @@ function getExtension(src: string): string {
65
65
  /**
66
66
  * Get MIME type for a source
67
67
  */
68
- function getMimeType(src: string): string {
68
+ export function getMimeType(src: string): string {
69
69
  const ext = getExtension(src)
70
70
  const mimeTypes: Record<string, string> = {
71
71
  mp4: 'video/mp4',
@@ -35,7 +35,7 @@ export type { Provider, ProviderLoader, ProviderType } from '../types'
35
35
  // =============================================================================
36
36
 
37
37
  import type { ProviderLoader, Src } from '../types'
38
- import { html5Loader } from './html5'
38
+ import { getMimeType, html5Loader } from './html5'
39
39
  import { youtubeLoader } from './youtube'
40
40
  import { vimeoLoader } from './vimeo'
41
41
  import { hlsLoader } from './hls'
@@ -52,6 +52,32 @@ export const defaultLoaders: ProviderLoader[] = [
52
52
  html5Loader, // HTML5 as fallback
53
53
  ]
54
54
 
55
+ /**
56
+ * The URL of a source object that says nothing about its kind: `{ src }`,
57
+ * perhaps with a quality or size, but no `type`. Only its URL can tell a
58
+ * YouTube link from an MP4, so that is what the loaders are asked about.
59
+ */
60
+ function untypedUrl(src: Src): string | null {
61
+ if (!src || typeof src !== 'object' || typeof src.src !== 'string')
62
+ return null
63
+ return 'type' in src && src.type ? null : src.src
64
+ }
65
+
66
+ /** What a loader's source type is written as, for a source it matched by URL alone. */
67
+ const TYPE_FOR_LOADER: Record<string, (url: string) => string> = {
68
+ youtube: () => 'youtube',
69
+ vimeo: () => 'vimeo',
70
+ hls: () => 'application/x-mpegurl',
71
+ dash: () => 'dash',
72
+ video: url => getMimeType(url),
73
+ }
74
+
75
+ function withType(src: Src, loader: ProviderLoader): Src {
76
+ const url = untypedUrl(src)
77
+ const type = url && TYPE_FOR_LOADER[loader.type]
78
+ return url && type ? { ...(src as object), type: type(url) } as Src : src
79
+ }
80
+
55
81
  /**
56
82
  * Find the appropriate provider loader for a source
57
83
  */
@@ -61,6 +87,13 @@ export function findLoader(src: Src, loaders: ProviderLoader[] = defaultLoaders)
61
87
  return loader
62
88
  }
63
89
  }
90
+ const url = untypedUrl(src)
91
+ if (url) {
92
+ for (const loader of loaders) {
93
+ if (loader.canPlay(url))
94
+ return loader
95
+ }
96
+ }
64
97
  return null
65
98
  }
66
99
 
@@ -75,7 +108,8 @@ export function findSourceCandidates(sources: readonly Src[], loaders: ProviderL
75
108
  const candidates: SourceCandidate[] = []
76
109
  sources.forEach((src, index) => {
77
110
  const loader = findLoader(src, loaders)
78
- if (loader) candidates.push({ src, index, loader })
111
+ // Typed for the provider that claimed it, which reads `type` to know how.
112
+ if (loader) candidates.push({ src: withType(src, loader), index, loader })
79
113
  })
80
114
  return candidates
81
115
  }
@@ -145,6 +145,8 @@ declare global {
145
145
  elementId: string | HTMLElement,
146
146
  options: {
147
147
  videoId?: string
148
+ /** Where the iframe is served from, e.g. https://www.youtube-nocookie.com. */
149
+ host?: string
148
150
  width?: number | string
149
151
  height?: number | string
150
152
  playerVars?: Record<string, string | number>
@@ -184,6 +186,8 @@ export class YouTubeProvider extends BaseProvider {
184
186
  private player: YTPlayer | null = null
185
187
  private iframe: HTMLIFrameElement | null = null
186
188
  private pendingVideoId: string | null = null
189
+ /** YouTube's own ready event: the player's methods exist only after it. */
190
+ private playerReady = false
187
191
  private startTime?: number
188
192
  private endTime?: number
189
193
  private timeUpdateInterval: ReturnType<typeof setInterval> | null = null
@@ -227,11 +231,14 @@ export class YouTubeProvider extends BaseProvider {
227
231
  Object.assign(playerVars, this.options.youtubeParams)
228
232
  }
229
233
 
230
- // Use privacy-enhanced mode
231
- playerVars.host = 'https://www.youtube-nocookie.com'
232
234
 
233
- // Create player
234
- this.player = new window.YT!.Player(playerDiv.id, {
235
+ // Create player. The element, not its id: the API looks an id up with
236
+ // document.getElementById, which cannot see into the shadow root the
237
+ // <video-player> element renders into, so an id found nothing there.
238
+ this.player = new window.YT!.Player(playerDiv, {
239
+ // Privacy-enhanced mode. A top-level option: set among playerVars, the
240
+ // API ignored it and embedded from youtube.com.
241
+ host: 'https://www.youtube-nocookie.com',
235
242
  width: '100%',
236
243
  height: '100%',
237
244
  playerVars,
@@ -287,12 +294,15 @@ export class YouTubeProvider extends BaseProvider {
287
294
 
288
295
  this.player?.destroy()
289
296
  this.player = null
297
+ this.playerReady = false
290
298
  this.iframe = null
291
299
  }
292
300
 
293
301
  // === Event Handlers ===
294
302
 
295
303
  private onReady(): void {
304
+ this.playerReady = true
305
+
296
306
  // Start time update polling
297
307
  this.timeUpdateInterval = setInterval(() => {
298
308
  if (this.player && this.lastState === 1) {
@@ -383,7 +393,10 @@ export class YouTubeProvider extends BaseProvider {
383
393
  this.startTime = parsed.startTime
384
394
  this.endTime = parsed.endTime
385
395
 
386
- if (!this.player || !this._ready) {
396
+ // `_ready` means setup finished, which is before YouTube has attached
397
+ // cueVideoById and friends to the player: calling one then threw, and the
398
+ // source was reported as unplayable. onReady loads the pending video.
399
+ if (!this.player || !this._ready || !this.playerReady) {
387
400
  this.pendingVideoId = parsed.videoId
388
401
  return
389
402
  }