Added proxy route for channel icons and other images, updated readme
This commit is contained in:
@@ -157,14 +157,15 @@ If you're running NodeCast TV behind a reverse proxy (Nginx, Caddy, Traefik) wit
|
|||||||
| Symptom | Likely Cause | Solution |
|
| Symptom | Likely Cause | Solution |
|
||||||
|---------|--------------|----------|
|
|---------|--------------|----------|
|
||||||
| Streams fail with `fragLoadError` | Mixed content (HTTPS page loading HTTP streams) | Enable **"Force Backend Proxy"** in Settings → Streaming |
|
| Streams fail with `fragLoadError` | Mixed content (HTTPS page loading HTTP streams) | Enable **"Force Backend Proxy"** in Settings → Streaming |
|
||||||
| Channel icons don't load | Mixed content on images | Icons are loaded directly; no fix yet for HTTP icons on HTTPS |
|
| Channel icons don't load | Mixed content on images | Icons are now automatically proxied on HTTPS - update to latest version |
|
||||||
| Streams work on HTTP but not HTTPS | Reverse proxy not passing streams correctly | Ensure your proxy config supports streaming (disable buffering) |
|
| Streams work on HTTP but not HTTPS | Reverse proxy not passing headers correctly | Ensure `X-Forwarded-Proto` header is set (see examples below) |
|
||||||
|
|
||||||
**Caddy example:**
|
**Caddy example:**
|
||||||
```
|
```
|
||||||
tv.domain.com {
|
tv.domain.com {
|
||||||
reverse_proxy nodecast:3000 {
|
reverse_proxy nodecast:3000 {
|
||||||
flush_interval -1
|
flush_interval -1
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -176,10 +177,11 @@ location / {
|
|||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### TVHeadend
|
### TVHeadend
|
||||||
|
|
||||||
If you're using TVHeadend as your source, you may need to configure a few settings for streams to play correctly in NodeCast TV:
|
If you're using TVHeadend as your source, you may need to configure a few settings for streams to play correctly in NodeCast TV:
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ class ChannelList {
|
|||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get proxied image URL to avoid mixed content errors on HTTPS
|
||||||
|
* Only proxies HTTP URLs when on HTTPS page
|
||||||
|
*/
|
||||||
|
getProxiedImageUrl(url) {
|
||||||
|
if (!url || url.length === 0) return '/img/placeholder.png';
|
||||||
|
// Only proxy if we're on HTTPS and the image is HTTP
|
||||||
|
if (window.location.protocol === 'https:' && url.startsWith('http://')) {
|
||||||
|
return `/api/proxy/image?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load collapsed state from localStorage
|
* Load collapsed state from localStorage
|
||||||
*/
|
*/
|
||||||
@@ -399,7 +412,7 @@ class ChannelList {
|
|||||||
data-stream-id="${channel.streamId || ''}"
|
data-stream-id="${channel.streamId || ''}"
|
||||||
data-url="${channel.url || ''}"
|
data-url="${channel.url || ''}"
|
||||||
data-render-id="${renderId}">
|
data-render-id="${renderId}">
|
||||||
<img class="channel-logo" src="${channel.tvgLogo && channel.tvgLogo.length > 0 ? channel.tvgLogo : '/img/placeholder.png'}"
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
<div class="channel-info">
|
<div class="channel-info">
|
||||||
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||||
@@ -851,7 +864,7 @@ class ChannelList {
|
|||||||
div.dataset.url = channel.url || '';
|
div.dataset.url = channel.url || '';
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<img class="channel-logo" src="${channel.tvgLogo && channel.tvgLogo.length > 0 ? channel.tvgLogo : '/img/placeholder.png'}"
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
<div class="channel-info">
|
<div class="channel-info">
|
||||||
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||||
@@ -1102,7 +1115,7 @@ class ChannelList {
|
|||||||
modalBody.innerHTML = `
|
modalBody.innerHTML = `
|
||||||
<div class="epg-info-modal">
|
<div class="epg-info-modal">
|
||||||
<div class="channel-details">
|
<div class="channel-details">
|
||||||
<img class="channel-logo" src="${channel.tvgLogo || '/img/placeholder.png'}"
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
onerror="this.onerror=null;this.src='/img/placeholder.png'" />
|
onerror="this.onerror=null;this.src='/img/placeholder.png'" />
|
||||||
<div class="channel-meta">
|
<div class="channel-meta">
|
||||||
<p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p>
|
<p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p>
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ class EpgGuide {
|
|||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get proxied image URL to avoid mixed content errors on HTTPS
|
||||||
|
* Only proxies HTTP URLs when on HTTPS page
|
||||||
|
*/
|
||||||
|
getProxiedImageUrl(url) {
|
||||||
|
if (!url || url.length === 0) return '/img/placeholder.png';
|
||||||
|
// Only proxy if we're on HTTPS and the image is HTTP
|
||||||
|
if (window.location.protocol === 'https:' && url.startsWith('http://')) {
|
||||||
|
return `/api/proxy/image?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.prevBtn.addEventListener('click', () => this.navigate(-2));
|
this.prevBtn.addEventListener('click', () => this.navigate(-2));
|
||||||
this.nextBtn.addEventListener('click', () => this.navigate(2));
|
this.nextBtn.addEventListener('click', () => this.navigate(2));
|
||||||
@@ -440,7 +453,7 @@ class EpgGuide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback values if EPG channel is missing
|
// Fallback values if EPG channel is missing
|
||||||
const logo = sourceChannel.tvgLogo || (epgChannel && epgChannel.icon) || '/img/placeholder.png';
|
const logo = this.getProxiedImageUrl(sourceChannel.tvgLogo || (epgChannel && epgChannel.icon));
|
||||||
const name = sourceChannel.name || (epgChannel && epgChannel.name);
|
const name = sourceChannel.name || (epgChannel && epgChannel.name);
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
|
|||||||
@@ -397,4 +397,42 @@ router.get('/stream', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy images (channel logos, posters)
|
||||||
|
* Fixes mixed content errors when loading HTTP images on HTTPS pages
|
||||||
|
* GET /api/proxy/image?url=...
|
||||||
|
*/
|
||||||
|
router.get('/image', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { url } = req.query;
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({ error: 'URL required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'image/*,*/*;q=0.8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return res.status(response.status).send('Failed to fetch image');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward content type
|
||||||
|
const contentType = response.headers.get('content-type') || 'image/png';
|
||||||
|
res.set('Content-Type', contentType);
|
||||||
|
res.set('Access-Control-Allow-Origin', '*');
|
||||||
|
res.set('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours
|
||||||
|
|
||||||
|
// Pipe the image data
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
res.send(Buffer.from(buffer));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Image proxy error:', err.message);
|
||||||
|
res.status(500).send('Image proxy error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user