myetv-player 1.1.6 → 1.3.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.
Files changed (47) hide show
  1. package/css/myetv-player.css +153 -0
  2. package/css/myetv-player.min.css +1 -1
  3. package/dist/myetv-player.js +654 -129
  4. package/dist/myetv-player.min.js +579 -115
  5. package/package.json +35 -16
  6. package/plugins/twitch/myetv-player-twitch-plugin.js +125 -11
  7. package/plugins/vimeo/myetv-player-vimeo.js +80 -49
  8. package/plugins/youtube/README.md +5 -2
  9. package/plugins/youtube/myetv-player-youtube-plugin.js +891 -14
  10. package/.github/workflows/codeql.yml +0 -100
  11. package/.github/workflows/npm-publish.yml +0 -30
  12. package/SECURITY.md +0 -50
  13. package/build.js +0 -195
  14. package/scss/README.md +0 -161
  15. package/scss/_audio-player.scss +0 -21
  16. package/scss/_base.scss +0 -116
  17. package/scss/_controls.scss +0 -188
  18. package/scss/_loading.scss +0 -111
  19. package/scss/_menus.scss +0 -432
  20. package/scss/_mixins.scss +0 -112
  21. package/scss/_poster.scss +0 -8
  22. package/scss/_progress-bar.scss +0 -319
  23. package/scss/_resolution.scss +0 -68
  24. package/scss/_responsive.scss +0 -1360
  25. package/scss/_themes.scss +0 -30
  26. package/scss/_title-overlay.scss +0 -60
  27. package/scss/_tooltips.scss +0 -7
  28. package/scss/_variables.scss +0 -49
  29. package/scss/_video.scss +0 -221
  30. package/scss/_volume.scss +0 -122
  31. package/scss/_watermark.scss +0 -128
  32. package/scss/myetv-player.scss +0 -51
  33. package/scss/package.json +0 -16
  34. package/src/README.md +0 -560
  35. package/src/chapters.js +0 -521
  36. package/src/controls.js +0 -1243
  37. package/src/core.js +0 -1922
  38. package/src/events.js +0 -456
  39. package/src/fullscreen.js +0 -82
  40. package/src/i18n.js +0 -374
  41. package/src/playlist.js +0 -177
  42. package/src/plugins.js +0 -384
  43. package/src/quality.js +0 -963
  44. package/src/streaming.js +0 -346
  45. package/src/subtitles.js +0 -524
  46. package/src/utils.js +0 -65
  47. package/src/watermark.js +0 -246
@@ -35,6 +35,9 @@
35
35
  // Enable or disable click over youtube player
36
36
  mouseClick: options.mouseClick !== undefined ? options.mouseClick : false,
37
37
 
38
+ // Show YouTube chapters in the timeline
39
+ showYoutubeChapters: options.showYoutubeChapters !== undefined ? options.showYoutubeChapters : true,
40
+
38
41
  debug: true,
39
42
  ...options
40
43
  };
@@ -67,6 +70,9 @@
67
70
  this.resizeListenerAdded = false;
68
71
  // Channel data cache
69
72
  this.channelData = null;
73
+ // Chapters cache in localStorage
74
+ this.cacheExpiration = 21600000; // 6 hour in milliseconds
75
+ this.cachePrefix = 'myetv_yt_chapters_'; // prefix for localStorage keys
70
76
  //live streaming
71
77
  this.isLiveStream = false;
72
78
  this.liveCheckInterval = null;
@@ -1388,6 +1394,18 @@
1388
1394
  this.updatePlayerWatermark();
1389
1395
  }
1390
1396
 
1397
+ // Load and display chapters if API key is available
1398
+ if (this.options.apiKey) {
1399
+ this.fetchVideoChapters(this.videoId).then(chapters => {
1400
+ if (chapters) {
1401
+ this.displayChaptersOnProgressBar(chapters);
1402
+ }
1403
+ });
1404
+ }
1405
+
1406
+ // Clear expired cache entries on player ready
1407
+ this.clearExpiredCache();
1408
+
1391
1409
  // Set auto caption language
1392
1410
  if (this.options.autoCaptionLanguage) {
1393
1411
  setTimeout(() => this.setAutoCaptionLanguage(), 1500);
@@ -2077,15 +2095,58 @@
2077
2095
  document.head.appendChild(style);
2078
2096
  this.api.container.classList.add('youtube-active');
2079
2097
 
2098
+ // Add chapter styles
2099
+ const chapterStyles = `
2100
+ /* Chapter segments styling */
2101
+ .chapter-segment {
2102
+ box-sizing: border-box;
2103
+ }
2104
+
2105
+ /* Make progress fill respect chapter segments */
2106
+ .progress-filled {
2107
+ z-index: 5 !important;
2108
+ pointer-events: none;
2109
+ }
2110
+
2111
+ /* Ensure chapter markers are visible */
2112
+ .chapter-marker {
2113
+ z-index: 6 !important;
2114
+ }
2115
+
2116
+ /* Enhanced tooltip */
2117
+ .yt-seek-tooltip {
2118
+ animation: fadeIn 0.15s ease-in-out;
2119
+ }
2120
+
2121
+ @keyframes fadeIn {
2122
+ from {
2123
+ opacity: 0;
2124
+ transform: translateX(-50%) translateY(-5px);
2125
+ }
2126
+ to {
2127
+ opacity: 1;
2128
+ transform: translateX(-50%) translateY(0);
2129
+ }
2130
+ }
2131
+
2132
+ /* Hover effect on progress container */
2133
+ .progress-container:hover .chapter-segment {
2134
+ background: rgba(255, 255, 255, 0.4) !important;
2135
+ }
2136
+ `;
2137
+
2138
+ style.textContent = chapterStyles;
2139
+ document.head.appendChild(style);
2140
+
2080
2141
  if (this.api.player.options.debug) {
2081
2142
  console.log('YT Plugin: CSS override injected');
2082
2143
  }
2083
2144
  }
