ts-video-player 0.1.9 → 0.1.10

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.
@@ -148,6 +148,8 @@ class BaseProvider2 {
148
148
  }
149
149
  destroy() {
150
150
  this._ready = false;
151
+ this.stopEmbedFullscreen?.();
152
+ this.stopEmbedFullscreen = null;
151
153
  this.cleanup();
152
154
  this.events.removeAllListeners();
153
155
  }
@@ -167,6 +169,25 @@ class BaseProvider2 {
167
169
  return [];
168
170
  }
169
171
  setAudioTrack(_trackId) {}
172
+ stopEmbedFullscreen = null;
173
+ announceEmbedFeatures() {
174
+ if (typeof document === "undefined" || typeof HTMLMediaElement === "undefined")
175
+ return;
176
+ const fullscreenAvailability = this.getFeatureAvailability("fullscreen");
177
+ const pipAvailability = this.getFeatureAvailability("pip");
178
+ const volumeAvailability = this.getFeatureAvailability("volume");
179
+ this.events.emit("statechange", {
180
+ canFullscreen: fullscreenAvailability === "available",
181
+ canPictureInPicture: pipAvailability === "available",
182
+ fullscreenAvailability,
183
+ pipAvailability,
184
+ volumeAvailability
185
+ });
186
+ this.events.emit("availabilitychange", "fullscreen", fullscreenAvailability);
187
+ this.events.emit("availabilitychange", "pip", pipAvailability);
188
+ this.events.emit("availabilitychange", "volume", volumeAvailability);
189
+ this.stopEmbedFullscreen ??= onFullscreenChange2((fullscreen) => this.events.emit("fullscreenchange", fullscreen));
190
+ }
170
191
  getFeatureAvailability(feature) {
171
192
  const el = this.mediaElement;
172
193
  const media = el instanceof HTMLMediaElement ? el : null;
@@ -267,6 +288,8 @@ class HTML5Provider2 extends BaseProvider2 {
267
288
  this.isAudio = this.options.mediaType === "audio";
268
289
  this.media = document.createElement(this.isAudio ? "audio" : "video");
269
290
  this.media.className = "ts-video-player__media";
291
+ if (!this.isAudio)
292
+ this.media.style.cssText = "display:block;width:100%;height:100%;object-fit:contain;background:#000";
270
293
  if (this.options.autoplay)
271
294
  this.media.autoplay = true;
272
295
  if (this.options.loop)
@@ -715,6 +738,7 @@ class YouTubeProvider2 extends BaseProvider2 {
715
738
  }, 250);
716
739
  this.events.emit("loadedmetadata");
717
740
  this.events.emit("canplay");
741
+ this.announceEmbedFeatures();
718
742
  if (this.pendingVideoId) {
719
743
  const videoId = this.pendingVideoId;
720
744
  this.pendingVideoId = null;
@@ -1066,10 +1090,9 @@ class VimeoProvider2 extends BaseProvider2 {
1066
1090
  this.events.emit("statechange", {
1067
1091
  videoWidth: width,
1068
1092
  videoHeight: height,
1069
- aspectRatio: width / height || 16 / 9,
1070
- canFullscreen: true,
1071
- canPictureInPicture: false
1093
+ aspectRatio: width / height || 16 / 9
1072
1094
  });
1095
+ this.announceEmbedFeatures();
1073
1096
  }
1074
1097
  attachVimeoEvents() {
1075
1098
  if (!this.player)
@@ -1279,6 +1302,7 @@ class HLSProvider2 extends BaseProvider2 {
1279
1302
  }
1280
1303
  this.video = document.createElement("video");
1281
1304
  this.video.className = "ts-video-player__media";
1305
+ this.video.style.cssText = "display:block;width:100%;height:100%;object-fit:contain;background:#000";
1282
1306
  if (this.options.autoplay)
1283
1307
  this.video.autoplay = true;
1284
1308
  if (this.options.loop)
@@ -3241,7 +3265,8 @@ class Player2 {
3241
3265
  if (this.state.pictureInPicture) {
3242
3266
  await this.exitPiP();
3243
3267
  }
3244
- await this._provider?.enterFullscreen();
3268
+ const media = this._provider?.mediaElement;
3269
+ await enterFullscreen2(this._el, media instanceof HTMLMediaElement ? media : null);
3245
3270
  if (this._options.fullscreenOrientationLock !== false) {
3246
3271
  lockOrientation2("landscape");
3247
3272
  }
@@ -23,7 +23,7 @@ import {
23
23
  MediaErrorDisplay2,
24
24
  MediaRemotePlaybackButton2,
25
25
  registerElements2
26
- } from "../chunk-fvxawgp5.js";
26
+ } from "../chunk-cay3ahqr.js";
27
27
  export {
28
28
  MediaAirplayButton2 as MediaAirplayButton,
29
29
  MediaErrorDisplay2 as MediaErrorDisplay,
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  applyPosition2,
64
64
  MediaSettingsMenu2,
65
65
  registerElements2
66
- } from "./chunk-fvxawgp5.js";
66
+ } from "./chunk-cay3ahqr.js";
67
67
  import {
68
68
  playback2,
69
69
  volume2,
@@ -43,6 +43,18 @@ export declare abstract class BaseProvider implements Provider {
43
43
  setTextTrackMode(_trackId: string, _mode: 'disabled' | 'hidden' | 'showing'): void;
44
44
  getAudioTracks(): AudioTrack[];
45
45
  setAudioTrack(_trackId: string): void;
46
+ private stopEmbedFullscreen;
47
+ /**
48
+ * What an embedded player (YouTube, Vimeo) can do, said once it is ready.
49
+ *
50
+ * The native-media providers say it on loadedmetadata; the embeds said
51
+ * nothing, so fullscreen stayed "unavailable" and its button disabled,
52
+ * though the player can fill the screen like any other. Fullscreen is the
53
+ * container's own (the player's controls come along); picture in picture
54
+ * is the embed's business. Also follows the document into and out of
55
+ * fullscreen, which only the native providers did.
56
+ */
57
+ protected announceEmbedFeatures(): void;
46
58
  getFeatureAvailability(feature: 'volume' | 'fullscreen' | 'pip'): FeatureAvailability;
47
59
  enterFullscreen(): Promise<void>;
48
60
  exitFullscreen(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-video-player",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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",
package/src/player.ts CHANGED
@@ -22,6 +22,7 @@ import { createMediaSession, type MediaSessionOptions } from './core/media-sessi
22
22
  import { lockOrientation, unlockOrientation } from './core/orientation'
23
23
  import { defaultLoaders, findSourceCandidates } from './providers'
24
24
  import { embedCover } from './core/cover'
25
+ import { enterFullscreen as enterFullscreenElement } from './core/features'
25
26
  import { label } from './elements/utils'
26
27
 
27
28
  // =============================================================================
@@ -799,7 +800,12 @@ export class Player implements IPlayer {
799
800
  if (this.state.pictureInPicture) {
800
801
  await this.exitPiP()
801
802
  }
802
- await this._provider?.enterFullscreen()
803
+ // The whole player, controls included: the provider's own container holds
804
+ // only the media, so a fullscreen video had no controls to pause or seek
805
+ // with. A browser that can only take the <video> itself fullscreen (iOS
806
+ // Safari) still gets it, with the system's controls.
807
+ const media = this._provider?.mediaElement
808
+ await enterFullscreenElement(this._el, media instanceof HTMLMediaElement ? media : null)
803
809
  if (this._options.fullscreenOrientationLock !== false) {
804
810
  lockOrientation('landscape')
805
811
  }
@@ -18,7 +18,7 @@ import type {
18
18
  MediaError,
19
19
  FeatureAvailability,
20
20
  } from '../types'
21
- import { EventEmitter } from '../core/events'
21
+ import { EventEmitter, onFullscreenChange } from '../core/events'
22
22
  import {
23
23
  detectVolumeAvailability,
24
24
  detectFullscreenAvailability,
@@ -63,6 +63,8 @@ export abstract class BaseProvider implements Provider {
63
63
 
64
64
  destroy(): void {
65
65
  this._ready = false
66
+ this.stopEmbedFullscreen?.()
67
+ this.stopEmbedFullscreen = null
66
68
  this.cleanup()
67
69
  this.events.removeAllListeners()
68
70
  }
@@ -132,6 +134,37 @@ export abstract class BaseProvider implements Provider {
132
134
 
133
135
  // === Feature Availability ===
134
136
 
137
+ private stopEmbedFullscreen: (() => void) | null = null
138
+
139
+ /**
140
+ * What an embedded player (YouTube, Vimeo) can do, said once it is ready.
141
+ *
142
+ * The native-media providers say it on loadedmetadata; the embeds said
143
+ * nothing, so fullscreen stayed "unavailable" and its button disabled,
144
+ * though the player can fill the screen like any other. Fullscreen is the
145
+ * container's own (the player's controls come along); picture in picture
146
+ * is the embed's business. Also follows the document into and out of
147
+ * fullscreen, which only the native providers did.
148
+ */
149
+ protected announceEmbedFeatures(): void {
150
+ // Nothing to measure without a page (server rendering, tests).
151
+ if (typeof document === 'undefined' || typeof HTMLMediaElement === 'undefined') return
152
+ const fullscreenAvailability = this.getFeatureAvailability('fullscreen')
153
+ const pipAvailability = this.getFeatureAvailability('pip')
154
+ const volumeAvailability = this.getFeatureAvailability('volume')
155
+ this.events.emit('statechange', {
156
+ canFullscreen: fullscreenAvailability === 'available',
157
+ canPictureInPicture: pipAvailability === 'available',
158
+ fullscreenAvailability,
159
+ pipAvailability,
160
+ volumeAvailability,
161
+ })
162
+ this.events.emit('availabilitychange', 'fullscreen', fullscreenAvailability)
163
+ this.events.emit('availabilitychange', 'pip', pipAvailability)
164
+ this.events.emit('availabilitychange', 'volume', volumeAvailability)
165
+ this.stopEmbedFullscreen ??= onFullscreenChange(fullscreen => this.events.emit('fullscreenchange', fullscreen))
166
+ }
167
+
135
168
  getFeatureAvailability(feature: 'volume' | 'fullscreen' | 'pip'): FeatureAvailability {
136
169
  // Only pass media element if it's actually an HTMLMediaElement (not an iframe)
137
170
  const el = this.mediaElement
@@ -207,6 +207,11 @@ export class HLSProvider extends BaseProvider {
207
207
  // Create video element
208
208
  this.video = document.createElement('video')
209
209
  this.video.className = 'ts-video-player__media'
210
+ // Sized here: inside <video-player>'s shadow root no page stylesheet
211
+ // reaches it, and unsized a <video> shows at its own resolution, clipped
212
+ // by the player (a 1280px clip in a 1024px dialog lost its edges, and
213
+ // in fullscreen it sat in a corner). Letterboxed, never cropped.
214
+ this.video.style.cssText = 'display:block;width:100%;height:100%;object-fit:contain;background:#000'
210
215
 
211
216
  // Set attributes
212
217
  if (this.options.autoplay) this.video.autoplay = true
@@ -114,6 +114,11 @@ export class HTML5Provider extends BaseProvider {
114
114
  // Create media element
115
115
  this.media = document.createElement(this.isAudio ? 'audio' : 'video')
116
116
  this.media.className = 'ts-video-player__media'
117
+ // Sized here: inside <video-player>'s shadow root no page stylesheet
118
+ // reaches it, and unsized a <video> shows at its own resolution, clipped
119
+ // by the player (a 1280px clip in a 1024px dialog lost its edges, and
120
+ // in fullscreen it sat in a corner). Letterboxed, never cropped.
121
+ if (!this.isAudio) this.media.style.cssText = 'display:block;width:100%;height:100%;object-fit:contain;background:#000'
117
122
 
118
123
  // Set attributes
119
124
  if (this.options.autoplay) this.media.autoplay = true
@@ -310,9 +310,8 @@ export class VimeoProvider extends BaseProvider {
310
310
  videoWidth: width,
311
311
  videoHeight: height,
312
312
  aspectRatio: width / height || 16 / 9,
313
- canFullscreen: true,
314
- canPictureInPicture: false,
315
313
  })
314
+ this.announceEmbedFeatures()
316
315
  }
317
316
 
318
317
  private attachVimeoEvents(): void {
@@ -328,6 +328,7 @@ export class YouTubeProvider extends BaseProvider {
328
328
  // Emit initial state
329
329
  this.events.emit('loadedmetadata')
330
330
  this.events.emit('canplay')
331
+ this.announceEmbedFeatures()
331
332
 
332
333
  // Load pending video if any
333
334
  if (this.pendingVideoId) {