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.
@@ -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
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
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
+ &nbsp; &nbsp;
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
+ })();