2084
2145
 
2085
2146
  /**
2086
- * Inject CSS styles for controlbar and title overlay gradients
2087
- * Uses YouTube-specific selectors to avoid conflicts with other plugins
2088
- */
2147
+ * Inject CSS styles for controlbar and title overlay gradients
2148
+ * Uses YouTube-specific selectors to avoid conflicts with other plugins
2149
+ */
2089
2150
  injectControlbarGradientStyles() {
2090
2151
  // Check if styles are already injected
2091
2152
  if (document.getElementById('yt-controlbar-gradient-styles')) {
@@ -3979,13 +4040,16 @@
3979
4040
  this.api.container.style.setProperty('--player-volume-fill', `${initialVolume}%`);
3980
4041
  }
3981
4042
 
4043
+ // Handle volume changes with proper unmute logic
4044
+ // VOLUME SLIDER DRAG SUPPORT
4045
+ let isDraggingVolume = false;
4046
+
3982
4047
  // Handle volume changes with proper unmute logic
3983
4048
  newVolumeSlider.addEventListener('input', (e) => {
3984
4049
  const volume = parseFloat(e.target.value);
3985
-
3986
4050
  if (this.ytPlayer && this.ytPlayer.setVolume) {
3987
4051
  this.ytPlayer.setVolume(volume);
3988
- this.api.container.style.setProperty('--player-volume-fill', `${volume}%`);
4052
+ this.api.container.style.setProperty('--player-volume-fill', volume + '%');
3989
4053
 
3990
4054
  // Always update mute button state correctly
3991
4055
  if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
@@ -4004,20 +4068,134 @@
4004
4068
  this.api.player.updateVolumeTooltipPosition(volume / 100);
4005
4069
  }
4006
4070
 
4007
- // Update tooltip position during drag
4008
- if (this.api.player.updateVolumeTooltipPosition) {
4009
- this.api.player.updateVolumeTooltipPosition(volume / 100);
4010
- }
4011
-
4012
- // Update tooltip text manually instead of using updateVolumeTooltip
4071
+ // Update tooltip text manually
4013
4072
  const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4014
4073
  if (volumeTooltip) {
4015
4074
  volumeTooltip.textContent = Math.round(volume) + '%';
4016
4075
  }
4076
+ }
4077
+ });
4017
4078
 
4079
+ // MOUSE DRAG - Start
4080
+ newVolumeSlider.addEventListener('mousedown', (e) => {
4081
+ isDraggingVolume = true;
4082
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4083
+ if (volumeTooltip) {
4084
+ volumeTooltip.classList.add('visible');
4018
4085
  }
4019
4086
  });
4020
4087
 
