myetv-player 1.7.1 → 1.7.2
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 +461 -155
- package/dist/myetv-player.js +391 -82
- package/dist/myetv-player.min.js +314 -53
- package/package.json +1 -1
- package/plugins/autosub/myetv-autosub-plugin.js +303 -155
- package/plugins/autosub/readme.md +63 -69
- package/plugins/radio/api.php +154 -0
- package/plugins/radio/icons/bolt-lightning-solid.png +0 -0
- package/plugins/radio/icons/compact-disc-solid.png +0 -0
- package/plugins/radio/icons/guitar-solid.png +0 -0
- package/plugins/radio/icons/headphones-solid.png +0 -0
- package/plugins/radio/icons/medal-solid.png +0 -0
- package/plugins/radio/icons/music-solid.png +0 -0
- package/plugins/radio/icons/newspaper-solid.png +0 -0
- package/plugins/radio/icons/radio-solid.png +0 -0
- package/plugins/radio/icons/readme.md +1 -0
- package/plugins/radio/myetv-radio-plugin.js +1655 -0
- package/plugins/radio/php/data/readme.md +1 -0
- package/plugins/radio/php/radio_stats.php +203 -0
- package/plugins/radio/proxy.php +26 -0
- package/plugins/radio/readme.md +176 -0
- package/plugins/radio/station_example.json +47 -0
- package/plugins/video-loading/myetv-video-loading-plugin.js +157 -0
- package/plugins/video-loading/readme.md +87 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder must be writable to save stats json files
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* MYETV Radio Stats Backend
|
|
4
|
+
* Handles live viewers polling and historical stats tracking via JSON files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
declare(strict_types=1);
|
|
8
|
+
|
|
9
|
+
// Enable CORS if the API is on a different subdomain/domain
|
|
10
|
+
header("Access-Control-Allow-Origin: *");
|
|
11
|
+
header("Content-Type: application/json; charset=UTF-8");
|
|
12
|
+
|
|
13
|
+
class RadioStatsManager {
|
|
14
|
+
|
|
15
|
+
private string $dataDir;
|
|
16
|
+
private int $liveTimeout;
|
|
17
|
+
|
|
18
|
+
public function __construct(string $dataDir = __DIR__ . '/data', int $liveTimeout = 30) {
|
|
19
|
+
$this->dataDir = $dataDir;
|
|
20
|
+
$this->liveTimeout = $liveTimeout; // Seconds before a client is considered offline
|
|
21
|
+
|
|
22
|
+
if (!is_dir($this->dataDir)) {
|
|
23
|
+
@mkdir($this->dataDir, 0755, true);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sanitizes strings to prevent path traversal and XSS
|
|
29
|
+
*/
|
|
30
|
+
private function sanitizeInput(?string $input): string {
|
|
31
|
+
if ($input === null) return '';
|
|
32
|
+
// Allow only alphanumeric characters, dashes, and underscores
|
|
33
|
+
return preg_replace('/[^a-zA-Z0-9\-_]/', '', $input);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Safely reads and decodes a JSON file using a shared lock
|
|
38
|
+
*/
|
|
39
|
+
private function readJson(string $filePath): array {
|
|
40
|
+
if (!file_exists($filePath)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$fp = fopen($filePath, 'r');
|
|
45
|
+
if (!$fp) return [];
|
|
46
|
+
|
|
47
|
+
flock($fp, LOCK_SH); // Shared lock for reading
|
|
48
|
+
$size = filesize($filePath);
|
|
49
|
+
$json = $size > 0 ? fread($fp, $size) : '';
|
|
50
|
+
flock($fp, LOCK_UN);
|
|
51
|
+
fclose($fp);
|
|
52
|
+
|
|
53
|
+
$data = json_decode($json ?: '', true);
|
|
54
|
+
return is_array($data) ? $data : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Safely encodes and writes to a JSON file using an exclusive lock
|
|
59
|
+
*/
|
|
60
|
+
private function writeJson(string $filePath, array $data): bool {
|
|
61
|
+
$fp = fopen($filePath, 'c'); // Open for write, create if not exists
|
|
62
|
+
if (!$fp) return false;
|
|
63
|
+
|
|
64
|
+
if (flock($fp, LOCK_EX)) { // Exclusive lock for writing
|
|
65
|
+
ftruncate($fp, 0); // Clear file content
|
|
66
|
+
fwrite($fp, json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
|
67
|
+
fflush($fp);
|
|
68
|
+
flock($fp, LOCK_UN);
|
|
69
|
+
fclose($fp);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fclose($fp);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generates time periods for history tracking
|
|
79
|
+
*/
|
|
80
|
+
private function getTimePeriods(): array {
|
|
81
|
+
$now = time();
|
|
82
|
+
return [
|
|
83
|
+
'daily' => date('Y-m-d', $now),
|
|
84
|
+
'weekly' => date('Y-W', $now), // Year and ISO-8601 week number
|
|
85
|
+
'monthly' => date('Y-m', $now),
|
|
86
|
+
'yearly' => date('Y', $now)
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Main processor
|
|
92
|
+
*/
|
|
93
|
+
public function processRequest(): void {
|
|
94
|
+
$stationId = $this->sanitizeInput($_GET['station_id'] ?? null);
|
|
95
|
+
$clientId = $this->sanitizeInput($_GET['client_id'] ?? null);
|
|
96
|
+
$status = $_GET['status'] ?? null;
|
|
97
|
+
$isPlaying = ($status === 'playing');
|
|
98
|
+
|
|
99
|
+
if (empty($stationId) || empty($clientId)) {
|
|
100
|
+
$this->sendResponse(false, ['error' => 'Missing parameters']);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
$now = time();
|
|
105
|
+
|
|
106
|
+
// Define file paths
|
|
107
|
+
$liveFile = $this->dataDir . '/live_' . $stationId . '.json';
|
|
108
|
+
$historyFile = $this->dataDir . '/history_' . $stationId . '.json';
|
|
109
|
+
$clientsFile = $this->dataDir . '/clients_' . $stationId . '.json';
|
|
110
|
+
|
|
111
|
+
// 1. Handle Live Viewers
|
|
112
|
+
$liveData = $this->readJson($liveFile);
|
|
113
|
+
|
|
114
|
+
if ($isPlaying) {
|
|
115
|
+
$liveData[$clientId] = $now;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Garbage collection: remove expired clients
|
|
119
|
+
$activeLiveCount = 0;
|
|
120
|
+
foreach ($liveData as $cId => $lastPing) {
|
|
121
|
+
if ($now - $lastPing > $this->liveTimeout) {
|
|
122
|
+
unset($liveData[$cId]);
|
|
123
|
+
} else {
|
|
124
|
+
$activeLiveCount++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
$this->writeJson($liveFile, $liveData);
|
|
129
|
+
|
|
130
|
+
// 2. Handle History Stats
|
|
131
|
+
$historyData = $this->readJson($historyFile);
|
|
132
|
+
$clientHistory = $this->readJson($clientsFile);
|
|
133
|
+
|
|
134
|
+
// Initialize default structure if empty
|
|
135
|
+
if (empty($historyData)) {
|
|
136
|
+
$historyData = [
|
|
137
|
+
'total' => 0,
|
|
138
|
+
'periods' => $this->getTimePeriods(),
|
|
139
|
+
'stats' => ['daily' => 0, 'weekly' => 0, 'monthly' => 0, 'yearly' => 0]
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
$currentPeriods = $this->getTimePeriods();
|
|
144
|
+
$historyUpdated = false;
|
|
145
|
+
|
|
146
|
+
// Reset counters if period changed (e.g., new day, new month)
|
|
147
|
+
foreach (['daily', 'weekly', 'monthly', 'yearly'] as $periodKey) {
|
|
148
|
+
if ($historyData['periods'][$periodKey] !== $currentPeriods[$periodKey]) {
|
|
149
|
+
$historyData['stats'][$periodKey] = 0;
|
|
150
|
+
$historyData['periods'][$periodKey] = $currentPeriods[$periodKey];
|
|
151
|
+
$historyUpdated = true;
|
|
152
|
+
|
|
153
|
+
// Clear the daily clients list to allow counting them again tomorrow
|
|
154
|
+
if ($periodKey === 'daily') {
|
|
155
|
+
$clientHistory = [];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If the user is playing, check if we need to count them as a new view for today
|
|
161
|
+
if ($isPlaying && !isset($clientHistory[$clientId])) {
|
|
162
|
+
$clientHistory[$clientId] = $now;
|
|
163
|
+
$this->writeJson($clientsFile, $clientHistory);
|
|
164
|
+
|
|
165
|
+
$historyData['total']++;
|
|
166
|
+
$historyData['stats']['daily']++;
|
|
167
|
+
$historyData['stats']['weekly']++;
|
|
168
|
+
$historyData['stats']['monthly']++;
|
|
169
|
+
$historyData['stats']['yearly']++;
|
|
170
|
+
$historyUpdated = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if ($historyUpdated) {
|
|
174
|
+
$this->writeJson($historyFile, $historyData);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. Output Response
|
|
178
|
+
$this->sendResponse(true, [
|
|
179
|
+
'live' => $activeLiveCount,
|
|
180
|
+
'total' => $historyData['total'],
|
|
181
|
+
'daily' => $historyData['stats']['daily'],
|
|
182
|
+
'weekly' => $historyData['stats']['weekly'],
|
|
183
|
+
'monthly' => $historyData['stats']['monthly'],
|
|
184
|
+
'yearly' => $historyData['stats']['yearly']
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Outputs the JSON payload and exits
|
|
190
|
+
*/
|
|
191
|
+
private function sendResponse(bool $success, array $payload): void {
|
|
192
|
+
echo json_encode([
|
|
193
|
+
'success' => $success,
|
|
194
|
+
'stats' => $success ? $payload : null,
|
|
195
|
+
'error' => !$success ? ($payload['error'] ?? 'Unknown error') : null
|
|
196
|
+
]);
|
|
197
|
+
exit;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Execute the request
|
|
202
|
+
$api = new RadioStatsManager();
|
|
203
|
+
$api->processRequest();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
// proxy.php?url=http://...
|
|
3
|
+
$url = $_GET['url'] ?? '';
|
|
4
|
+
|
|
5
|
+
// security: only allow valid URLs starting with http
|
|
6
|
+
if (!filter_var($url, FILTER_VALIDATE_URL) || !str_starts_with($url, 'http')) {
|
|
7
|
+
http_response_code(400);
|
|
8
|
+
exit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// header audio
|
|
12
|
+
$headers = get_headers($url, 1);
|
|
13
|
+
$contentType = $headers['Content-Type'] ?? 'audio/mpeg';
|
|
14
|
+
header('Content-Type: ' . $contentType);
|
|
15
|
+
header('Cache-Control: no-cache');
|
|
16
|
+
header('X-Accel-Buffering: no');
|
|
17
|
+
|
|
18
|
+
// direct stream
|
|
19
|
+
$stream = fopen($url, 'rb');
|
|
20
|
+
if (!$stream) { http_response_code(502); exit; }
|
|
21
|
+
|
|
22
|
+
while (!feof($stream)) {
|
|
23
|
+
echo fread($stream, 8192);
|
|
24
|
+
flush();
|
|
25
|
+
}
|
|
26
|
+
fclose($stream);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# 📻 MYETV Radio Plugin
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
A powerful and fully responsive Radio Tuner extension for the **MYETV Video Player Open Source**.
|
|
6
|
+
This plugin transforms your standard video player into a fully-fledged internet radio receiver, complete with interactive UI, real-time filtering, native player integration, and live analytics.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
12
|
+
- **🎨 Dual Themes**: Choose between two stunning interfaces:
|
|
13
|
+
- **Vintage**: A classic, draggable analog dial with a glowing needle.
|
|
14
|
+
- **Digital**: A modern, car-stereo style LCD display.
|
|
15
|
+
|
|
16
|
+
- **📊 Live Analytics**: Real-time tracking of live viewers and historical statistics (daily, weekly, monthly, yearly) using a robust backend polling system and an interactive UI modal.
|
|
17
|
+
|
|
18
|
+
- **⚡ Real-time Zapping & Filtering**: Instantly search stations by name, or filter them via the built-in country dropdown without reloading the page.
|
|
19
|
+
|
|
20
|
+
- **💾 Channel Memory**: Automatically remembers the user's last played channel using `localStorage`.
|
|
21
|
+
|
|
22
|
+
- **⌨️ Keyboard Override**: Seamlessly hijacks the native left/right arrow keys to zap through radio channels instead of seeking the timeline.
|
|
23
|
+
|
|
24
|
+
- **⚙️ Native Settings Integration**: Safely injects a custom "Radio Options" panel directly into the player's native settings menu.
|
|
25
|
+
|
|
26
|
+
- **📱 100% Responsive**: Pixel-perfect UI that adapts to mobile screens and protects the player's control bars with calculated safe-margins.
|
|
27
|
+
|
|
28
|
+
- **🧹 Clean UI**: Automatically hides irrelevant video controls (like Picture-in-Picture, playback speed, and the progress bar) for a pure audio experience.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📸 Screenshots
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="https://github.com/user-attachments/assets/1f5ffcc3-b5b2-4c90-8207-c4a8293d9b7e" alt="Vintage Theme" width="48%">
|
|
36
|
+
|
|
37
|
+
<img src="https://github.com/user-attachments/assets/95574ef2-b285-444f-af9a-1c05101d4182" alt="Digital Theme" width="48%">
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<em>Left: Vintage Theme | Right: Digital Theme</em>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🚀 Installation & Usage
|
|
47
|
+
|
|
48
|
+
1. Include the MYETV Video Player files in your HTML.
|
|
49
|
+
2. Include the `myetv-radio-plugin.js` script **after** the main player script.
|
|
50
|
+
3. Initialize the plugin via the player's configuration object.
|
|
51
|
+
|
|
52
|
+
### Basic Example
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<link rel="stylesheet" href="path/to/myetv-player.css">
|
|
56
|
+
<script src="path/to/myetv-player.js"></script>
|
|
57
|
+
|
|
58
|
+
<script src="path/to/myetv-radio-plugin.js"></script>
|
|
59
|
+
|
|
60
|
+
<script>
|
|
61
|
+
var myPlayer = new MYETVvideoplayer('videoElementId', {
|
|
62
|
+
autoplay: true,
|
|
63
|
+
plugins: {
|
|
64
|
+
radio: {
|
|
65
|
+
apiUrl: 'api.php', // required: the url of the JSON with all the radio station to load
|
|
66
|
+
theme: 'digital', // 'vintage' or 'digital'
|
|
67
|
+
startChannel: 1, // Default starting channel
|
|
68
|
+
statsApiUrl: 'https://mywebsite.com/radio/php/radio_stats.php', // Backend for live & history stats
|
|
69
|
+
statsPollInterval: 15000, // Polling interval in ms (15s)
|
|
70
|
+
filter: {
|
|
71
|
+
country: 'IT' // Filter the country selectbox (optional)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ⚙️ Configuration Options
|
|
83
|
+
|
|
84
|
+
When defining the `radio` object inside the player's plugins configuration, you can pass the following options:
|
|
85
|
+
|
|
86
|
+
| Option | Type | Default | Description |
|
|
87
|
+
| --- | --- | --- | --- |
|
|
88
|
+
| `apiUrl` | `String` | `'api.php'` | The endpoint to fetch the JSON list of radio stations. |
|
|
89
|
+
| `theme` | `String` | `'vintage'` | Sets the UI style. Accepted values: `'vintage'`, `'digital'`. |
|
|
90
|
+
| `startChannel` | `Number` | `1` | The channel to tune into on first load (overridden if the user has a saved channel in memory). |
|
|
91
|
+
| `showAllCountryOption` | `Boolean` | `true` | Show or hide the ALL option in the contry selectbox. |
|
|
92
|
+
| `startCountry` | `String` | `''` | The initial selction in the country selectbox (e.g., `'US'` or `'IT'`) |
|
|
93
|
+
| `filter.country` | `String` | `''` | 2-letter ISO code to filter the API stations (e.g., `'US'` or `'IT'`). |
|
|
94
|
+
| `filter.language` | `String` | `''` | String to filter the API stations by language (e.g, `'English'` or `'Italian'`. |
|
|
95
|
+
| `defaultIcon` | `String` | `''` | The default icon to show for the station and in the stations list. |
|
|
96
|
+
| `tagIcons` | `Array` | `'[]'` | An array of icons related to the station tag; see below for examples |
|
|
97
|
+
| `proxyUrl` | `String` | `''` | Optional proxy URL to bypass CORS issues for HTTP streams. |
|
|
98
|
+
| `statsApiUrl` | `String` | `''` | Absolute path to the backend script handling the analytics JSON operations. |
|
|
99
|
+
| `statsPollInterval` | `Number` | `15000` | Refresh rate in milliseconds for the live viewers counter. |
|
|
100
|
+
|
|
101
|
+
The API url is the `api.php` file, linked to the JSON file with all the stream url of the radio stations in the repository this is the file `station_example.json` that you can use for your tests.
|
|
102
|
+
|
|
103
|
+
The proxy option, for example proxy.php (already inside the repository), can be like this: `https://mywebsite.com/proxy.html?url=` and it can be used to stream an http file inside an https network.
|
|
104
|
+
|
|
105
|
+
Icons option example:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
defaultIcon: '[https://www.mywebsite.com/plugins/radio/icons/radio-solid.png](https://www.mywebsite.com/plugins/radio/icons/radio-solid.png)',
|
|
109
|
+
tagIcons: {
|
|
110
|
+
'news': '[https://www.mywebsite.com/plugins/radio/icons/newspaper-solid.png](https://www.mywebsite.com/plugins/radio/icons/newspaper-solid.png)',
|
|
111
|
+
'pop': '[https://www.mywebsite.com/plugins/radio/icons/headphones-solid.png](https://www.mywebsite.com/plugins/radio/icons/headphones-solid.png)',
|
|
112
|
+
'rock': '[https://www.mywebsite.com/plugins/radio/icons/bolt-lightning-solid.png](https://www.mywebsite.com/plugins/radio/icons/bolt-lightning-solid.png)',
|
|
113
|
+
'classical': '[https://www.mywebsite.com/plugins/radio/icons/music-solid.png](https://www.mywebsite.com/plugins/radio/icons/music-solid.png)',
|
|
114
|
+
'dance': '[https://www.mywebsite.com/plugins/radio/icons/compact-disc-solid.png](https://www.mywebsite.com/plugins/radio/icons/compact-disc-solid.png)',
|
|
115
|
+
'jazz': '[https://www.mywebsite.com/plugins/radio/icons/guitar-solid.png](https://www.mywebsite.com/plugins/radio/icons/guitar-solid.png)',
|
|
116
|
+
'sports': '[https://www.mywebsite.com/plugins/radio/icons/medal-solid.png](https://www.mywebsite.com/plugins/radio/icons/medal-solid.png)',
|
|
117
|
+
'religious': '[https://www.mywebsite.com/plugins/radio/icons/radio-solid.png](https://www.mywebsite.com/plugins/radio/icons/radio-solid.png)',
|
|
118
|
+
'various': '[https://www.mywebsite.com/plugins/radio/icons/radio-solid.png](https://www.mywebsite.com/plugins/radio/icons/radio-solid.png)'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🔌 Public API Methods
|
|
126
|
+
|
|
127
|
+
Once initialized, you can control the Radio plugin programmatically by accessing its instance.
|
|
128
|
+
|
|
129
|
+
| Method | Description |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `tune(channelNumber)` | Jumps directly to the specified channel number. |
|
|
132
|
+
| `play()` | Starts playback of the currently tuned station. |
|
|
133
|
+
| `stop()` | Stops playback and clears the video source. |
|
|
134
|
+
| `setTheme('themeName')` | Changes the visual theme on the fly (e.g., `setTheme('digital')`). |
|
|
135
|
+
| `dispose()` | Safely removes the radio UI, unbinds all events, stops polling, and destroys the plugin instance. |
|
|
136
|
+
|
|
137
|
+
**Example:**
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Access the plugin instance (assuming the player exposes active plugins)
|
|
141
|
+
var radioPlugin = myPlayer.getPlugin('radio'); // Use your player's specific method to get the plugin
|
|
142
|
+
|
|
143
|
+
// Change channel to 5
|
|
144
|
+
radioPlugin.tune(5);
|
|
145
|
+
|
|
146
|
+
// Switch to vintage theme
|
|
147
|
+
radioPlugin.setTheme('vintage');
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 🎮 Controls
|
|
154
|
+
|
|
155
|
+
* **Mouse / Touch**:
|
|
156
|
+
* Drag the analog dial to tune (Vintage theme).
|
|
157
|
+
* Click the `< TUNE >` buttons (Digital theme).
|
|
158
|
+
* Click the Live Viewers / Total Views counter to open the Analytics history modal.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
* **Keyboard**: Use the `Left Arrow` and `Right Arrow` keys to zap between channels seamlessly.
|
|
162
|
+
* **Control Bar**: Click the `RADIO LIST` button injected in the player's bottom bar to open the full station directory modal.
|
|
163
|
+
* **Settings Gear**: Click the native player settings icon to toggle the "Remember Channel" feature.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🤝 Contributing
|
|
168
|
+
|
|
169
|
+
Contributions, issues, and feature requests are welcome!
|
|
170
|
+
Feel free to check [issues page](https://github.com/OskarCosimo/myetv-video-player-opensource).
|
|
171
|
+
|
|
172
|
+
## 📝 License
|
|
173
|
+
|
|
174
|
+
This project is open-source and available under the MIT License.
|
|
175
|
+
|
|
176
|
+
Created by [MYETV.tv](https://www.myetv.tv) | [Oskar Cosimo](https://oskarcosimo.com)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"channel_id": 1,
|
|
4
|
+
"name": "Radio Example Pop (IT)",
|
|
5
|
+
"url": "https://stream.example.com/radio_it.mp3",
|
|
6
|
+
"url_resolved": "https://stream.example.com/radio_it.mp3",
|
|
7
|
+
"countrycode": "IT",
|
|
8
|
+
"language": "italian",
|
|
9
|
+
"tags": "pop, top40, news"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"channel_id": 2,
|
|
13
|
+
"name": "Classic Rock FM (US)",
|
|
14
|
+
"url": "https://stream.example.com/rock_us.aac",
|
|
15
|
+
"url_resolved": "https://stream.example.com/rock_us.aac",
|
|
16
|
+
"countrycode": "US",
|
|
17
|
+
"language": "english",
|
|
18
|
+
"tags": "rock, classic, 80s"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"channel_id": 3,
|
|
22
|
+
"name": "Smooth Jazz Lounge (UK)",
|
|
23
|
+
"url": "https://stream.example.com/jazz_uk.mp3",
|
|
24
|
+
"url_resolved": "https://stream.example.com/jazz_uk.mp3",
|
|
25
|
+
"countrycode": "GB",
|
|
26
|
+
"language": "english",
|
|
27
|
+
"tags": "jazz, chillout, lounge"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"channel_id": 4,
|
|
31
|
+
"name": "Chanson Française (FR)",
|
|
32
|
+
"url": "https://stream.example.com/chanson_fr.mp3",
|
|
33
|
+
"url_resolved": "https://stream.example.com/chanson_fr.mp3",
|
|
34
|
+
"countrycode": "FR",
|
|
35
|
+
"language": "french",
|
|
36
|
+
"tags": "chanson, acoustic, local"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"channel_id": 5,
|
|
40
|
+
"name": "Tokyo Synthwave (JP)",
|
|
41
|
+
"url": "https://stream.example.com/synth_jp.mp3",
|
|
42
|
+
"url_resolved": "https://stream.example.com/synth_jp.mp3",
|
|
43
|
+
"countrycode": "JP",
|
|
44
|
+
"language": "japanese",
|
|
45
|
+
"tags": "synthwave, retrowave, electronic"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MYETV Player - Video Loader Plugin
|
|
3
|
+
* File: myetv-video-loading-plugin.js
|
|
4
|
+
*/
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
class VideoLoaderPlugin {
|
|
9
|
+
constructor(player, options = {}) {
|
|
10
|
+
this.player = player;
|
|
11
|
+
this.options = {
|
|
12
|
+
videoUrl: options.videoUrl || null,
|
|
13
|
+
timeout: options.timeout !== undefined ? options.timeout : 3000,
|
|
14
|
+
backgroundColor: options.backgroundColor || '#000',
|
|
15
|
+
muted: options.muted !== undefined ? options.muted : true,
|
|
16
|
+
// Customizable text for TOS compliance, with a safe default
|
|
17
|
+
loadingText: options.loadingText || 'Loading engine...'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
this.loaderContainer = null;
|
|
21
|
+
this.loaderVideo = null;
|
|
22
|
+
// ADDED: Reference for the image element
|
|
23
|
+
this.loaderImage = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Blocks the main player queue until resolved
|
|
28
|
+
* @returns {Promise}
|
|
29
|
+
*/
|
|
30
|
+
waitForCompletion() {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
if (!this.options.videoUrl) {
|
|
33
|
+
resolve();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.initUI(resolve);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize the loader UI and start the countdown
|
|
42
|
+
*/
|
|
43
|
+
initUI(resolvePromise) {
|
|
44
|
+
if (!this.player.container) {
|
|
45
|
+
resolvePromise();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Regex check to determine if the URL is an animated image or a video
|
|
50
|
+
const isImage = /\.(gif|webp|png|jpg|jpeg)(\?.*)?$/i.test(this.options.videoUrl);
|
|
51
|
+
|
|
52
|
+
this.loaderContainer = document.createElement('div');
|
|
53
|
+
this.loaderContainer.className = 'myetv-video-loader-overlay';
|
|
54
|
+
|
|
55
|
+
// Added display flex to ensure images are centered properly
|
|
56
|
+
this.loaderContainer.style.cssText = `
|
|
57
|
+
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
58
|
+
z-index: 999999; background-color: ${this.options.backgroundColor};
|
|
59
|
+
display: flex; align-items: center; justify-content: center;
|
|
60
|
+
transition: opacity 0.5s ease;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
if (isImage) {
|
|
64
|
+
// --- IMAGE BEHAVIOR ---
|
|
65
|
+
this.loaderImage = document.createElement('img');
|
|
66
|
+
this.loaderImage.src = this.options.videoUrl;
|
|
67
|
+
this.loaderImage.style.cssText = 'width: 100%; height: 100%; object-fit: cover; pointer-events: none; border: none;';
|
|
68
|
+
this.loaderContainer.appendChild(this.loaderImage);
|
|
69
|
+
} else {
|
|
70
|
+
// --- ORIGINAL VIDEO BEHAVIOR ---
|
|
71
|
+
this.loaderVideo = document.createElement('video');
|
|
72
|
+
|
|
73
|
+
this.loaderVideo.poster = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
74
|
+
|
|
75
|
+
this.loaderVideo.autoplay = true;
|
|
76
|
+
this.loaderVideo.loop = true;
|
|
77
|
+
this.loaderVideo.muted = this.options.muted;
|
|
78
|
+
this.loaderVideo.playsInline = true;
|
|
79
|
+
this.loaderVideo.disablePictureInPicture = true;
|
|
80
|
+
|
|
81
|
+
if (this.options.muted) this.loaderVideo.setAttribute('muted', '');
|
|
82
|
+
this.loaderVideo.setAttribute('autoplay', '');
|
|
83
|
+
this.loaderVideo.setAttribute('playsinline', '');
|
|
84
|
+
|
|
85
|
+
this.loaderVideo.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; border: none; pointer-events: none; background: transparent; transform: translateZ(0); -webkit-transform: translateZ(0);';
|
|
86
|
+
|
|
87
|
+
this.loaderContainer.appendChild(this.loaderVideo);
|
|
88
|
+
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (this.loaderVideo) {
|
|
91
|
+
this.loaderVideo.src = this.options.videoUrl;
|
|
92
|
+
this.loaderVideo.load();
|
|
93
|
+
|
|
94
|
+
const playPromise = this.loaderVideo.play();
|
|
95
|
+
if (playPromise !== undefined) {
|
|
96
|
+
playPromise.catch(() => {
|
|
97
|
+
if (!this.options.muted) {
|
|
98
|
+
this.loaderVideo.muted = true;
|
|
99
|
+
this.loaderVideo.setAttribute('muted', '');
|
|
100
|
+
this.loaderVideo.play().catch(e => console.log('Loader play failed on TV:', e));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, 50);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const loadingLabel = document.createElement('div');
|
|
109
|
+
loadingLabel.innerText = this.options.loadingText;
|
|
110
|
+
loadingLabel.style.cssText = `
|
|
111
|
+
position: absolute; bottom: 20px; right: 20px;
|
|
112
|
+
background: rgba(0, 0, 0, 0.6); color: rgba(255, 255, 255, 0.8);
|
|
113
|
+
font-family: sans-serif; font-size: 11px; padding: 4px 8px;
|
|
114
|
+
border-radius: 4px; z-index: 2; pointer-events: none;
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
this.loaderContainer.appendChild(loadingLabel);
|
|
118
|
+
this.player.container.appendChild(this.loaderContainer);
|
|
119
|
+
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
this.finishAndUnlock(resolvePromise);
|
|
122
|
+
}, this.options.timeout);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fades out and resolves the queue
|
|
127
|
+
*/
|
|
128
|
+
finishAndUnlock(resolvePromise) {
|
|
129
|
+
// SIGNAL: Unlock the pipeline FIRST.
|
|
130
|
+
// This allows YouTube to silently instantiate underneath while the black screen fades.
|
|
131
|
+
resolvePromise();
|
|
132
|
+
|
|
133
|
+
if (this.loaderContainer) {
|
|
134
|
+
this.loaderContainer.style.opacity = '0';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
if (this.loaderContainer && this.loaderContainer.parentNode) {
|
|
139
|
+
this.loaderContainer.parentNode.removeChild(this.loaderContainer);
|
|
140
|
+
}
|
|
141
|
+
if (this.loaderVideo) {
|
|
142
|
+
this.loaderVideo.pause();
|
|
143
|
+
this.loaderVideo.removeAttribute('src');
|
|
144
|
+
this.loaderVideo.load();
|
|
145
|
+
}
|
|
146
|
+
if (this.loaderImage) {
|
|
147
|
+
// Clear RAM
|
|
148
|
+
this.loaderImage.removeAttribute('src');
|
|
149
|
+
}
|
|
150
|
+
}, 500);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof window.registerMYETVPlugin === 'function') {
|
|
155
|
+
window.registerMYETVPlugin('videoloader', VideoLoaderPlugin);
|
|
156
|
+
}
|
|
157
|
+
})();
|