myetv-player 1.3.0 → 1.5.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.
@@ -0,0 +1,89 @@
1
+ # MYETV SoundCloud Plugin
2
+
3
+ **SoundCloud Plugin for MyETV Player** - Seamlessly integrates SoundCloud into MyETV Player with native controls.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Native Controls**: Play/Pause, Volume, Mute/Unmute
8
+ - ✅ **Full Progress Bar**: Precise seek + time tooltip on mouseover
9
+ - ✅ **Auto Restore**: Hides SoundCloud after 10 seconds
10
+ - ✅ **Volume Slider**: Drag & drop with precise decimals (0-1)
11
+ - ✅ **YouTube Compatible**: Same style and behavior
12
+ - ✅ **Zero Conflicts**: Works only in plugin, doesn't touch main player
13
+
14
+ ## Installation
15
+
16
+ 1. **Add plugin to MyETV Player**:
17
+ ```
18
+ const player = new MyETVPlayer(container, {
19
+ plugins: [{
20
+ name: 'soundcloud',
21
+ src: 'myetv-player-soundcloud-plugin.js'
22
+ }]
23
+ });
24
+ ```
25
+
26
+ ## Detailed Features
27
+
28
+ | Function | Implementation | Status |
29
+ |----------|----------------|--------|
30
+ | **Play/Pause** | `widget.bind(PLAY/PAUSE)` | ✅ |
31
+ | **Volume** | `widget.setVolume(0-100)` | ✅ |
32
+ | **Seek** | `widget.seekTo(ms)` | ✅ |
33
+ | **Tooltip** | Mouseover progress bar | ✅ |
34
+ | **Auto-hide** | 10s timeout | ✅ |
35
+
36
+
37
+ ## Configurable Options
38
+ ```
39
+ plugins: [{
40
+ name: 'soundcloud',
41
+ src: 'myetv-player-soundcloud-plugin.js',
42
+ options: {
43
+ soundcloudUrl: 'https://soundcloud.com/artist/track', // REQUIRED
44
+ debug: true, // Detailed logs
45
+ controlsDisplayTime: 10000, // Auto-hide timeout (ms)
46
+ color: 'ff5500', // Player color (hex)
47
+ autoPlay: false, // Auto play
48
+ hideRelated: true, // Hide related tracks
49
+ showComments: false, // Show comments
50
+ showUser: true, // Show user info
51
+ showReposts: false, // Show reposts
52
+ showTeaser: false, // Show teaser
53
+ visual: false, // Waveform mode
54
+ showArtwork: true, // Show artwork
55
+ buying: false, // Show buy button
56
+ sharing: false, // Show share button
57
+ download: false, // Show download button
58
+ showPlaycount: false // Show play count
59
+ }
60
+ }]
61
+ ```
62
+
63
+ ## Compatibility
64
+
65
+ - ✅ **Desktop**: Chrome, Firefox, Safari, Edge
66
+ - ✅ **Mobile**: iOS Safari, Android Chrome
67
+ - ✅ **Smart TV**: Fire TV, Android TV
68
+ - ✅ **Picture-in-Picture**: Full support
69
+
70
+ ## Contributing
71
+
72
+ 1. Fork the project
73
+ 2. Create feature branch (`git checkout -b feature/AmazingFeature`)
74
+ 3. Commit (`git commit -m 'Add some AmazingFeature'`)
75
+ 4. Push (`git push origin feature/AmazingFeature`)
76
+ 5. Open Pull Request
77
+
78
+ ## License
79
+
80
+ Distributed under the [MIT License](LICENSE).
81
+
82
+ ## Authors & Acknowledgments
83
+
84
+ - **MYETV Team** - Main player
85
+ - **SoundCloud Widget API** - [Documentation](https://developers.soundcloud.com/docs/api/widget)
86
+
87
+ ---
88
+
89
+ ⭐ **Star this repo if useful!** ⭐
@@ -320,6 +320,25 @@
320
320
  }
321
321
  }
322
322
 
323
+ // Helper: update menu selection highlight
324
+ updateMenuHeight() {
325
+ const menu = this.api.container.querySelector('.settings-menu');
326
+ if (!menu) return;
327
+
328
+ const settingsBtn = this.api.container.querySelector('.settings-btn');
329
+ const containerRect = this.api.container.getBoundingClientRect();
330
+ const btnRect = settingsBtn.getBoundingClientRect();
331
+
332
+ // space from top of container to settings button
333
+ const distanceFromTop = btnRect.top - containerRect.top;
334
+
335
+ // Max height = distance from top - 30px padding
336
+ const maxHeight = Math.max(150, distanceFromTop - 30);
337
+
338
+ menu.style.maxHeight = `${maxHeight}px !important`;
339
+ menu.style.overflowY = 'scroll !important';
340
+ }
341
+
323
342
  /**
324
343
  * Handle responsive layout for mobile settings
325
344
  */