4088
+ // MOUSE DRAG - Move
4089
+ document.addEventListener('mousemove', (e) => {
4090
+ if (isDraggingVolume && newVolumeSlider) {
4091
+ const rect = newVolumeSlider.getBoundingClientRect();
4092
+ const clickX = e.clientX - rect.left;
4093
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
4094
+ const volume = Math.round(percentage * 100);
4095
+
4096
+ newVolumeSlider.value = volume;
4097
+
4098
+ if (this.ytPlayer && this.ytPlayer.setVolume) {
4099
+ this.ytPlayer.setVolume(volume);
4100
+ this.api.container.style.setProperty('--player-volume-fill', volume + '%');
4101
+
4102
+ // Update mute state
4103
+ if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
4104
+ this.ytPlayer.unMute();
4105
+ this.updateMuteButtonState(false);
4106
+ } else if (volume === 0) {
4107
+ if (this.ytPlayer.isMuted && !this.ytPlayer.isMuted()) {
4108
+ this.ytPlayer.mute();
4109
+ }
4110
+ this.updateMuteButtonState(true);
4111
+ }
4112
+
4113
+ // Update tooltip
4114
+ if (this.api.player.updateVolumeTooltipPosition) {
4115
+ this.api.player.updateVolumeTooltipPosition(volume / 100);
4116
+ }
4117
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4118
+ if (volumeTooltip) {
4119
+ volumeTooltip.textContent = volume + '%';
4120
+ }
4121
+ }
4122
+ }
4123
+ });
4124
+
4125
+ // MOUSE DRAG - End
4126
+ document.addEventListener('mouseup', () => {
4127
+ if (isDraggingVolume) {
4128
+ isDraggingVolume = false;
4129
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4130
+ if (volumeTooltip) {
4131
+ setTimeout(() => {
4132
+ volumeTooltip.classList.remove('visible');
4133
+ }, 300);
4134
+ }
4135
+ }
4136
+ });
4137
+
4138
+ // TOUCH DRAG - Start
4139
+ newVolumeSlider.addEventListener('touchstart', (e) => {
4140
+ isDraggingVolume = true;
4141
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4142
+ if (volumeTooltip) {
4143
+ volumeTooltip.classList.add('visible');
4144
+ }
4145
+ }, { passive: true });
4146
+
4147
+ // TOUCH DRAG - Move
4148
+ newVolumeSlider.addEventListener('touchmove', (e) => {
4149
+ if (isDraggingVolume) {
4150
+ const touch = e.touches[0];
4151
+ const rect = newVolumeSlider.getBoundingClientRect();
4152
+ const touchX = touch.clientX - rect.left;
4153
+ const percentage = Math.max(0, Math.min(1, touchX / rect.width));
4154
+ const volume = Math.round(percentage * 100);
4155
+
4156
+ newVolumeSlider.value = volume;
4157
+
4158
+ if (this.ytPlayer && this.ytPlayer.setVolume) {
4159
+ this.ytPlayer.setVolume(volume);
4160
+ this.api.container.style.setProperty('--player-volume-fill', volume + '%');
4161
+
4162
+ // Update mute state
4163
+ if (volume > 0 && this.ytPlayer.isMuted && this.ytPlayer.isMuted()) {
4164
+ this.ytPlayer.unMute();
4165
+ this.updateMuteButtonState(false);
4166
+ } else if (volume === 0) {
4167
+ if (this.ytPlayer.isMuted && !this.ytPlayer.isMuted()) {
4168
+ this.ytPlayer.mute();
4169
+ }
4170
+ this.updateMuteButtonState(true);
4171
+ }
4172
+
4173
+ // Update tooltip
4174
+ if (this.api.player.updateVolumeTooltipPosition) {
4175
+ this.api.player.updateVolumeTooltipPosition(volume / 100);
4176
+ }
4177
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4178
+ if (volumeTooltip) {
4179
+ volumeTooltip.textContent = volume + '%';
4180
+ }
4181
+ }
4182
+ }
4183
+ }, { passive: true });
4184
+
4185
+ // TOUCH DRAG - End
4186
+ newVolumeSlider.addEventListener('touchend', () => {
4187
+ if (isDraggingVolume) {
4188
+ isDraggingVolume = false;
4189
+ const volumeTooltip = this.api.container.querySelector('.volume-tooltip');
4190
+ if (volumeTooltip) {
4191
+ setTimeout(() => {
4192
+ volumeTooltip.classList.remove('visible');
4193
+ }, 300);
4194
+ }
4195
+ }
4196
+ }, { passive: true });
4197
+
4198
+
4021
4199
  // Update tooltip position on mousemove
4022
4200
  newVolumeSlider.addEventListener('mousemove', (e) => {
4023
4201
  const rect = newVolumeSlider.getBoundingClientRect();
@@ -4088,6 +4266,706 @@
4088
4266
  }
4089
4267
  }
4090
4268
 
