unified-video-framework 1.4.245 → 1.4.247

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.
@@ -1129,6 +1129,10 @@ export class WebPlayer extends BasePlayer {
1129
1129
  private youtubePlayer: any = null;
1130
1130
  private youtubePlayerReady: boolean = false;
1131
1131
  private youtubeIframe: HTMLIFrameElement | null = null;
1132
+ private isYoutubePiPActive: boolean = false;
1133
+ private youtubePiPContainer: HTMLDivElement | null = null;
1134
+ private youtubePiPOriginalParent: HTMLElement | null = null;
1135
+ private youtubePiPOriginalPosition: string = '';
1132
1136
 
1133
1137
  private async createYouTubePlayer(videoId: string): Promise<void> {
1134
1138
  const container = this.playerWrapper || this.video?.parentElement;
@@ -6239,10 +6243,18 @@ export class WebPlayer extends BasePlayer {
6239
6243
  .ytp-endscreen-element,
6240
6244
  .ytp-ce-element,
6241
6245
  .ytp-suggested-action,
6242
- .ytp-pause-overlay {
6246
+ .ytp-pause-overlay,
6247
+ .ytp-endscreen,
6248
+ .ytp-videowall-still,
6249
+ .ytp-ce-covering-overlay,
6250
+ .ytp-ce-element-show,
6251
+ .ytp-cards-button,
6252
+ .ytp-cards-button-icon,
6253
+ .ytp-impression-link {
6243
6254
  display: none !important;
6244
6255
  visibility: hidden !important;
6245
6256
  opacity: 0 !important;
6257
+ pointer-events: none !important;
6246
6258
  }
6247
6259
 
6248
6260
  /* Ultra-wide screens */
@@ -6900,15 +6912,21 @@ export class WebPlayer extends BasePlayer {
6900
6912
  }
6901
6913
  });
6902
6914
 
6903
- // Skip buttons with null safety
6915
+ // Skip buttons with null safety - works with both video and YouTube
6904
6916
  skipBackBtn?.addEventListener('click', () => {
6905
- if (this.video && !isNaN(this.video.duration)) {
6906
- this.seek(Math.max(0, this.video.currentTime - 10));
6917
+ const currentTime = this.getCurrentTime();
6918
+ const duration = this.getDuration();
6919
+ if (!isNaN(duration) && duration > 0) {
6920
+ this.seek(Math.max(0, currentTime - 10));
6921
+ this.showShortcutIndicator('-10s');
6907
6922
  }
6908
6923
  });
6909
6924
  skipForwardBtn?.addEventListener('click', () => {
6910
- if (this.video && !isNaN(this.video.duration)) {
6911
- this.seek(Math.min(this.video.duration, this.video.currentTime + 10));
6925
+ const currentTime = this.getCurrentTime();
6926
+ const duration = this.getDuration();
6927
+ if (!isNaN(duration) && duration > 0) {
6928
+ this.seek(Math.min(duration, currentTime + 10));
6929
+ this.showShortcutIndicator('+10s');
6912
6930
  }
6913
6931
  });
6914
6932
 
@@ -7726,12 +7744,29 @@ export class WebPlayer extends BasePlayer {
7726
7744
  private togglePlayPause(): void {
7727
7745
  this.debugLog('togglePlayPause called, video state:', {
7728
7746
  videoExists: !!this.video,
7729
- videoPaused: this.video?.paused,
7747
+ youtubePlayerExists: !!this.youtubePlayer,
7748
+ youtubePlayerReady: this.youtubePlayerReady,
7730
7749
  playerState: this.state
7731
7750
  });
7732
7751
 
7752
+ // Handle YouTube player
7753
+ if (this.youtubePlayer && this.youtubePlayerReady) {
7754
+ const playerState = this.youtubePlayer.getPlayerState();
7755
+ this.debugLog('YouTube player state:', playerState);
7756
+
7757
+ if (playerState === window.YT.PlayerState.PLAYING) {
7758
+ this.debugLog('YouTube video is playing, calling pause()');
7759
+ this.pause();
7760
+ } else {
7761
+ this.debugLog('YouTube video is paused, calling play()');
7762
+ this.play();
7763
+ }
7764
+ return;
7765
+ }
7766
+
7767
+ // Handle regular video element
7733
7768
  if (!this.video) {
7734
- this.debugError('No video element available for toggle');
7769
+ this.debugError('No video element or YouTube player available for toggle');
7735
7770
  return;
7736
7771
  }
7737
7772
 
@@ -8035,12 +8070,28 @@ export class WebPlayer extends BasePlayer {
8035
8070
 
8036
8071
  private updateTimeDisplay(): void {
8037
8072
  const timeDisplay = document.getElementById('uvf-time-display');
8038
- if (timeDisplay && this.video) {
8039
- const current = this.formatTime(this.video.currentTime || 0);
8040
- const duration = this.formatTime(this.video.duration || 0);
8041
- timeDisplay.textContent = `${current} / ${duration}`;
8042
- this.debugLog('Time display updated:', `${current} / ${duration}`);
8073
+ if (!timeDisplay) return;
8074
+
8075
+ let current = 0;
8076
+ let duration = 0;
8077
+
8078
+ // Get time from YouTube player if available
8079
+ if (this.youtubePlayer && this.youtubePlayerReady) {
8080
+ try {
8081
+ current = this.youtubePlayer.getCurrentTime() || 0;
8082
+ duration = this.youtubePlayer.getDuration() || 0;
8083
+ } catch (error) {
8084
+ this.debugWarn('Error getting YouTube player time:', error);
8085
+ }
8086
+ } else if (this.video) {
8087
+ current = this.video.currentTime || 0;
8088
+ duration = this.video.duration || 0;
8043
8089
  }
8090
+
8091
+ const currentFormatted = this.formatTime(current);
8092
+ const durationFormatted = this.formatTime(duration);
8093
+ timeDisplay.textContent = `${currentFormatted} / ${durationFormatted}`;
8094
+ this.debugLog('Time display updated:', `${currentFormatted} / ${durationFormatted}`);
8044
8095
  }
8045
8096
 
8046
8097
  private showControls(): void {
@@ -9001,8 +9052,16 @@ export class WebPlayer extends BasePlayer {
9001
9052
  }
9002
9053
 
9003
9054
  private setSpeed(speed: number): void {
9004
- if (!this.video) return;
9005
- this.video.playbackRate = speed;
9055
+ // Handle YouTube player
9056
+ if (this.youtubePlayer && this.youtubePlayerReady) {
9057
+ try {
9058
+ this.youtubePlayer.setPlaybackRate(speed);
9059
+ } catch (error) {
9060
+ this.debugWarn('YouTube playback rate not supported:', error);
9061
+ }
9062
+ } else if (this.video) {
9063
+ this.video.playbackRate = speed;
9064
+ }
9006
9065
 
9007
9066
  // Update UI
9008
9067
  document.querySelectorAll('.speed-option').forEach(option => {
@@ -9014,6 +9073,20 @@ export class WebPlayer extends BasePlayer {
9014
9073
  }
9015
9074
 
9016
9075
  private setQualityByLabel(quality: string): void {
9076
+ // Handle YouTube player
9077
+ if (this.youtubePlayer && this.youtubePlayerReady) {
9078
+ // YouTube quality is limited - show notification
9079
+ this.showShortcutIndicator('Quality control not available for YouTube');
9080
+ // Update UI to reflect current quality
9081
+ document.querySelectorAll('.quality-option').forEach(option => {
9082
+ option.classList.remove('active');
9083
+ if ((option as HTMLElement).dataset.quality === 'auto') {
9084
+ option.classList.add('active');
9085
+ }
9086
+ });
9087
+ return;
9088
+ }
9089
+
9017
9090
  const qualityBadge = document.getElementById('uvf-quality-badge');
9018
9091
 
9019
9092
  // Update UI
@@ -9047,6 +9120,17 @@ export class WebPlayer extends BasePlayer {
9047
9120
 
9048
9121
  private async togglePiP(): Promise<void> {
9049
9122
  try {
9123
+ // Handle YouTube player with custom PiP
9124
+ if (this.youtubePlayer && this.youtubePlayerReady) {
9125
+ if (this.isYoutubePiPActive) {
9126
+ await this.exitYoutubePiP();
9127
+ } else {
9128
+ await this.enterYoutubePiP();
9129
+ }
9130
+ return;
9131
+ }
9132
+
9133
+ // Native PiP for regular video
9050
9134
  if ((document as any).pictureInPictureElement) {
9051
9135
  await this.exitPictureInPicture();
9052
9136
  } else {
@@ -9057,6 +9141,137 @@ export class WebPlayer extends BasePlayer {
9057
9141
  }
9058
9142
  }
9059
9143
 
9144
+ private async enterYoutubePiP(): Promise<void> {
9145
+ try {
9146
+ const playerWrapper = this.playerWrapper || this.container?.querySelector('.uvf-player-wrapper');
9147
+ if (!playerWrapper) {
9148
+ this.showNotification('Player container not found');
9149
+ return;
9150
+ }
9151
+
9152
+ // Store original position
9153
+ this.youtubePiPOriginalParent = playerWrapper.parentElement;
9154
+ this.youtubePiPOriginalPosition = playerWrapper.getAttribute('style') || '';
9155
+
9156
+ // Create PiP container
9157
+ this.youtubePiPContainer = document.createElement('div');
9158
+ this.youtubePiPContainer.id = 'youtube-pip-container';
9159
+ this.youtubePiPContainer.style.cssText = `
9160
+ position: fixed;
9161
+ bottom: 20px;
9162
+ right: 20px;
9163
+ width: 400px;
9164
+ height: 225px;
9165
+ z-index: 9999;
9166
+ background: #000;
9167
+ border: 2px solid rgba(255, 255, 255, 0.3);
9168
+ border-radius: 8px;
9169
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
9170
+ cursor: grab;
9171
+ overflow: hidden;
9172
+ `;
9173
+
9174
+ // Add drag functionality
9175
+ let isDragging = false;
9176
+ let offset = { x: 0, y: 0 };
9177
+
9178
+ this.youtubePiPContainer.addEventListener('mousedown', (e) => {
9179
+ if ((e.target as HTMLElement).closest('.youtube-pip-controls')) return;
9180
+ isDragging = true;
9181
+ offset.x = e.clientX - this.youtubePiPContainer!.getBoundingClientRect().left;
9182
+ offset.y = e.clientY - this.youtubePiPContainer!.getBoundingClientRect().top;
9183
+ this.youtubePiPContainer!.style.cursor = 'grabbing';
9184
+ });
9185
+
9186
+ document.addEventListener('mousemove', (e) => {
9187
+ if (!isDragging || !this.youtubePiPContainer) return;
9188
+ this.youtubePiPContainer.style.left = (e.clientX - offset.x) + 'px';
9189
+ this.youtubePiPContainer.style.top = (e.clientY - offset.y) + 'px';
9190
+ });
9191
+
9192
+ document.addEventListener('mouseup', () => {
9193
+ isDragging = false;
9194
+ if (this.youtubePiPContainer) this.youtubePiPContainer.style.cursor = 'grab';
9195
+ });
9196
+
9197
+ // Add close button
9198
+ const closeBtn = document.createElement('button');
9199
+ closeBtn.className = 'youtube-pip-controls';
9200
+ closeBtn.innerHTML = '✕';
9201
+ closeBtn.style.cssText = `
9202
+ position: absolute;
9203
+ top: 8px;
9204
+ right: 8px;
9205
+ width: 32px;
9206
+ height: 32px;
9207
+ background: rgba(255, 255, 255, 0.2);
9208
+ border: none;
9209
+ color: white;
9210
+ font-size: 18px;
9211
+ border-radius: 4px;
9212
+ cursor: pointer;
9213
+ z-index: 10000;
9214
+ transition: background 0.2s;
9215
+ `;
9216
+
9217
+ closeBtn.addEventListener('hover', () => {
9218
+ closeBtn.style.background = 'rgba(255, 255, 255, 0.4)';
9219
+ });
9220
+
9221
+ closeBtn.addEventListener('click', async () => {
9222
+ await this.exitYoutubePiP();
9223
+ });
9224
+
9225
+ // Move player to PiP container
9226
+ this.youtubePiPContainer.appendChild(playerWrapper as HTMLElement);
9227
+ this.youtubePiPContainer.appendChild(closeBtn);
9228
+ document.body.appendChild(this.youtubePiPContainer);
9229
+
9230
+ // Style player for PiP
9231
+ (playerWrapper as HTMLElement).style.width = '100%';
9232
+ (playerWrapper as HTMLElement).style.height = '100%';
9233
+
9234
+ this.isYoutubePiPActive = true;
9235
+ this.showNotification('PiP mode active (drag to move)');
9236
+ this.debugLog('YouTube PiP mode enabled');
9237
+ } catch (error) {
9238
+ this.debugError('Failed to enter YouTube PiP:', error);
9239
+ this.showNotification('PiP mode failed');
9240
+ }
9241
+ }
9242
+
9243
+ private async exitYoutubePiP(): Promise<void> {
9244
+ try {
9245
+ if (!this.youtubePiPContainer || !this.youtubePiPOriginalParent) {
9246
+ return;
9247
+ }
9248
+
9249
+ const playerWrapper = this.playerWrapper || this.container?.querySelector('.uvf-player-wrapper');
9250
+ if (playerWrapper) {
9251
+ // Restore original styling
9252
+ if (this.youtubePiPOriginalPosition) {
9253
+ playerWrapper.setAttribute('style', this.youtubePiPOriginalPosition);
9254
+ } else {
9255
+ playerWrapper.removeAttribute('style');
9256
+ }
9257
+
9258
+ // Move back to original parent
9259
+ this.youtubePiPOriginalParent.appendChild(playerWrapper);
9260
+ }
9261
+
9262
+ // Remove PiP container
9263
+ this.youtubePiPContainer.remove();
9264
+ this.youtubePiPContainer = null;
9265
+ this.youtubePiPOriginalParent = null;
9266
+
9267
+ this.isYoutubePiPActive = false;
9268
+ this.showNotification('PiP mode closed');
9269
+ this.debugLog('YouTube PiP mode disabled');
9270
+ } catch (error) {
9271
+ this.debugError('Failed to exit YouTube PiP:', error);
9272
+ }
9273
+ }
9274
+
9060
9275
  private setupCastContextSafe(): void {
9061
9276
  try {
9062
9277
  const castNs = (window as any).cast;
@@ -9582,6 +9797,17 @@ export class WebPlayer extends BasePlayer {
9582
9797
  private detectAvailableSubtitles(): void {
9583
9798
  this.availableSubtitles = [{ value: 'off', label: 'Off' }];
9584
9799
 
9800
+ // YouTube player - subtitles available but not fully controllable
9801
+ if (this.youtubePlayer && this.youtubePlayerReady) {
9802
+ // YouTube supports captions but control is limited
9803
+ // We can only toggle on/off, not select language
9804
+ this.availableSubtitles.push({
9805
+ value: 'youtube-cc',
9806
+ label: 'YouTube Captions'
9807
+ });
9808
+ return;
9809
+ }
9810
+
9585
9811
  if (this.video?.textTracks) {
9586
9812
  // HTML5 text tracks
9587
9813
  Array.from(this.video.textTracks).forEach((track, index) => {
@@ -9939,6 +10165,25 @@ export class WebPlayer extends BasePlayer {
9939
10165
  private setSubtitle(subtitle: string): void {
9940
10166
  this.currentSubtitle = subtitle;
9941
10167
 
10168
+ if (this.youtubePlayer && this.youtubePlayerReady) {
10169
+ // YouTube subtitles - limited control
10170
+ if (subtitle === 'off') {
10171
+ try {
10172
+ this.youtubePlayer.setOption('captions', 'fontSize', -1); // Hide captions
10173
+ } catch (e) {
10174
+ this.debugWarn('YouTube caption control limited:', e);
10175
+ }
10176
+ } else if (subtitle === 'youtube-cc') {
10177
+ try {
10178
+ this.youtubePlayer.setOption('captions', 'fontSize', 0); // Show captions
10179
+ } catch (e) {
10180
+ this.debugWarn('YouTube caption control limited:', e);
10181
+ }
10182
+ }
10183
+ this.showShortcutIndicator('Subtitle language selection not available for YouTube');
10184
+ return;
10185
+ }
10186
+
9942
10187
  if (subtitle === 'off') {
9943
10188
  // Disable all subtitles
9944
10189
  if (this.video?.textTracks) {