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 |
|
||||
|---------|--------------|----------|
|
||||
| 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 |
|
||||
| Streams work on HTTP but not HTTPS | Reverse proxy not passing streams correctly | Ensure your proxy config supports streaming (disable buffering) |
|
||||
| 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 headers correctly | Ensure `X-Forwarded-Proto` header is set (see examples below) |
|
||||
|
||||
**Caddy example:**
|
||||
```
|
||||
tv.domain.com {
|
||||
reverse_proxy nodecast:3000 {
|
||||
flush_interval -1
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -176,10 +177,11 @@ location / {
|
||||
proxy_buffering off;
|
||||
proxy_set_header Host $host;
|
||||
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
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -399,7 +412,7 @@ class ChannelList {
|
||||
data-stream-id="${channel.streamId || ''}"
|
||||
data-url="${channel.url || ''}"
|
||||
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'">
|
||||
<div class="channel-info">
|
||||
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||
@@ -851,7 +864,7 @@ class ChannelList {
|
||||
div.dataset.url = channel.url || '';
|
||||
|
||||
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'">
|
||||
<div class="channel-info">
|
||||
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||
@@ -1102,7 +1115,7 @@ class ChannelList {
|
||||
modalBody.innerHTML = `
|
||||
<div class="epg-info-modal">
|
||||
<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'" />
|
||||
<div class="channel-meta">
|
||||
<p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p>
|
||||
|
||||
@@ -34,6 +34,19 @@ class EpgGuide {
|
||||
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() {
|
||||
this.prevBtn.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
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user