@@ -334,43 +353,18 @@
334
353
  pipBtn.style.display = 'none';
335
354
  }
336
355
 
337
- // Breakpoint at 600px
338
- if (containerWidth < 600) {
339
356
  // Add max-height and scroll to settings menu on mobile
340
357
  if (settingsMenu) {
341
358
  const playerHeight = this.api.container.offsetHeight;
342
- const maxMenuHeight = playerHeight - 100; // Leave 100px margin from top/bottom
343
-
344
- settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
345
- settingsMenu.style.overflowY = 'auto';
346
- settingsMenu.style.overflowX = 'hidden';
347
-
348
- // Add scrollbar styling
349
- if (!document.getElementById('yt-settings-scrollbar-style')) {
350
- const scrollbarStyle = document.createElement('style');
351
- scrollbarStyle.id = 'yt-settings-scrollbar-style';
352
- scrollbarStyle.textContent = `
353
- .settings-menu::-webkit-scrollbar {
354
- width: 6px;
355
- }
356
- .settings-menu::-webkit-scrollbar-track {
357
- background: rgba(255,255,255,0.05);
358
- border-radius: 3px;
359
- }
360
- .settings-menu::-webkit-scrollbar-thumb {
361
- background: rgba(255,255,255,0.3);
362
- border-radius: 3px;
363
- }
364
- .settings-menu::-webkit-scrollbar-thumb:hover {
365
- background: rgba(255,255,255,0.5);
366
- }
367
- `;
368
- document.head.appendChild(scrollbarStyle);
369
- }
359
+ const settingsBtn = this.api.container.querySelector('.settings-btn');
360
+ const settingsBtnRect = settingsBtn.getBoundingClientRect();
361
+ const containerRect = this.api.container.getBoundingClientRect();
362
+
363
+ // Calculate distance from top of container to settings button
364
+ const distanceFromTop = settingsBtnRect.top - containerRect.top;
365
+
366
+ const maxMenuHeight = Math.min(300, playerHeight * 0.5);
370
367
 
371
- // Firefox scrollbar
372
- settingsMenu.style.scrollbarWidth = 'thin';
373
- settingsMenu.style.scrollbarColor = 'rgba(255,255,255,0.3) transparent';
374
368
  }
375
369
 
376
370
  // Hide subtitles button
@@ -417,7 +411,6 @@
417
411
  // Create trigger
418
412
  const trigger = document.createElement('div');
419
413
  trigger.className = 'quality-option';
420
- trigger.style.fontSize = '10px';
421
414
  trigger.textContent = subtitlesText;
422
415
 
423
416
  // Add arrow indicator
@@ -450,7 +443,6 @@
450
443
  padding: 6px 12px;
451
444
  cursor: pointer;
452
445
  color: white;
453
- font-size: 10px;
454
446
  white-space: normal;
455
447
  word-wrap: break-word;
456
448
  opacity: 0.8;
@@ -494,16 +486,19 @@
494
486
  let isExpanded = false;
495
487
  trigger.addEventListener('click', (e) => {
496
488
  e.stopPropagation();
497
-
498
489
  isExpanded = !isExpanded;
499
490
 
500
491
  if (isExpanded) {
501
492
  rebuildOptions();
502
493
  optionsContainer.style.display = 'block';
503
494
  arrow.style.transform = 'rotate(180deg)';
495
+
496
+ setTimeout(() => this.updateMenuHeight(), 10);
504
497
  } else {
505
498
  optionsContainer.style.display = 'none';
506
499
  arrow.style.transform = 'rotate(0deg)';
500
+
501
+ setTimeout(() => this.updateMenuHeight(), 10);
507
502
  }
508
503
  });
509
504
 
@@ -533,7 +528,6 @@
533
528
  // Create trigger
534
529
  const trigger = document.createElement('div');
535
530
  trigger.className = 'quality-option';
536
- trigger.style.fontSize = '10px';
537
531
 
538
532
  // Get current speed
539
533
  const getCurrentSpeed = () => {
@@ -574,7 +568,6 @@
574
568
  padding: 6px 12px;
575
569
  cursor: pointer;
576
570
  color: white;
577
- font-size: 10px;
578
571
  white-space: normal;
579
572
  word-wrap: break-word;
580
573
  opacity: 0.8;
@@ -630,9 +623,11 @@
630
623
  rebuildOptions();
631
624
  optionsContainer.style.display = 'block';
632
625
  arrow.style.transform = 'rotate(180deg)';
626
+ setTimeout(() => this.updateMenuHeight(), 10);
633
627
  } else {
634
628
  optionsContainer.style.display = 'none';
635
629
  arrow.style.transform = 'rotate(0deg)';
630
+ setTimeout(() => this.updateMenuHeight(), 10);
636
631
  }
637
632
  });