4269
+ /**
4270
+ * Fetch video chapters from description using YouTube Data API v3
4271
+ * Uses localStorage for persistent caching
4272
+ */
4273
+ async fetchVideoChapters(videoId) {
4274
+
4275
+ // Check if chapters display is enabled
4276
+ if (!this.options.showYoutubeChapters) {
4277
+ if (this.api.player.options.debug) {
4278
+ console.log('YT Plugin: YouTube chapters display disabled by option');
4279
+ }
4280
+ return null;
4281
+ }
4282
+
4283
+ // Check if API key is available
4284
+ if (!this.options.apiKey) {
4285
+ if (this.api.player.options.debug) {
4286
+ console.warn('YT Plugin: API Key required to fetch chapters');
4287
+ }
4288
+ return null;
4289
+ }
4290
+
4291
+ // Check cache first (localStorage)
4292
+ const cached = this.getCachedChapters(videoId);
4293
+ if (cached) {
4294
+ if (this.api.player.options.debug) {
4295
+ console.log('YT Plugin: Using cached chapters for', videoId);
4296
+ }
4297
+ return cached;
4298
+ }
4299
+
4300
+ try {
4301
+ const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${this.options.apiKey}`;
4302
+ const response = await fetch(url);
4303
+ const data = await response.json();
4304
+
4305
+ if (!data.items || data.items.length === 0) {
4306
+ // Cache null result too to avoid repeated calls
4307
+ this.setCachedChapters(videoId, null);
4308
+ return null;
4309
+ }
4310
+
4311
+ const description = data.items[0].snippet.description;
4312
+ const chapters = this.parseChaptersFromDescription(description);
4313
+
4314
+ // Cache the result (even if null)
4315
+ this.setCachedChapters(videoId, chapters);
4316
+
4317
+ if (this.api.player.options.debug) {
4318
+ console.log('YT Plugin: Chapters fetched and cached:', chapters);
4319
+ }
4320
+
4321
+ return chapters;
4322
+ } catch (error) {
4323
+ if (this.api.player.options.debug) {
4324
+ console.error('YT Plugin: Error fetching chapters', error);
4325
+ }
4326
+ return null;
4327
+ }
4328
+ }
4329
+
4330
+ /**
4331
+ * Get cached chapters from localStorage
4332
+ */
4333
+ getCachedChapters(videoId) {
4334
+ try {
4335
+ const key = this.cachePrefix + videoId;
4336
+ const cached = localStorage.getItem(key);
4337
+
4338
+ if (!cached) return null;
4339
+
4340
+ const data = JSON.parse(cached);
4341
+ const now = Date.now();
4342
+
4343
+ // Check if cache is expired
4344
+ if (now - data.timestamp >= this.cacheExpiration) {
4345
+ localStorage.removeItem(key);
4346
+ if (this.api.player.options.debug) {
4347
+ console.log('YT Plugin: Cache expired for', videoId);
4348
+ }
4349
+ return null;
4350
+ }
4351
+
4352
+ return data.chapters;
4353
+ } catch (error) {
4354
+ if (this.api.player.options.debug) {
4355
+ console.error('YT Plugin: Error reading cache', error);
4356
+ }
4357
+ return null;
4358
+ }
4359
+ }
4360
+
4361
+ /**
4362
+ * Set cached chapters in localStorage
4363
+ */
4364
+ setCachedChapters(videoId, chapters) {
4365
+ try {
4366
+ const key = this.cachePrefix + videoId;
4367
+ const data = {
4368
+ chapters: chapters,
4369
+ timestamp: Date.now()
4370
+ };
4371
+ localStorage.setItem(key, JSON.stringify(data));
4372
+
4373
+ if (this.api.player.options.debug) {
4374
+ console.log('YT Plugin: Chapters cached for', videoId);
4375
+ }
4376
+ } catch (error) {
4377
+ if (this.api.player.options.debug) {
4378
+ console.error('YT Plugin: Error writing cache', error);
4379
+ }
4380
+ }
4381
+ }
4382
+
4383
+ /**
4384
+ * Parse chapters from video description
4385
+ * Validates YouTube chapter requirements: 3+ chapters, starts at 0:00, 10+ seconds each
4386
+ */
4387
+ parseChaptersFromDescription(description) {
4388
+ if (!description) return null;
4389
+
4390
+ const chapters = [];
4391
+ // Regex for timestamps: 0:00, 00:00, 0:00:00
4392
+ // Matches both "0:00 Title" and "0:00 - Title"
4393
+ const timestampRegex = /(?:^|\n)(\d{1,2}:?\d{0,2}:?\d{2})\s*[-–—]?\s*(.+?)(?=\n|$)/gm;
4394
+ let match;
4395
+
4396
+ while ((match = timestampRegex.exec(description)) !== null) {
4397
+ const timeString = match[1].trim();
4398
+ const title = match[2].trim();
4399
+
4400
+ // Skip if title is too short or empty
4401
+ if (!title || title.length < 2) continue;
4402
+
4403
+ const seconds = this.parseTimeToSeconds(timeString);
4404
+
4405
+ if (seconds !== null) {
4406
+ chapters.push({
4407
+ time: seconds,
4408
+ title: title,
4409
+ timeString: timeString
4410
+ });
4411
+ }
4412
+ }
4413
+
4414
+ // Sort by time
4415
+ chapters.sort((a, b) => a.time - b.time);
4416
+
4417
+ // Validate: at least 3 chapters and first starts at 0:00
4418
+ if (chapters.length >= 3 && chapters[0].time === 0) {
4419
+ // Validate minimum duration (10 seconds)
4420
+ let valid = true;
4421
+ for (let i = 0; i < chapters.length - 1; i++) {
4422
+ if (chapters[i + 1].time - chapters[i].time < 10) {
4423
+ valid = false;
4424
+ break;
4425
+ }
4426
+ }
4427
+
4428
+ if (valid) {
4429
+ if (this.api.player.options.debug) {
4430
+ console.log(`YT Plugin: Found ${chapters.length} valid chapters`);
4431
+ }
4432
+ return chapters;
4433
+ }
4434
+ }
4435
+
4436
+ if (this.api.player.options.debug) {
4437
+ console.log('YT Plugin: No valid chapters found (requires 3+ chapters, starting at 0:00, each 10+ seconds)');
4438
+ }
4439
+
4440
+ return null;
4441
+ }
4442
+
4443
+ /**
4444
+ * Parse time string to seconds
4445
+ */
4446
+ parseTimeToSeconds(timeString) {
4447
+ const parts = timeString.split(':').map(p => parseInt(p, 10));
4448
+
4449
+ if (parts.some(p => isNaN(p))) return null;
4450
+
4451
+ if (parts.length === 2) {
4452
+ // mm:ss
4453
+ return parts[0] * 60 + parts[1];
4454
+ } else if (parts.length === 3) {
4455
+ // hh:mm:ss
4456
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
4457
+ }
4458
+
4459
+ return null;
4460
+ }
4461
+
4462
+ /**
4463
+ * Display chapter markers on progress bar with YouTube-style segments
4464
+ */
4465
+ displayChaptersOnProgressBar(chapters) {
4466
+
4467
+ // Check if chapters display is enabled
4468
+ if (!this.options.showYoutubeChapters) {
4469
+ if (this.api.player.options.debug) {
4470
+ console.log('YT Plugin: YouTube chapters display disabled');
4471
+ }
4472
+ return;
4473
+ }
4474
+
4475
+ if (!chapters || chapters.length === 0) return;
4476
+
4477
+ const progressContainer = this.api.container.querySelector('.progress-container');
4478
+ if (!progressContainer) return;
4479
+
4480
+ const duration = this.ytPlayer.getDuration();
4481
+ if (!duration || duration === 0) return;
4482
+
4483
+ // Store chapters for tooltip display
4484
+ this.chapters = chapters;
4485
+
4486
+ // Remove existing chapter markers and segments
4487
+ progressContainer.querySelectorAll('.chapter-marker, .chapter-segment').forEach(m => m.remove());
4488
+
4489
+ // Create segments for each chapter (physically divided)
4490
+ chapters.forEach((chapter, index) => {
4491
+ const nextChapter = chapters[index + 1];
4492
+ const startPercent = (chapter.time / duration) * 100;
4493
+ const endPercent = nextChapter ? (nextChapter.time / duration) * 100 : 100;
4494
+
4495
+ // Calculate segment width minus the gap
4496
+ const gapSize = nextChapter ? 6 : 0; // 6px gap between segments
4497
+ const widthPercent = endPercent - startPercent;
4498
+
4499
+ // Create segment container
4500
+ const segment = document.createElement('div');
4501
+ segment.className = 'chapter-segment';
4502
+ segment.style.cssText = `
4503
+ position: absolute;
4504
+ left: ${startPercent}%;
4505
+ top: 0;
4506
+ width: calc(${widthPercent}% - ${gapSize}px);
4507
+ height: 100%;
4508
+ background: rgba(255, 255, 255, 0.3);
4509
+ cursor: pointer;
4510
+ z-index: 3;
4511
+ transition: background 0.2s;
4512
+ pointer-events: none;
4513
+ `;
4514
+
4515
+ segment.dataset.chapterIndex = index;
4516
+ segment.dataset.time = chapter.time;
4517
+ segment.dataset.title = chapter.title;
4518
+
4519
+ progressContainer.appendChild(segment);
4520
+
4521
+ // Add marker at the START of next segment (not between)
4522
+ if (nextChapter) {
4523
+ const marker = document.createElement('div');
4524
+ marker.className = 'chapter-marker';
4525
+ marker.style.cssText = `
4526
+ position: absolute !important;
4527
+ left: ${endPercent}% !important;
4528
+ top: 0 !important;
4529
+ width: 6px !important;
4530
+ height: 100% !important;
4531
+ background: transparent !important;
4532
+ border: none !important;
4533
+ box-shadow: none !important;
4534
+ margin-left: -3px !important;
4535
+ cursor: pointer !important;
4536
+ z-index: 10 !important;
4537
+ `;
4538
+
4539
+ marker.dataset.chapterTime = nextChapter.time;
4540
+ marker.dataset.chapterTitle = nextChapter.title;
4541
+
4542
+ // Click on marker to jump to chapter start
4543
+ marker.addEventListener('click', (e) => {
4544
+ e.stopPropagation();
4545
+ this.ytPlayer.seekTo(nextChapter.time, true);
4546
+
4547
+ if (this.api.player.options.debug) {
4548
+ console.log(`YT Plugin: Jumped to chapter "${nextChapter.title}" at ${nextChapter.timeString}`);
4549
+ }
4550
+ });
4551
+
4552
+ progressContainer.appendChild(marker);
4553
+ }
4554
+ });
4555
+
4556
+ if (this.api.player.options.debug) {
4557
+ console.log(`YT Plugin: ${chapters.length} chapter segments displayed with gaps`);
4558
+ }
4559
+
4560
+ // Initialize chapter display in title overlay
4561
+ this.initChapterDisplayInOverlay();
4562
+
4563
+ // Add chapter title tooltip
4564
+ this.addChapterTooltip();
4565
+ }
4566
+
4567
+ /**
4568
+ * Add chapter title tooltip with edge detection
4569
+ */
4570
+ addChapterTooltip() {
4571
+
4572
+ // Check if chapters display is enabled
4573
+ if (!this.options.showYoutubeChapters) {
4574
+ if (this.api.player.options.debug) {
4575
+ console.log('YT Plugin: Chapter tooltip display disabled');
4576
+ }
4577
+ return;
4578
+ }
4579
+
4580
+ if (!this.chapters || this.chapters.length === 0) return;
4581
+
4582
+ const progressContainer = this.api.container.querySelector('.progress-container');
4583
+ if (!progressContainer) return;
4584
+
4585
+ // Remove existing chapter tooltip
4586
+ let chapterTooltip = progressContainer.querySelector('.yt-chapter-tooltip');
4587
+ if (chapterTooltip) {
4588
+ chapterTooltip.remove();
4589
+ }
4590
+
4591
+ // Create chapter tooltip
4592
+ chapterTooltip = document.createElement('div');
4593
+ chapterTooltip.className = 'yt-chapter-tooltip';
4594
+ chapterTooltip.style.cssText = `
4595
+ position: absolute;
4596
+ bottom: calc(100% + 35px);
4597
+ left: 0;
4598
+ background: rgba(28, 28, 28, 0.95);
4599
+ color: #fff;
4600
+ padding: 6px 12px;
4601
+ border-radius: 4px;
4602
+ font-size: 12px;
4603
+ font-weight: 600;
4604
+ white-space: nowrap;
4605
+ pointer-events: none;
4606
+ visibility: hidden;
4607
+ opacity: 0;
4608
+ z-index: 100001;
4609
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4610
+ max-width: 250px;
4611
+ overflow: hidden;
4612
+ text-overflow: ellipsis;
4613
+ transform: translateX(-50%);
4614
+ transition: opacity 0.15s, visibility 0.15s;
4615
+ `;
4616
+ progressContainer.appendChild(chapterTooltip);
4617
+
4618
+ // Get player container bounds for edge detection
4619
+ const getPlayerBounds = () => {
4620
+ return this.api.container.getBoundingClientRect();
4621
+ };
4622
+
4623
+ // Show/hide chapter tooltip with edge detection
4624
+ progressContainer.addEventListener('mousemove', (e) => {
4625
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
4626
+
4627
+ const rect = progressContainer.getBoundingClientRect();
4628
+ const playerRect = getPlayerBounds();
4629
+ const mouseX = e.clientX - rect.left;
4630
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
4631
+ const duration = this.ytPlayer.getDuration();
4632
+ const time = percentage * duration;
4633
+
4634
+ // Find current chapter
4635
+ let currentChapter = null;
4636
+ for (let i = this.chapters.length - 1; i >= 0; i--) {
4637
+ if (time >= this.chapters[i].time) {
4638
+ currentChapter = this.chapters[i];
4639
+ break;
4640
+ }
4641
+ }
4642
+
4643
+ if (currentChapter) {
4644
+ chapterTooltip.textContent = currentChapter.title;
4645
+ chapterTooltip.style.visibility = 'visible';
4646
+ chapterTooltip.style.opacity = '1';
4647
+
4648
+ // Wait for tooltip to render to get its width
4649
+ setTimeout(() => {
4650
+ const tooltipWidth = chapterTooltip.offsetWidth;
4651
+ const tooltipHalfWidth = tooltipWidth / 2;
4652
+
4653
+ // Calculate absolute position
4654
+ const absoluteX = e.clientX;
4655
+
4656
+ // Check left edge
4657
+ if (absoluteX - tooltipHalfWidth < playerRect.left) {
4658
+ // Stick to left edge
4659
+ const leftOffset = playerRect.left - rect.left;
4660
+ chapterTooltip.style.left = `${leftOffset + tooltipHalfWidth}px`;
4661
+ chapterTooltip.style.transform = 'translateX(-50%)';
4662
+ }
4663
+ // Check right edge
4664
+ else if (absoluteX + tooltipHalfWidth > playerRect.right) {
4665
+ // Stick to right edge
4666
+ const rightOffset = playerRect.right - rect.left;
4667
+ chapterTooltip.style.left = `${rightOffset - tooltipHalfWidth}px`;
4668
+ chapterTooltip.style.transform = 'translateX(-50%)';
4669
+ }
4670
+ // Normal centering
4671
+ else {
4672
+ chapterTooltip.style.left = `${mouseX}px`;
4673
+ chapterTooltip.style.transform = 'translateX(-50%)';
4674
+ }
4675
+ }, 0);
4676
+ } else {
4677
+ chapterTooltip.style.visibility = 'hidden';
4678
+ chapterTooltip.style.opacity = '0';
4679
+ }
4680
+ });
4681
+
4682
+ progressContainer.addEventListener('mouseleave', () => {
4683
+ chapterTooltip.style.visibility = 'hidden';
4684
+ chapterTooltip.style.opacity = '0';
4685
+ });
4686
+
4687
+ if (this.api.player.options.debug) {
4688
+ console.log('YT Plugin: Chapter tooltip added with edge detection');
4689
+ }
4690
+ }
4691
+
4692
+ /**
4693
+ * Initialize chapter display in title overlay
4694
+ */
4695
+ initChapterDisplayInOverlay() {
4696
+ if (!this.chapters || this.chapters.length === 0) return;
4697
+
4698
+ const titleOverlay = this.api.container.querySelector('.title-overlay');
4699
+ if (!titleOverlay) return;
4700
+
4701
+ // Remove existing chapter element if present
4702
+ let chapterElement = titleOverlay.querySelector('.chapter-name');
4703
+ if (chapterElement) {
4704
+ chapterElement.remove();
4705
+ }
4706
+
4707
+ // Create chapter name element
4708
+ chapterElement = document.createElement('div');
4709
+ chapterElement.className = 'chapter-name';
4710
+ chapterElement.style.cssText = `
4711
+ font-size: 13px;
4712
+ font-weight: 500;
4713
+ color: rgba(255, 255, 255, 0.9);
4714
+ margin-top: 6px;
4715
+ max-width: 400px;
4716
+ overflow: hidden;
4717
+ text-overflow: ellipsis;
4718
+ white-space: nowrap;
4719
+ `;
4720
+
4721
+ // Append to title overlay
4722
+ titleOverlay.appendChild(chapterElement);
4723
+
4724
+ // Start monitoring playback to update chapter name
4725
+ this.startChapterNameMonitoring();
4726
+
4727
+ if (this.api.player.options.debug) {
4728
+ console.log('YT Plugin: Chapter name display initialized in title overlay');
4729
+ }
4730
+ }
4731
+
4732
+ /**
4733
+ * Monitor playback and update chapter name dynamically
4734
+ */
4735
+ startChapterNameMonitoring() {
4736
+ if (this.chapterMonitorInterval) {
4737
+ clearInterval(this.chapterMonitorInterval);
4738
+ }
4739
+
4740
+ // Update every 500ms to catch chapter changes
4741
+ this.chapterMonitorInterval = setInterval(() => {
4742
+ if (!this.ytPlayer || !this.chapters || this.chapters.length === 0) {
4743
+ return;
4744
+ }
4745
+
4746
+ try {
4747
+ const currentTime = this.ytPlayer.getCurrentTime();
4748
+ this.updateChapterNameDisplay(currentTime);
4749
+ } catch (error) {
4750
+ if (this.api.player.options.debug) {
4751
+ console.error('YT Plugin: Error updating chapter name', error);
4752
+ }
4753
+ }
4754
+ }, 500);
4755
+ }
4756
+
4757
+ /**
4758
+ * Update chapter name in overlay based on current time
4759
+ */
4760
+ updateChapterNameDisplay(currentTime) {
4761
+ const chapterElement = this.api.container.querySelector('.title-overlay .chapter-name');
4762
+ if (!chapterElement) return;
4763
+
4764
+ // Find current chapter
4765
+ let currentChapter = null;
4766
+ for (let i = this.chapters.length - 1; i >= 0; i--) {
4767
+ if (currentTime >= this.chapters[i].time) {
4768
+ currentChapter = this.chapters[i];
4769
+ break;
4770
+ }
4771
+ }
4772
+
4773
+ // Update or clear chapter name
4774
+ if (currentChapter) {
4775
+ chapterElement.textContent = currentChapter.title;
4776
+ chapterElement.style.display = 'block';
4777
+ } else {
4778
+ chapterElement.style.display = 'none';
4779
+ }
4780
+ }
4781
+
4782
+ /**
4783
+ * Stop chapter name monitoring
4784
+ */
4785
+ stopChapterNameMonitoring() {
4786
+ if (this.chapterMonitorInterval) {
4787
+ clearInterval(this.chapterMonitorInterval);
4788
+ this.chapterMonitorInterval = null;
4789
+ }
4790
+ }
4791
+
4792
+ /**
4793
+ * Enhance progress bar tooltip to show chapter title
4794
+ */
4795
+ enhanceProgressTooltipWithChapters() {
4796
+ if (!this.chapters || this.chapters.length === 0) return;
4797
+
4798
+ const progressContainer = this.api.container.querySelector('.progress-container');
4799
+ if (!progressContainer) return;
4800
+
4801
+ // Find existing tooltip
4802
+ let tooltip = progressContainer.querySelector('.yt-seek-tooltip');
4803
+
4804
+ if (!tooltip) {
4805
+ // Create tooltip if it doesn't exist
4806
+ tooltip = document.createElement('div');
4807
+ tooltip.className = 'yt-seek-tooltip';
4808
+ tooltip.style.cssText = `
4809
+ position: absolute;
4810
+ bottom: calc(100% + 10px);
4811
+ left: 0;
4812
+ background: rgba(28, 28, 28, 0.95);
4813
+ color: #fff;
4814
+ padding: 8px 12px;
4815
+ border-radius: 4px;
4816
+ font-size: 13px;
4817
+ font-weight: 500;
4818
+ white-space: nowrap;
4819
+ pointer-events: none;
4820
+ visibility: hidden;
4821
+ z-index: 99999;
4822
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4823
+ display: flex;
4824
+ flex-direction: column;
4825
+ gap: 4px;
4826
+ min-width: 80px;
4827
+ `;
4828
+ progressContainer.appendChild(tooltip);
4829
+ }
4830
+
4831
+ // Add chapter title element to tooltip
4832
+ let chapterTitle = tooltip.querySelector('.chapter-title');
4833
+ if (!chapterTitle) {
4834
+ chapterTitle = document.createElement('div');
4835
+ chapterTitle.className = 'chapter-title';
4836
+ chapterTitle.style.cssText = `
4837
+ font-size: 12px;
4838
+ font-weight: 600;
4839
+ color: #fff;
4840
+ max-width: 200px;
4841
+ overflow: hidden;
4842
+ text-overflow: ellipsis;
4843
+ white-space: nowrap;
4844
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
4845
+ padding-bottom: 4px;
4846
+ margin-bottom: 2px;
4847
+ `;
4848
+ tooltip.insertBefore(chapterTitle, tooltip.firstChild);
4849
+ }
4850
+
4851
+ // Time display element
4852
+ let timeDisplay = tooltip.querySelector('.time-display');
4853
+ if (!timeDisplay) {
4854
+ timeDisplay = document.createElement('div');
4855
+ timeDisplay.className = 'time-display';
4856
+ timeDisplay.style.cssText = `
4857
+ font-size: 13px;
4858
+ font-weight: 500;
4859
+ color: rgba(255, 255, 255, 0.9);
4860
+ `;
4861
+ tooltip.appendChild(timeDisplay);
4862
+ }
4863
+
4864
+ // Update tooltip on mousemove
4865
+ progressContainer.addEventListener('mousemove', (e) => {
4866
+ if (!this.ytPlayer || !this.ytPlayer.getDuration) return;
4867
+
4868
+ const rect = progressContainer.getBoundingClientRect();
4869
+ const mouseX = e.clientX - rect.left;
4870
+ const percentage = Math.max(0, Math.min(1, mouseX / rect.width));
4871
+ const duration = this.ytPlayer.getDuration();
4872
+ const time = percentage * duration;
4873
+
4874
+ // Find current chapter at this time
4875
+ let currentChapter = null;
4876
+ for (let i = this.chapters.length - 1; i >= 0; i--) {
4877
+ if (time >= this.chapters[i].time) {
4878
+ currentChapter = this.chapters[i];
4879
+ break;
4880
+ }
4881
+ }
4882
+
4883
+ // Update chapter title
4884
+ if (currentChapter) {
4885
+ chapterTitle.textContent = currentChapter.title;
4886
+ chapterTitle.style.display = 'block';
4887
+ } else {
4888
+ chapterTitle.style.display = 'none';
4889
+ }
4890
+
4891
+ // Update time display
4892
+ timeDisplay.textContent = this.formatTime(time);
4893
+
4894
+ // Position tooltip
4895
+ tooltip.style.left = `${mouseX}px`;
4896
+ tooltip.style.visibility = 'visible';
4897
+ tooltip.style.transform = 'translateX(-50%)';
4898
+ });
4899
+
4900
+ progressContainer.addEventListener('mouseleave', () => {
4901
+ tooltip.style.visibility = 'hidden';
4902
+ });
4903
+
4904
+ if (this.api.player.options.debug) {
4905
+ console.log('YT Plugin: Progress tooltip enhanced with chapter info');
4906
+ }
4907
+ }
4908
+
4909
+ /**
4910
+ * Clear all cached chapters from localStorage
4911
+ */
4912
+ clearChaptersCache() {
4913
+ try {
4914
+ const keys = [];
4915
+ for (let i = 0; i < localStorage.length; i++) {
4916
+ const key = localStorage.key(i);
4917
+ if (key && key.startsWith(this.cachePrefix)) {
4918
+ keys.push(key);
4919
+ }
4920
+ }
4921
+
4922
+ keys.forEach(key => localStorage.removeItem(key));
4923
+
4924
+ if (this.api.player.options.debug) {
4925
+ console.log(`YT Plugin: Cleared ${keys.length} cached chapters`);
4926
+ }
4927
+ } catch (error) {
4928
+ if (this.api.player.options.debug) {
4929
+ console.error('YT Plugin: Error clearing cache', error);
4930
+ }
4931
+ }
4932
+ }
4933
+
4934
+ /**
4935
+ * Clear expired cache entries from localStorage
4936
+ */
4937
+ clearExpiredCache() {
4938
+ try {
4939
+ const now = Date.now();
4940
+ const keys = [];
4941
+
4942
+ for (let i = 0; i < localStorage.length; i++) {
4943
+ const key = localStorage.key(i);
4944
+ if (key && key.startsWith(this.cachePrefix)) {
4945
+ try {
4946
+ const data = JSON.parse(localStorage.getItem(key));
4947
+ if (now - data.timestamp >= this.cacheExpiration) {
4948
+ keys.push(key);
4949
+ }
4950
+ } catch (e) {
4951
+ // Invalid data, remove it
4952
+ keys.push(key);
4953
+ }
4954
+ }
4955
+ }
4956
+
4957
+ keys.forEach(key => localStorage.removeItem(key));
4958
+
4959
+ if (this.api.player.options.debug) {
4960
+ console.log(`YT Plugin: Cleared ${keys.length} expired cache entries`);
4961
+ }
4962
+ } catch (error) {
4963
+ if (this.api.player.options.debug) {
4964
+ console.error('YT Plugin: Error clearing expired cache', error);
4965
+ }
4966
+ }
4967
+ }
4968
+
4091
4969
  // ===== CLEANUP =====
4092
4970
 
4093
4971
  dispose() {
@@ -4152,9 +5030,8 @@
4152
5030
  const styleEl = document.getElementById('youtube-controls-override');
4153
5031
  if (styleEl) styleEl.remove();
4154
5032
 
4155
- if (this.player.qualities && this.player.qualities.length > 0) {
4156
- // Remove YouTube fake qualities
4157
- }
5033
+ // Stop chapter monitoring when plugin is destroyed
5034
+ this.stopChapterNameMonitoring();
4158
5035
 
4159
5036
  if (this.api.video) {
4160
5037
  this.api.video.style.display = '';