diff --git a/Assets/online-sources.json b/Assets/online-sources.json
index 952ec04..3f497be 100644
--- a/Assets/online-sources.json
+++ b/Assets/online-sources.json
@@ -1,191 +1,59 @@
{
- "_comment": "Online-Streams für HomeStream. URLs Stand Mai 2026, Quelle: harryshomepage.de + rundfunkforum. Privatsender (RTL, ProSieben, Sat.1) NICHT enthalten wegen Lizenz/Paywall (Joyn, RTL+).",
+ "_comment": "Online-Streams fuer HomeStream. URLs Stand Mai 2026, Quelle: harryshomepage.de + rundfunkforum.",
"tv": [
- {
- "name": "Das Erste HD",
- "url": "https://daserste-live.ard-mcdn.de/daserste/live/hls/de/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "ZDF HD",
- "url": "https://zdf-hls-15.akamaized.net/hls/live/2016498/de/veryhigh/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "3sat HD",
- "url": "https://zdf-hls-18.akamaized.net/hls/live/2016501/dach/veryhigh/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "ARTE HD",
- "url": "https://artesimulcast.akamaized.net/hls/live/2030993/artelive_de/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "ARD-alpha HD",
- "url": "https://mcdn.br.de/br/fs/ard_alpha/hls/de/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "ONE HD",
- "url": "https://mcdn-one.ard.de/ardone/hls/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "ZDFneo HD",
- "url": "https://zdf-hls-16.akamaized.net/hls/live/2016499/de/veryhigh/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "ZDFinfo HD",
- "url": "https://zdf-hls-17.akamaized.net/hls/live/2016500/de/veryhigh/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "Tagesschau24 HD",
- "url": "https://tagesschau.akamaized.net/hls/live/2020117/tagesschau/tagesschau_3/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "Phoenix HD",
- "url": "https://zdf-hls-19.akamaized.net/hls/live/2016502/de/veryhigh/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "BR Süd HD",
- "url": "https://livestreams.br.de/i/bfssued_germany@119890/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "WDR HD",
- "url": "https://wdr-wdrfernsehen-koeln.icecast.wdr.de/wdr/wdrfernsehen/koeln/hls/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "NDR HD",
- "url": "https://ndrfs-lh.akamaihd.net/i/ndrfs_nds@430233/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "HR HD",
- "url": "https://hr-hrfernsehen-live.akamaized.net/hls/live/2016105/hrfernsehen/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "MDR Sachsen HD",
- "url": "https://mdrsnhls-lh.akamaihd.net/i/livetvmdr_sachsen@439432/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "RBB Berlin HD",
- "url": "https://rbb_berlin-lh.akamaihd.net/i/rbbBerlin_Live@380294/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "SWR BW HD",
- "url": "https://swrbw-lh.akamaihd.net/i/swrbw_live@196738/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "SR Fernsehen HD",
- "url": "https://live2_sr-lh.akamaihd.net/i/sr_universal02@107595/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "KiKA HD",
- "url": "https://kikageohls.akamaized.net/hls/live/2022693/livetvkika_de/master.m3u8",
- "kind": "TvHd"
- },
- {
- "name": "Deutsche Welle",
- "url": "https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8",
- "kind": "TvHd"
- }
+ { "name": "Das Erste HD", "url": "https://daserste-live.ard-mcdn.de/daserste/live/hls/de/master.m3u8", "kind": "TvHd" },
+ { "name": "ZDF HD", "url": "https://zdf-hls-15.akamaized.net/hls/live/2016498/de/veryhigh/master.m3u8", "kind": "TvHd" },
+ { "name": "3sat HD", "url": "https://zdf-hls-18.akamaized.net/hls/live/2016501/dach/veryhigh/master.m3u8", "kind": "TvHd" },
+ { "name": "ARTE HD", "url": "https://artesimulcast.akamaized.net/hls/live/2030993/artelive_de/master.m3u8", "kind": "TvHd" },
+ { "name": "ARD-alpha HD", "url": "https://mcdn.br.de/br/fs/ard_alpha/hls/de/master.m3u8", "kind": "TvHd" },
+ { "name": "ONE HD", "url": "https://mcdn-one.ard.de/ardone/hls/master.m3u8", "kind": "TvHd" },
+ { "name": "ZDFneo HD", "url": "https://zdf-hls-16.akamaized.net/hls/live/2016499/de/veryhigh/master.m3u8", "kind": "TvHd" },
+ { "name": "ZDFinfo HD", "url": "https://zdf-hls-17.akamaized.net/hls/live/2016500/de/veryhigh/master.m3u8", "kind": "TvHd" },
+ { "name": "Tagesschau24 HD", "url": "https://tagesschau.akamaized.net/hls/live/2020117/tagesschau/tagesschau_3/master.m3u8", "kind": "TvHd" },
+ { "name": "Phoenix HD", "url": "https://zdf-hls-19.akamaized.net/hls/live/2016502/de/veryhigh/master.m3u8", "kind": "TvHd" },
+ { "name": "BR Sued HD", "url": "https://livestreams.br.de/i/bfssued_germany@119890/master.m3u8", "kind": "TvHd" },
+ { "name": "WDR HD", "url": "https://wdr-wdrfernsehen-koeln.icecast.wdr.de/wdr/wdrfernsehen/koeln/hls/master.m3u8", "kind": "TvHd" },
+ { "name": "NDR HD", "url": "https://ndrfs-lh.akamaihd.net/i/ndrfs_nds@430233/master.m3u8", "kind": "TvHd" },
+ { "name": "HR HD", "url": "https://hr-hrfernsehen-live.akamaized.net/hls/live/2016105/hrfernsehen/master.m3u8", "kind": "TvHd" },
+ { "name": "MDR Sachsen HD", "url": "https://mdrsnhls-lh.akamaihd.net/i/livetvmdr_sachsen@439432/master.m3u8", "kind": "TvHd" },
+ { "name": "RBB Berlin HD", "url": "https://rbb_berlin-lh.akamaihd.net/i/rbbBerlin_Live@380294/master.m3u8", "kind": "TvHd" },
+ { "name": "SWR BW HD", "url": "https://swrbw-lh.akamaihd.net/i/swrbw_live@196738/master.m3u8", "kind": "TvHd" },
+ { "name": "SR Fernsehen HD", "url": "https://live2_sr-lh.akamaihd.net/i/sr_universal02@107595/master.m3u8", "kind": "TvHd" },
+ { "name": "KiKA HD", "url": "https://kikageohls.akamaized.net/hls/live/2022693/livetvkika_de/master.m3u8", "kind": "TvHd" },
+ { "name": "Deutsche Welle", "url": "https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8", "kind": "TvHd" }
],
"radio": [
- {
- "name": "Bayern 1",
- "url": "https://dispatcher.rndfnk.com/br/br1/obb/mp3/high"
- },
- {
- "name": "Bayern 2",
- "url": "https://dispatcher.rndfnk.com/br/br2/sued/mp3/high"
- },
- {
- "name": "Bayern 3",
- "url": "https://dispatcher.rndfnk.com/br/br3/live/mp3/high"
- },
- {
- "name": "BR Klassik",
- "url": "https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high"
- },
- {
- "name": "B5 aktuell",
- "url": "https://dispatcher.rndfnk.com/br/br5/live/mp3/high"
- },
- {
- "name": "Deutschlandfunk",
- "url": "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3"
- },
- {
- "name": "Deutschlandfunk Kultur",
- "url": "https://st02.sslstream.dlf.de/dlf/02/128/mp3/stream.mp3"
- },
- {
- "name": "Deutschlandfunk Nova",
- "url": "https://st03.sslstream.dlf.de/dlf/03/128/mp3/stream.mp3"
- },
- {
- "name": "WDR 2",
- "url": "https://wdr-wdr2-rheinland.icecastssl.wdr.de/wdr/wdr2/rheinland/mp3/128/stream.mp3"
- },
- {
- "name": "WDR 4",
- "url": "https://wdr-wdr4-live.icecastssl.wdr.de/wdr/wdr4/live/mp3/128/stream.mp3"
- },
- {
- "name": "1LIVE",
- "url": "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3"
- },
- {
- "name": "NDR 2",
- "url": "https://icecast.ndr.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3"
- },
- {
- "name": "N-JOY",
- "url": "https://icecast.ndr.de/ndr/njoy/live/mp3/128/stream.mp3"
- },
- {
- "name": "SWR1 BW",
- "url": "https://liveradio.swr.de/sw282p3/swr1bw/play.mp3"
- },
- {
- "name": "SWR3",
- "url": "https://liveradio.swr.de/sw282p3/swr3/play.mp3"
- },
- {
- "name": "HR1",
- "url": "https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3"
- },
- {
- "name": "HR3",
- "url": "https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3"
- },
- {
- "name": "MDR Sachsen",
- "url": "https://mdr-284350-0.cast.mdr.de/mdr/284350/0/mp3/high/stream.mp3"
- },
- {
- "name": "RBB Radio Eins",
- "url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3"
- },
- {
- "name": "Antenne Bayern",
- "url": "https://stream.antenne.de/antenne/stream/mp3"
- },
- {
- "name": "Radio Eins",
- "url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3"
- }
+ { "name": "Bayern 1", "url": "https://dispatcher.rndfnk.com/br/br1/obb/mp3/high" },
+ { "name": "Bayern 2", "url": "https://dispatcher.rndfnk.com/br/br2/sued/mp3/high" },
+ { "name": "Bayern 3", "url": "https://dispatcher.rndfnk.com/br/br3/live/mp3/high" },
+ { "name": "BR Klassik", "url": "https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high" },
+ { "name": "B5 aktuell", "url": "https://dispatcher.rndfnk.com/br/br5/live/mp3/high" },
+ { "name": "Deutschlandfunk", "url": "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3" },
+ { "name": "Deutschlandfunk Kultur", "url": "https://st02.sslstream.dlf.de/dlf/02/128/mp3/stream.mp3" },
+ { "name": "Deutschlandfunk Nova", "url": "https://st03.sslstream.dlf.de/dlf/03/128/mp3/stream.mp3" },
+ { "name": "WDR 2", "url": "https://wdr-wdr2-rheinland.icecastssl.wdr.de/wdr/wdr2/rheinland/mp3/128/stream.mp3" },
+ { "name": "WDR 4", "url": "https://wdr-wdr4-live.icecastssl.wdr.de/wdr/wdr4/live/mp3/128/stream.mp3" },
+ { "name": "1LIVE", "url": "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3" },
+ { "name": "NDR 2", "url": "https://icecast.ndr.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3" },
+ { "name": "N-JOY", "url": "https://icecast.ndr.de/ndr/njoy/live/mp3/128/stream.mp3" },
+ { "name": "SWR1 BW", "url": "https://liveradio.swr.de/sw282p3/swr1bw/play.mp3" },
+ { "name": "SWR3", "url": "https://liveradio.swr.de/sw282p3/swr3/play.mp3" },
+ { "name": "HR1", "url": "https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3" },
+ { "name": "HR3", "url": "https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3" },
+ { "name": "MDR Sachsen", "url": "https://mdr-284350-0.cast.mdr.de/mdr/284350/0/mp3/high/stream.mp3" },
+ { "name": "RBB Radio Eins", "url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3" },
+ { "name": "Antenne Bayern", "url": "https://stream.antenne.de/antenne/stream/mp3" }
+ ],
+ "web": [
+ { "name": "YouTube", "url": "https://www.youtube.com" },
+ { "name": "ARD Mediathek", "url": "https://www.ardmediathek.de" },
+ { "name": "ZDF Mediathek", "url": "https://www.zdf.de/serien-und-filme" },
+ { "name": "Netflix", "url": "https://www.netflix.com" },
+ { "name": "Disney+", "url": "https://www.disneyplus.com" },
+ { "name": "Joyn", "url": "https://www.joyn.de" },
+ { "name": "RTL+", "url": "https://plus.rtl.de" },
+ { "name": "ARTE", "url": "https://www.arte.tv/de" },
+ { "name": "Apple TV+", "url": "https://tv.apple.com" },
+ { "name": "Amazon Prime Video", "url": "https://www.amazon.de/gp/video/storefront" }
]
}
diff --git a/HomeStream.csproj b/HomeStream.csproj
index de50237..ef15772 100644
--- a/HomeStream.csproj
+++ b/HomeStream.csproj
@@ -19,6 +19,7 @@
+
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 5e76b52..dab8199 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -1,6 +1,7 @@
+
@@ -224,8 +227,9 @@
-
+
+
+
+
+
+
{
if (_currentChannel == null) return;
- // EIT alle 10s neu lesen (FritzBox-TV)
if (_currentChannel.Source == ChannelSource.FritzBox && _currentMedia != null)
UpdateEpgFromMedia(_currentMedia);
- // XMLTV nur alle 60s (Online-TV + Danach für alle)
_epgRefreshTick++;
if (_epgRefreshTick >= 6)
{
@@ -86,7 +88,6 @@ public partial class MainWindow : Window
private void OnVideoDoubleClick(object sender, MouseButtonEventArgs e)
{
- // Doppelklick auf VideoView → Vollbild
ToggleFullscreen();
e.Handled = true;
}
@@ -104,16 +105,35 @@ public partial class MainWindow : Window
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
- // Core.Initialize lädt libVLC-Natives (~5-10s) und blockiert sonst den UI-Thread.
- // Volume vorab auf UI-Thread lesen, dann Natives auf Background-Thread initialisieren.
var initialVolume = (int)SldVolume.Value;
await Task.Run(() => InitializePlayerNatives(initialVolume));
- VideoView.MediaPlayer = _player; // UI-Zuweisung auf UI-Thread
+ VideoView.MediaPlayer = _player;
+
+ // WebView2 async initialisieren (persistentes Profil in %APPDATA%\HomeStream\webview2)
+ _ = InitializeWebViewAsync();
+
await LoadChannelsAsync();
RestoreLastChannel();
_ = LoadEpgInBackgroundAsync();
}
+ private async Task InitializeWebViewAsync()
+ {
+ try
+ {
+ var profileDir = AppPaths.WebView2Profile;
+ Directory.CreateDirectory(profileDir);
+ var env = await CoreWebView2Environment.CreateAsync(null, profileDir);
+ await WebView.EnsureCoreWebView2Async(env);
+ _webViewReady = true;
+ }
+ catch (Exception ex)
+ {
+ // WebView2 nicht verfügbar — Web-Sender werden deaktiviert
+ System.Diagnostics.Debug.WriteLine($"WebView2 init failed: {ex.Message}");
+ }
+ }
+
/// Background-Thread: libVLC-Natives laden. Kein UI-Zugriff.
private void InitializePlayerNatives(int initialVolume)
{
@@ -172,7 +192,6 @@ public partial class MainWindow : Window
TxtStatus.Text = "Lade Senderliste…";
try
{
- // FritzBox + Online-Sender parallel laden
var fritzClient = new FritzBoxClient(_settings.FritzBoxIp);
var onlineClient = new OnlineSourceClient();
@@ -188,7 +207,6 @@ public partial class MainWindow : Window
}
catch (Exception ex)
{
- // FritzBox nicht erreichbar (z.B. unterwegs)? Trotzdem mit Online-Sendern weitermachen
fritzChannels = new List();
TxtStatus.Text = $"FritzBox-Fehler: {ex.Message}";
}
@@ -223,7 +241,6 @@ public partial class MainWindow : Window
private async Task LoadLogosInBackgroundAsync()
{
- // Parallel laden, aber begrenzt (max 6 gleichzeitig) damit AVM nicht zum DDoS wird
using var sem = new SemaphoreSlim(6);
var tasks = _allChannels
.Where(c => c.LogoPath == null)
@@ -233,7 +250,6 @@ public partial class MainWindow : Window
try
{
var path = await _logoService.GetLogoPathAsync(ch.Name);
- // Property-Change läuft über INotifyPropertyChanged → UI updated
await Dispatcher.InvokeAsync(() => ch.LogoPath = path);
}
finally { sem.Release(); }
@@ -278,36 +294,31 @@ public partial class MainWindow : Window
{
IEnumerable q = _allChannels;
- // Kategorie-Filter
q = _currentCategory switch
{
- "tv-fritz" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Source == ChannelSource.FritzBox)),
- "tv-online" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Source == ChannelSource.Online)),
+ "tv-fritz" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web && c.Source == ChannelSource.FritzBox)),
+ "tv-online" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web && c.Source == ChannelSource.Online)),
"radio-fritz" => q.Where(c => c.Kind == ChannelKind.Radio && c.Source == ChannelSource.FritzBox),
"radio-online" => q.Where(c => c.Kind == ChannelKind.Radio && c.Source == ChannelSource.Online),
+ "web" => q.Where(c => c.Kind == ChannelKind.Web),
"fav" => MergeHdSd(q.Where(c => c.IsFavorite)),
- _ => MergeHdSd(q) // "all"
+ _ => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Web)) // "all" ohne Web
};
if (!string.IsNullOrEmpty(_searchTerm))
q = q.Where(c => c.Name.Contains(_searchTerm, StringComparison.OrdinalIgnoreCase));
- // Alle Listen alphabetisch
q = q.OrderBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase);
_filteredChannels.Clear();
foreach (var c in q) _filteredChannels.Add(c);
}
- ///
- /// HD + SD zusammenfassen: gleicher Basisname → HD-Variante bevorzugen.
- /// Radio-Sender werden nicht angefasst.
- ///
private static IEnumerable MergeHdSd(IEnumerable source)
{
var list = source.ToList();
var radio = list.Where(c => c.Kind == ChannelKind.Radio);
- var tv = list.Where(c => c.Kind != ChannelKind.Radio);
+ var tv = list.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web);
var merged = tv
.GroupBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase)
@@ -316,9 +327,6 @@ public partial class MainWindow : Window
return merged.Concat(radio);
}
- ///
- /// Sendername normalisieren für Sort & Merge: " HD"-Suffix weg, klein, getrimmt.
- ///
private static string NormalizeName(string name)
{
var s = name.Trim();
@@ -335,7 +343,6 @@ public partial class MainWindow : Window
{
if (LstChannels.SelectedItem is Channel ch)
{
- // Debounce: schnelle ↑/↓-Navigation soll nicht jeden Sender starten
_pendingChannel = ch;
_zapTimer.Stop();
_zapTimer.Start();
@@ -344,17 +351,31 @@ public partial class MainWindow : Window
private void PlayChannel(Channel ch)
{
+ _currentChannel = ch;
+ TxtCurrentChannel.Text = ch.Name;
+ TxtChannelSource.Text = ch.Source == ChannelSource.Online ? "● Online" : "● FritzBox";
+ TxtChannelSource.Foreground = ch.Source == ChannelSource.Online
+ ? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x00, 0x78, 0xD4))
+ : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x66, 0xBB, 0x6A));
+ TxtNoChannel.Visibility = Visibility.Collapsed;
+ UpdateFavButton();
+
+ if (ch.Kind == ChannelKind.Web)
+ {
+ PlayWebChannel(ch);
+ return;
+ }
+
+ // VLC-Player aktiv, WebView verstecken
+ WebView.Visibility = Visibility.Collapsed;
+ VideoView.Visibility = Visibility.Visible;
+
if (_libVLC == null || _player == null) return;
try
{
- _currentChannel = ch;
-
- // Radio-Cover (zeigt Logo + Sendername statt weißes Bild)
UpdateRadioCover(ch);
- // KEIN Stop() — libVLC ersetzt Media bei laufendem Player ohne Frame-Lücke.
var oldMedia = _currentMedia;
-
var media = new Media(_libVLC, new Uri(ch.Url));
media.MetaChanged += OnMediaMetaChanged;
@@ -362,24 +383,14 @@ public partial class MainWindow : Window
_player.Media = media;
_player.Play();
- TxtCurrentChannel.Text = ch.Name;
- // Quelle-Badge anzeigen (FritzBox/Online)
- TxtChannelSource.Text = ch.Source == ChannelSource.Online ? "● Online" : "● FritzBox";
- TxtChannelSource.Foreground = ch.Source == ChannelSource.Online
- ? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x00, 0x78, 0xD4)) // Blau für Online
- : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x66, 0xBB, 0x6A)); // Grün für FritzBox
TxtEpgNow.Text = "EPG wird geladen…";
TxtEpgNext.Text = "";
- TxtNoChannel.Visibility = Visibility.Collapsed;
- UpdateFavButton();
_epgTimer.Stop();
_epgTimer.Tag = (DateTime.Now, ch);
_epgTimer.Start();
try { oldMedia?.Dispose(); } catch { }
-
- // "Danach"-Liste aus XMLTV-EPG vorbefüllen (überlebt bis EIT-Daten kommen)
UpdateNextFromEpgService(ch);
}
catch (Exception ex)
@@ -388,17 +399,42 @@ public partial class MainWindow : Window
}
}
- ///
- /// Füllt Jetzt+Danach aus dem XMLTV-EPG.
- ///
- /// Online-Streams (HLS) haben kein EIT, daher müssen wir hier auch das AKTUELLE
- /// Programm aus XMLTV setzen. FritzBox-TV nutzt primär EIT, XMLTV nur als "Danach"-Vorschau.
- ///
+ private void PlayWebChannel(Channel ch)
+ {
+ // VLC stoppen, WebView zeigen
+ _player?.Stop();
+ VideoView.Visibility = Visibility.Collapsed;
+ RadioCover.Visibility = Visibility.Collapsed;
+ WebView.Visibility = Visibility.Visible;
+
+ TxtEpgNow.Text = "🌐 Browser";
+ TxtEpgNext.Text = "";
+ TxtChannelSource.Text = "● Web";
+ TxtChannelSource.Foreground = new System.Windows.Media.SolidColorBrush(
+ System.Windows.Media.Color.FromRgb(0xFF, 0x99, 0x00)); // Orange für Web
+
+ if (_webViewReady)
+ {
+ WebView.Source = new Uri(ch.Url);
+ }
+ else
+ {
+ // WebView noch nicht bereit — kurz warten und retry
+ var retryTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
+ retryTimer.Tick += (_, _) =>
+ {
+ retryTimer.Stop();
+ if (_webViewReady && _currentChannel?.Kind == ChannelKind.Web)
+ WebView.Source = new Uri(ch.Url);
+ };
+ retryTimer.Start();
+ }
+ }
+
private void UpdateNextFromEpgService(Channel ch)
{
- if (ch.Kind == ChannelKind.Radio) return; // Radio: kein TV-EPG
+ if (ch.Kind == ChannelKind.Radio || ch.Kind == ChannelKind.Web) return;
- // EPG noch nicht geladen? Hintergrund-Load anstossen, kommt später wieder
if (!_epgService.IsCurrent)
{
_ = LoadEpgInBackgroundAsync();
@@ -407,41 +443,34 @@ public partial class MainWindow : Window
var allEvents = _epgService.GetEvents(ch.Name, hoursAhead: 12);
- // Aktuelle Sendung aus XMLTV: nur für Online-TV setzen (FritzBox: EIT überschreibt)
if (ch.Source == ChannelSource.Online)
{
var current = allEvents.FirstOrDefault(e => e.IsCurrent);
if (current != null)
{
- var jetzt = current.Title;
- if (!string.IsNullOrWhiteSpace(current.Description))
- jetzt += $" · {current.Description.Trim()}";
- TxtEpgNow.Text = $"▶ Jetzt: {jetzt}";
- _epgTimer.Stop(); // keine EIT-Updates erwarten
+ TxtEpgNow.Text = $"▶ Jetzt: {current.Title}";
+ _epgTimer.Stop();
}
else if (TxtEpgNow.Text == "EPG wird geladen\u2026")
{
- TxtEpgNow.Text = "(kein EPG verf\u00fcgbar)";
+ TxtEpgNow.Text = "(kein EPG verfügbar)";
_epgTimer.Stop();
}
}
+ else if (ch.Source == ChannelSource.FritzBox)
+ {
+ var current = allEvents.FirstOrDefault(e => e.IsCurrent);
+ if (current != null && (TxtEpgNow.Text == "EPG wird geladen\u2026" || TxtEpgNow.Text == "(kein EPG verfügbar)"))
+ TxtEpgNow.Text = $"▶ Jetzt: {current.Title}";
+ }
- // Danach-Liste (gleich für beide Quellen)
var next = allEvents.Where(e => e.StartTime > DateTime.Now).Take(3).ToList();
if (next.Count > 0)
- {
- TxtEpgNext.Text = string.Join(" · ",
- next.Select(e => $"{e.StartTime:HH:mm} {e.Title}"));
- }
+ TxtEpgNext.Text = string.Join(" · ", next.Select(e => $"{e.StartTime:HH:mm} {e.Title}"));
}
- ///
- /// Bei Radio-Sendern Cover-Bild anzeigen, bei TV verbergen.
- /// libVLC zeigt sonst bei reinen Audio-Streams einen weißen Default-Hintergrund.
- ///
private void UpdateRadioCover(Channel ch)
{
- // Bisher gebundener Channel abhängen damit wir nicht doppelt hören
if (_radioCoverBoundChannel != null)
_radioCoverBoundChannel.PropertyChanged -= OnCurrentChannelPropertyChanged;
_radioCoverBoundChannel = null;
@@ -452,8 +481,6 @@ public partial class MainWindow : Window
TxtRadioName.Text = ch.Name;
TxtRadioText.Text = "";
ApplyRadioLogo(ch);
-
- // Anhören falls Logo erst später (async) geladen wird
ch.PropertyChanged += OnCurrentChannelPropertyChanged;
_radioCoverBoundChannel = ch;
}
@@ -491,7 +518,7 @@ public partial class MainWindow : Window
RadioFallbackIcon.Visibility = Visibility.Collapsed;
return;
}
- catch { /* fallthrough zu Fallback-Icon */ }
+ catch { }
}
RadioLogo.Source = null;
RadioLogo.Visibility = Visibility.Collapsed;
@@ -506,20 +533,17 @@ public partial class MainWindow : Window
private void EpgTimer_Tick(object? sender, EventArgs e)
{
if (_currentMedia == null) { _epgTimer.Stop(); return; }
-
- // Online-Streams haben kein EIT — EpgService liefert Daten, EIT-Polling unnötig
- if (_currentChannel?.Source == ChannelSource.Online)
+ if (_currentChannel?.Source == ChannelSource.Online || _currentChannel?.Kind == ChannelKind.Web)
{
_epgTimer.Stop();
return;
}
-
if (_epgTimer.Tag is (DateTime started, Channel ch))
{
if ((DateTime.Now - started).TotalSeconds > 30)
{
_epgTimer.Stop();
- if (TxtEpgNow.Text == "EPG wird geladen…")
+ if (TxtEpgNow.Text == "EPG wird geladen\u2026")
TxtEpgNow.Text = "(kein EPG verfügbar)";
return;
}
@@ -531,19 +555,12 @@ public partial class MainWindow : Window
{
try
{
- // EIT-Daten (TV) oder RDS-Daten (Radio) aus dem Stream:
- // NowPlaying → TV: aktuelle Sendung; Radio: "Künstler - Titel"
- // ShowName → TV: manchmal Titel; Radio: meist Sendername
- // Description → TV: Untertitel
- // Title → Radio: Track-Titel (RDS RadioText+)
- // Artist → Radio: Künstler
var nowPlaying = m.Meta(MetadataType.NowPlaying) ?? "";
var showName = m.Meta(MetadataType.ShowName) ?? "";
var description = m.Meta(MetadataType.Description) ?? "";
var title = m.Meta(MetadataType.Title) ?? "";
var artist = m.Meta(MetadataType.Artist) ?? "";
- // URLs ausfiltern (libVLC packt manchmal die Stream-URL rein)
if (description.StartsWith("rtsp:", StringComparison.OrdinalIgnoreCase) ||
description.StartsWith("http:", StringComparison.OrdinalIgnoreCase))
description = "";
@@ -555,14 +572,6 @@ public partial class MainWindow : Window
if (isRadio)
{
- // Radio-RDS: verschiedene Sender befüllen unterschiedliche Felder.
- // Reihenfolge der Versuche:
- // 1. Artist + Title kombiniert (sauberster Fall)
- // 2. NowPlaying (oft "Künstler - Titel")
- // 3. Title alleine
- // 4. Artist alleine
- // 5. Description (manche Sender packen RDS-Text dort rein)
- // 6. ShowName (Fallback)
string nowText = "";
if (!string.IsNullOrWhiteSpace(artist) && !string.IsNullOrWhiteSpace(title))
nowText = $"{artist.Trim()} – {title.Trim()}";
@@ -587,7 +596,6 @@ public partial class MainWindow : Window
}
else
{
- // TV: EIT NowPlaying / ShowName
string nowText = !string.IsNullOrWhiteSpace(nowPlaying) ? nowPlaying : showName;
if (!string.IsNullOrWhiteSpace(nowText))
@@ -597,15 +605,12 @@ public partial class MainWindow : Window
jetzt += $" · {description.Trim()}";
TxtEpgNow.Text = $"▶ Jetzt: {jetzt}";
- // "Danach" lassen wir aus dem XMLTV-EPG (3 Sendungen) — das ist informativer
- // als die einzelne EIT-ShowName-Angabe. Nur fallback wenn EPG-Service leer ist.
if (string.IsNullOrEmpty(TxtEpgNext.Text))
{
var next = (!string.IsNullOrWhiteSpace(showName) && showName != nowPlaying)
? showName : "";
TxtEpgNext.Text = string.IsNullOrWhiteSpace(next) ? "" : $"⏭ Danach: {next.Trim()}";
}
-
_epgTimer.Stop();
}
}
@@ -696,7 +701,6 @@ public partial class MainWindow : Window
}
else
{
- // ItemTemplate-Refresh ohne Stream-Reset
if (LstChannels.ItemContainerGenerator.ContainerFromItem(_currentChannel)
is ListBoxItem lbi)
{
@@ -735,7 +739,7 @@ public partial class MainWindow : Window
{
if (_currentChannel == null)
{
- MessageBox.Show("Erst einen Sender auswählen.", "FRITZ!TV");
+ MessageBox.Show("Erst einen Sender auswählen.", "HomeStream");
return;
}
var w = new EpgChannelWindow(_epgService, _currentChannel.Name) { Owner = this };
@@ -754,12 +758,11 @@ public partial class MainWindow : Window
// ────────── EPG-Overlay (Joyn-Style) ──────────
- // Layout-Konstanten fürs Grid
- private const double EpgPxPerMin = 4.0; // 4 px/min → 240 px/h
- private const double EpgChannelColWidth = 200; // Senderspalte links (Logo + Name)
- private const double EpgRowHeight = 64; // jede Senderzeile
- private const double EpgHeaderHeight = 40; // Zeit-Header oben
- private const int EpgTotalHours = 8; // 8 Stunden sichtbar/scrollbar
+ private const double EpgPxPerMin = 4.0;
+ private const double EpgChannelColWidth = 200;
+ private const double EpgRowHeight = 64;
+ private const double EpgHeaderHeight = 40;
+ private const int EpgTotalHours = 8;
private DateTime _epgStartTime;
@@ -797,7 +800,7 @@ public partial class MainWindow : Window
EpgCanvas.Children.Clear();
var channels = _allChannels
- .Where(c => c.Kind != ChannelKind.Radio)
+ .Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web)
.GroupBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase)
.Select(g => g.FirstOrDefault(c => c.Kind == ChannelKind.TvHd) ?? g.First())
.OrderBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase)
@@ -814,7 +817,6 @@ public partial class MainWindow : Window
BuildEpgHeader(totalMinutes, contentWidth, contentHeight);
BuildEpgNowLine(totalMinutes, contentHeight);
- // Zeilen in Batches einf\u00fcgen damit UI-Thread nicht h\u00e4ngt (82 Sender w\u00e4ren >5s)
const int batchSize = 10;
for (int i = 0; i < channels.Count; i++)
{
@@ -830,28 +832,22 @@ public partial class MainWindow : Window
private void BuildEpgHeader(double totalMinutes, double contentWidth, double contentHeight)
{
- // Header-Hintergrund
var bg = new System.Windows.Shapes.Rectangle
{
- Width = contentWidth,
- Height = EpgHeaderHeight,
+ Width = contentWidth, Height = EpgHeaderHeight,
Fill = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x10, 0x10, 0x10))
};
Canvas.SetLeft(bg, 0); Canvas.SetTop(bg, 0);
EpgCanvas.Children.Add(bg);
- // Stunden-Marker + vertikale Trennlinien
for (int h = 0; h <= EpgTotalHours; h++)
{
var t = _epgStartTime.AddHours(h);
var x = EpgChannelColWidth + h * 60 * EpgPxPerMin;
-
var label = new TextBlock
{
- Text = t.ToString("HH:mm"),
- Foreground = System.Windows.Media.Brushes.White,
- FontSize = 13,
- FontWeight = FontWeights.SemiBold
+ Text = t.ToString("HH:mm"), Foreground = System.Windows.Media.Brushes.White,
+ FontSize = 13, FontWeight = FontWeights.SemiBold
};
Canvas.SetLeft(label, x + 6); Canvas.SetTop(label, 11);
EpgCanvas.Children.Add(label);
@@ -886,11 +882,9 @@ public partial class MainWindow : Window
{
var y = EpgHeaderHeight + rowIndex * EpgRowHeight;
- // Sender-Spalte links: Logo + Name
var nameCell = new Border
{
- Width = EpgChannelColWidth,
- Height = EpgRowHeight,
+ Width = EpgChannelColWidth, Height = EpgRowHeight,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x16, 0x16, 0x16)),
BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x22, 0x22, 0x22)),
BorderThickness = new Thickness(0, 0, 1, 1)
@@ -913,10 +907,8 @@ public partial class MainWindow : Window
}
var nameTb = new TextBlock
{
- Text = ch.Name,
- Foreground = System.Windows.Media.Brushes.White,
- FontSize = 12,
- FontWeight = FontWeights.SemiBold,
+ Text = ch.Name, Foreground = System.Windows.Media.Brushes.White,
+ FontSize = 12, FontWeight = FontWeights.SemiBold,
VerticalAlignment = VerticalAlignment.Center,
Padding = new Thickness(8, 0, 8, 0),
TextTrimming = TextTrimming.CharacterEllipsis
@@ -927,17 +919,14 @@ public partial class MainWindow : Window
Canvas.SetLeft(nameCell, 0); Canvas.SetTop(nameCell, y);
EpgCanvas.Children.Add(nameCell);
- // Zeilen-Hintergrund (rechts der Sendernamen)
var rowBg = new System.Windows.Shapes.Rectangle
{
- Width = contentWidth - EpgChannelColWidth,
- Height = EpgRowHeight,
+ Width = contentWidth - EpgChannelColWidth, Height = EpgRowHeight,
Fill = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x1A, 0x1A, 0x1A))
};
Canvas.SetLeft(rowBg, EpgChannelColWidth); Canvas.SetTop(rowBg, y);
EpgCanvas.Children.Add(rowBg);
- // Events für diesen Sender
var events = _epgService.GetEvents(ch.Name, hoursAhead: EpgTotalHours);
var totalMinutes = EpgTotalHours * 60;
foreach (var ev in events)
@@ -962,91 +951,63 @@ public partial class MainWindow : Window
{
var isCurrent = ev.IsCurrent;
var bgColor = isCurrent
- ? System.Windows.Media.Color.FromRgb(0x9E, 0x1B, 0x32) // Joyn-Rot für aktuell
+ ? System.Windows.Media.Color.FromRgb(0x9E, 0x1B, 0x32)
: System.Windows.Media.Color.FromRgb(0x2A, 0x2A, 0x2A);
var borderColor = isCurrent
? System.Windows.Media.Color.FromRgb(0xC9, 0x29, 0x42)
: System.Windows.Media.Color.FromRgb(0x44, 0x44, 0x44);
var content = new StackPanel { Margin = new Thickness(8, 6, 8, 6) };
- var timeLabel = new TextBlock
+ content.Children.Add(new TextBlock
{
Text = $"{ev.StartTime:HH:mm} – {ev.EndTime:HH:mm}",
Foreground = isCurrent ? System.Windows.Media.Brushes.White
- : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xAA, 0xAA, 0xAA)),
- FontSize = 10,
- TextTrimming = TextTrimming.CharacterEllipsis
- };
- var titleLabel = new TextBlock
+ : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xAA, 0xAA, 0xAA)),
+ FontSize = 10, TextTrimming = TextTrimming.CharacterEllipsis
+ });
+ content.Children.Add(new TextBlock
{
- Text = ev.Title,
- Foreground = System.Windows.Media.Brushes.White,
- FontSize = 12,
- FontWeight = FontWeights.SemiBold,
+ Text = ev.Title, Foreground = System.Windows.Media.Brushes.White,
+ FontSize = 12, FontWeight = FontWeights.SemiBold,
TextTrimming = TextTrimming.CharacterEllipsis,
- TextWrapping = TextWrapping.Wrap,
- MaxHeight = 36
- };
- content.Children.Add(timeLabel);
- content.Children.Add(titleLabel);
+ TextWrapping = TextWrapping.Wrap, MaxHeight = 36
+ });
var box = new Border
{
- Width = width,
- Height = EpgRowHeight - 4,
+ Width = width, Height = EpgRowHeight - 4,
Background = new System.Windows.Media.SolidColorBrush(bgColor),
BorderBrush = new System.Windows.Media.SolidColorBrush(borderColor),
- BorderThickness = new Thickness(1),
- CornerRadius = new CornerRadius(3),
+ BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(3),
Cursor = Cursors.Hand,
ToolTip = $"{ev.StartTime:HH:mm}–{ev.EndTime:HH:mm}\n{ev.Title}" +
(string.IsNullOrWhiteSpace(ev.Description) ? "" : $"\n\n{ev.Description}"),
- Tag = ch,
- Child = content
+ Tag = ch, Child = content
};
- // Hover-Effekt
- box.MouseEnter += (_, _) =>
- {
- var hoverColor = isCurrent
- ? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A)
- : System.Windows.Media.Color.FromRgb(0x36, 0x36, 0x36);
- box.Background = new System.Windows.Media.SolidColorBrush(hoverColor);
- };
- box.MouseLeave += (_, _) =>
- box.Background = new System.Windows.Media.SolidColorBrush(bgColor);
+ box.MouseEnter += (_, _) => box.Background = new System.Windows.Media.SolidColorBrush(
+ isCurrent ? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A)
+ : System.Windows.Media.Color.FromRgb(0x36, 0x36, 0x36));
+ box.MouseLeave += (_, _) => box.Background = new System.Windows.Media.SolidColorBrush(bgColor);
- // Click → Sender wechseln + Overlay schließen
box.MouseLeftButtonDown += (_, _) =>
{
- // Channel-Match: gleicher normalisierter Name in der Senderliste
var target = _allChannels.FirstOrDefault(c =>
- NormalizeName(c.Name).Equals(NormalizeName(ch.Name), StringComparison.OrdinalIgnoreCase))
- ?? ch;
-
+ NormalizeName(c.Name).Equals(NormalizeName(ch.Name), StringComparison.OrdinalIgnoreCase)) ?? ch;
HideEpgOverlay();
-
- // Falls Sender schon selektiert ist, feuert SelectionChanged nicht
- // → Direkt PlayChannel aufrufen. Sonst über Listbox damit Highlight passt.
var listEntry = _filteredChannels.FirstOrDefault(c =>
NormalizeName(c.Name).Equals(NormalizeName(ch.Name), StringComparison.OrdinalIgnoreCase));
if (listEntry != null && LstChannels.SelectedItem != listEntry)
- {
LstChannels.SelectedItem = listEntry;
- }
else
- {
- // Direkt abspielen (Sender war schon selektiert ODER nicht in der gefilterten Liste)
PlayChannel(target);
- }
};
return box;
}
- // ────────── Sidebar (zwei separate Toggles für Kategorien und Liste) ──────────
+ // ────────── Sidebar ──────────
- // Beide Bereiche unabhängig: Kategorien zu/auf, Liste zu/auf
private bool _categoriesCollapsed;
private bool _listCollapsed;
@@ -1062,9 +1023,6 @@ public partial class MainWindow : Window
ApplySidebarLayout();
}
- ///
- /// Strg+B / globaler Burger: öffnet beide wenn beide zu sind, sonst schließt beide.
- ///
private void BtnSidebarToggle_Click(object sender, RoutedEventArgs e)
{
if (_categoriesCollapsed && _listCollapsed)
@@ -1084,12 +1042,8 @@ public partial class MainWindow : Window
{
SidebarColumn.Width = _categoriesCollapsed ? new GridLength(0) : new GridLength(180);
ChannelsColumn.Width = _listCollapsed ? new GridLength(0) : new GridLength(280);
-
- // Burger-Toggle (im Player-Bereich) nur sichtbar wenn beide Spalten zu sind
BtnSidebarToggleOverlay.Visibility = (_categoriesCollapsed && _listCollapsed)
? Visibility.Visible : Visibility.Collapsed;
-
- // Internes Flag für Fullscreen-Restore
_sidebarHidden = _categoriesCollapsed && _listCollapsed;
}
@@ -1099,7 +1053,6 @@ public partial class MainWindow : Window
{
if (Keyboard.FocusedElement is TextBox) return;
- // Strg+B: Sidebar/Liste komplett toggle
if (e.Key == Key.B && Keyboard.Modifiers == ModifierKeys.Control)
{
BtnSidebarToggle_Click(this, new RoutedEventArgs());
@@ -1127,4 +1080,3 @@ public partial class MainWindow : Window
}
}
}
-
diff --git a/Models/Channel.cs b/Models/Channel.cs
index 3672462..65fa21e 100644
--- a/Models/Channel.cs
+++ b/Models/Channel.cs
@@ -3,7 +3,7 @@ using System.Runtime.CompilerServices;
namespace FritzTV.Models;
-public enum ChannelKind { TvSd, TvHd, Radio }
+public enum ChannelKind { TvSd, TvHd, Radio, Web }
/// Woher kommt der Sender? FritzBox (DVB-C/lokal) oder Online (HLS-Stream)
public enum ChannelSource { FritzBox, Online }
diff --git a/Services/AppPaths.cs b/Services/AppPaths.cs
index 958ea5c..6e55dc1 100644
--- a/Services/AppPaths.cs
+++ b/Services/AppPaths.cs
@@ -35,4 +35,5 @@ public static class AppPaths
public static string Logos => Path.Combine(Root, "logos");
public static string Epg => Path.Combine(Root, "epg");
public static string CrashLog => Path.Combine(Root, "crash.log");
+ public static string WebView2Profile => Path.Combine(Root, "webview2");
}
diff --git a/Services/OnlineSourceClient.cs b/Services/OnlineSourceClient.cs
index 7b265fb..f496c81 100644
--- a/Services/OnlineSourceClient.cs
+++ b/Services/OnlineSourceClient.cs
@@ -68,6 +68,25 @@ public class OnlineSourceClient
}
}
+ if (root.TryGetProperty("web", out var webArr))
+ {
+ foreach (var item in webArr.EnumerateArray())
+ {
+ var name = item.GetProperty("name").GetString();
+ var url = item.GetProperty("url").GetString();
+ if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url)) continue;
+
+ channels.Add(new Channel
+ {
+ Name = name,
+ Url = url,
+ Kind = ChannelKind.Web,
+ Number = number++,
+ Source = ChannelSource.Online
+ });
+ }
+ }
+
return Task.FromResult(channels);
}
catch