638
633
 
@@ -649,51 +644,7 @@
649
644
  }
650
645
  }
651
646
  }
652
- } else {
653
- // Wide screen
654
- if (subtitlesBtn) {
655
- subtitlesBtn.style.display = '';
656
- }
657
-
658
- // Reset settings menu styles
659
- if (settingsMenu) {
660
- settingsMenu.style.maxHeight = '';
661
- settingsMenu.style.overflowY = '';
662
- settingsMenu.style.overflowX = '';
663
- settingsMenu.style.scrollbarWidth = '';
664
- settingsMenu.style.scrollbarColor = '';
665
- }
666
-
667
- // Show original speed option again
668
- if (settingsMenu) {
669
- const originalSpeedOption = settingsMenu.querySelector('[data-action="speed"]');
670
- if (originalSpeedOption) {
671
- originalSpeedOption.style.display = '';
672
- }
673
-
674
- // Show expandable speed option again
675
- const expandableSpeedWrapper = settingsMenu.querySelector('[data-action="speed-expand"]');
676
- if (expandableSpeedWrapper) {
677
- const wrapper = expandableSpeedWrapper.closest('.settings-expandable-wrapper');
678
- if (wrapper) {
679
- wrapper.style.display = '';
680
- }
681
- }
682
- }
683
-
684
- // Remove from settings
685
- if (settingsMenu) {
686
- const subtitlesWrapper = settingsMenu.querySelector('.yt-subtitles-wrapper');
687
- if (subtitlesWrapper) {
688
- subtitlesWrapper.remove();
689
- }
690
647
 
691
- const speedWrapper = settingsMenu.querySelector('.yt-speed-wrapper');
692
- if (speedWrapper) {
693
- speedWrapper.remove();
694
- }
695
- }
696
- }
697
648
  }
698
649
 
