myetv-player 1.4.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.
- package/README.md +1 -1
- package/css/myetv-player.css +7 -2
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +200 -59
- package/dist/myetv-player.min.js +152 -34
- package/package.json +2 -1
- package/plugins/soundcloud/myetv-player-soundcloud-plugin.js +927 -0
- package/plugins/soundcloud/readme.md +89 -0
- package/plugins/youtube/myetv-player-youtube-plugin.js +83 -125
|
@@ -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
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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:
|
|
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
|
|
1370
|
+
// Initialize cursor synchronization with player's auto-hide system
|
|
1420
1371
|
if (!this.options.showYouTubeUI && this.api.player.options.hideCursor) {
|
|
1421
|
-
//
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
|
|
4000
|
-
if (this.options.
|
|
4001
|
-
return;
|
|
4002
|
-
}
|
|
3963
|
+
if (this.options.showYouTubeUI) return;
|
|
3964
|
+
if (!this.api.player.options.hideCursor) return;
|
|
4003
3965
|
|
|
4004
|
-
//
|
|
4005
|
-
|
|
4006
|
-
|
|
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
|
-
|
|
4011
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
4025
|
-
|
|
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
|
-
|
|
4385
|
-
|
|
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 =
|
|
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
|
-
|
|
4420
|
-
|
|
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
|
|
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
|
|