699
650
  hidePipFromSettingsMenuOnly() {
@@ -1047,7 +998,7 @@
1047
998
  z-index: 2;
1048
999
  background: transparent;
1049
1000
  pointer-events: ${pointerEvents};
1050
- cursor: default;
1001
+ cursor: auto;
1051
1002
  `;
1052
1003
 
1053
1004
  this.api.container.insertBefore(this.mouseMoveOverlay, this.api.controls);
@@ -1416,14 +1367,27 @@
1416
1367
  this.checkInitialCaptionState();
1417
1368
  }, 2500); // after 2.5s
1418
1369
 
1419
- // Initialize cursor state based on controls visibility
1370
+ // Initialize cursor synchronization with player's auto-hide system
1420
1371
  if (!this.options.showYouTubeUI && this.api.player.options.hideCursor) {
1421
- // Check if controls are visible
1422
- const controlsVisible = this.api.controls && this.api.controls.classList.contains('show');
1423
- if (!controlsVisible) {
1424
- this.hideCursor();
1372
+ // Hook into player's hideControlsNow method
1373
+ const originalHideControlsNow = this.api.player.hideControlsNow;
1374
+ this.api.player.hideControlsNow = function () {
1375
+ originalHideControlsNow.call(this);
1376
+ // Cursor is already hidden by the player's hideCursor() call inside hideControlsNow
1377
+ };
1378
+
1379
+ // Hook into player's showControlsNow method
1380
+ const originalShowControlsNow = this.api.player.showControlsNow;
1381
+ this.api.player.showControlsNow = function () {
1382
+ originalShowControlsNow.call(this);
1383
+ // Cursor is already shown by the player's showCursor() call inside showControlsNow
1384
+ };
1385
+
1386
+ if (this.api.player.options.debug) {
1387
+ console.log('[YT Plugin] Cursor sync hooks installed');
1425
1388
  }
1426
1389
  }
1390
+
1427
1391
  if (this.api.player.options.debug) console.log('YT Plugin: Setup completed');
1428
1392
  this.api.triggerEvent('youtubeplugin:playerready', {});
1429
1393
 
@@ -3996,19 +3960,20 @@
3996
3960
  * Only works when showYouTubeUI is false (custom controls)
3997
3961
  */
3998
3962
  hideCursor() {
3999
- // Don't hide cursor if YouTube native UI is active
4000
- if (this.options.showYouTubeUI) {
4001
- return;
4002
- }
3963
+ if (this.options.showYouTubeUI) return;
3964
+ if (!this.api.player.options.hideCursor) return;
4003
3965
 
4004
- // Add hide-cursor class to MAIN PLAYER CONTAINER
4005
- // This ensures cursor is hidden everywhere in the player
4006
- if (this.api.container) {
4007
- this.api.container.classList.add('hide-cursor');
3966
+ // Update main container
3967
+ if (this.api.player && this.api.player.container) {
3968
+ this.api.player.container.classList.add('hide-cursor');
4008
3969
  }
4009
3970
 
4010
- if (this.api.player.options.debug) {
4011
- console.log('[YT Plugin] Cursor hidden on main container');
3971
+ // Update overlay cursor as well!
3972
+ if (this.mouseMoveOverlay) {
3973
+ this.mouseMoveOverlay.style.cursor = 'none';
3974
+ if (this.api.player.options.debug) {
3975
+ console.log('[YT Plugin] Overlay cursor hidden');
3976
+ }
4012
3977
  }
4013
3978
  }
4014
3979
 
@@ -4016,13 +3981,17 @@
4016
3981
  * Show mouse cursor in YouTube player
4017
3982
  */
4018
3983
  showCursor() {
4019
- // Remove hide-cursor class from MAIN PLAYER CONTAINER
4020
- if (this.api.container) {
4021
- this.api.container.classList.remove('hide-cursor');
3984
+ // Update main container
3985
+ if (this.api.player && this.api.player.container) {
3986
+ this.api.player.container.classList.remove('hide-cursor');
4022
3987
  }
4023
3988
 
4024
- if (this.api.player.options.debug) {
4025
- console.log('[YT Plugin] Cursor shown on main container');
3989
+ // Update overlay cursor back to default!
3990
+ if (this.mouseMoveOverlay) {
3991
+ this.mouseMoveOverlay.style.cursor = 'default';
3992
+ if (this.api.player.options.debug) {
3993
+ console.log('[YT Plugin] Overlay cursor shown');
3994
+ }
4026
3995
  }
4027
3996
  }
4028
3997
 
@@ -4381,18 +4350,19 @@
4381
4350
  }
4382
4351
 
4383
4352
  /**
4384
- * Parse chapters from video description
4385
- * Validates YouTube chapter requirements: 3+ chapters, starts at 0:00, 10+ seconds each
4386
- */
4353
+ * Parse chapters from video description
4354
+ * Validates YouTube chapter requirements: 3 chapters, starts at 0:00
4355
+ */
4387
4356
  parseChaptersFromDescription(description) {
4388
4357
  if (!description) return null;
4389
4358
 
4390
4359
  const chapters = [];
4360
+
4391
4361
  // Regex for timestamps: 0:00, 00:00, 0:00:00
4392
4362
  // 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;
4363
+ const timestampRegex = /^(\d{1,2}:\d{2}(?::\d{2})?)\s*[-–—]*\s*(.+)$/gm;
4395
4364
 
4365
+ let match;
4396
4366
  while ((match = timestampRegex.exec(description)) !== null) {
4397
4367
  const timeString = match[1].trim();
4398
4368
  const title = match[2].trim();
@@ -4401,7 +4371,6 @@
4401
4371
  if (!title || title.length < 2) continue;
4402
4372
 
4403
4373
  const seconds = this.parseTimeToSeconds(timeString);
4404
-
4405
4374
  if (seconds !== null) {
4406
4375
  chapters.push({
4407
4376
  time: seconds,
@@ -4416,27 +4385,16 @@
4416
4385
 
4417
4386
  // Validate: at least 3 chapters and first starts at 0:00
4418
4387
  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;
4388
+ if (this.api.player.options.debug) {
4389
+ console.log('[YT Plugin] ✅ Found', chapters.length, 'valid chapters');
4433
4390
  }
4391
+ return chapters;
4434
4392
  }
4435
4393
 
4436
4394
  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)');
4395
+ console.log('[YT Plugin] No valid chapters found (requires 3 chapters starting at 0:00)');
4396
+ console.log('[YT Plugin] Debug: Found', chapters.length, 'chapters, first time:', chapters[0]?.time);
4438
4397
  }
4439
-
4440
4398
  return null;
4441
4399
  }
4442
4400