diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts
index c2cc0759e..1f5607745 100644
--- a/apps/electron-backend/src/app/api/main.preload.ts
+++ b/apps/electron-backend/src/app/api/main.preload.ts
@@ -5,7 +5,10 @@ contextBridge.exposeInMainWorld('electron', {
onChannelChange: (
callback: (data: { direction: 'up' | 'down' }) => void
) => {
- ipcRenderer.on('CHANNEL_CHANGE', (_event, data) => callback(data));
+ const handler = (_event: Electron.IpcRendererEvent, data: any) =>
+ callback(data);
+ ipcRenderer.on('CHANNEL_CHANGE', handler);
+ return () => ipcRenderer.off('CHANNEL_CHANGE', handler);
},
// Player error listener
onPlayerError: (
diff --git a/apps/remote-control-web/README.md b/apps/remote-control-web/README.md
index 9137bddae..2e7c1ff8f 100644
--- a/apps/remote-control-web/README.md
+++ b/apps/remote-control-web/README.md
@@ -1,6 +1,6 @@
# Remote Control Web App
-A lightweight, mobile-optimized web application that provides remote control functionality for IPTVnator. Control channel switching from your phone or tablet on the same network.
+A lightweight, mobile-optimized web application that provides remote control functionality for IPTVnator. Control live channel switching from your phone or tablet on the same network.
## Overview
@@ -11,6 +11,7 @@ This is a standalone Angular application designed to be served by IPTVnator's El
- **Apple TV-Inspired Design** - Premium dark theme with glass morphism effects
- **Mobile-Optimized** - Touch-friendly interface designed for phones and tablets
- **Real-time Control** - Instant channel switching via REST API
+- **Multi-Portal Support** - Works with M3U playlists, Xtream Live TV, and Stalker ITV sections
- **Minimal & Fast** - Lightweight app (~55 KB compressed) with no external dependencies
- **Responsive** - Adapts to different screen sizes (320px - 480px+)
@@ -35,7 +36,7 @@ This is a standalone Angular application designed to be served by IPTVnator's El
2. **Static Files**: Serves this Angular app's built files
3. **REST API**: Exposes `/api/remote-control/channel/up` and `/api/remote-control/channel/down` endpoints
4. **IPC Communication**: HTTP server sends events to the main Electron window
-5. **Channel Switching**: Main app receives events and switches to next/previous channel
+5. **Channel Switching**: Main app receives events and switches to next/previous live channel in the active live view
### API Endpoints
@@ -166,7 +167,9 @@ Potential improvements:
- **HTTP Server**: `apps/electron-backend/src/app/server/http-server.ts`
- **Remote Control Events**: `apps/electron-backend/src/app/events/remote-control.events.ts`
-- **Main App Handler**: `apps/web/src/app/home/video-player/video-player.component.ts` (handleRemoteChannelChange method)
+- **M3U Handler**: `apps/web/src/app/home/video-player/video-player.component.ts` (handleRemoteChannelChange method)
+- **Xtream Live Handler**: `apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts`
+- **Stalker ITV Handler**: `apps/web/src/app/stalker/stalker-live-stream-layout/stalker-live-stream-layout.component.ts`
- **Settings UI**: `apps/web/src/app/settings/settings.component.html`
## License
diff --git a/apps/web/src/app/home/video-player/video-player.component.ts b/apps/web/src/app/home/video-player/video-player.component.ts
index cc8f40a3c..378bbb116 100644
--- a/apps/web/src/app/home/video-player/video-player.component.ts
+++ b/apps/web/src/app/home/video-player/video-player.component.ts
@@ -5,6 +5,7 @@ import {
Component,
HostListener,
Injector,
+ OnDestroy,
OnInit,
effect,
inject,
@@ -53,6 +54,7 @@ import {
VideoPlayer,
} from 'shared-interfaces';
import { SettingsStore } from '../../services/settings-store.service';
+import { getAdjacentChannelItem } from '../../shared/services/remote-channel-navigation.util';
import { SettingsComponent } from '../../settings/settings.component';
@Component({
@@ -74,7 +76,7 @@ import { SettingsComponent } from '../../settings/settings.component';
templateUrl: './video-player.component.html',
styleUrl: './video-player.component.scss',
})
-export class VideoPlayerComponent implements OnInit {
+export class VideoPlayerComponent implements OnInit, OnDestroy {
private readonly activatedRoute = inject(ActivatedRoute);
private readonly dataService = inject(DataService);
private readonly dialog = inject(MatDialog);
@@ -108,6 +110,7 @@ export class VideoPlayerComponent implements OnInit {
/** EPG overlay reference */
private overlayRef!: OverlayRef;
+ private unsubscribeRemoteChannelChange?: () => void;
/** Info overlay component reference for manual triggering */
readonly infoOverlay = viewChild(InfoOverlayComponent);
@@ -144,11 +147,14 @@ export class VideoPlayerComponent implements OnInit {
// Setup remote control channel change listener (Electron only)
if (this.isDesktop && window.electron?.onChannelChange) {
- window.electron.onChannelChange(
- (data: { direction: 'up' | 'down' }) => {
- this.handleRemoteChannelChange(data.direction);
- }
- );
+ const unsubscribe = window.electron.onChannelChange((data: {
+ direction: 'up' | 'down';
+ }) => {
+ this.handleRemoteChannelChange(data.direction);
+ });
+ if (typeof unsubscribe === 'function') {
+ this.unsubscribeRemoteChannelChange = unsubscribe;
+ }
}
this.channels$ = this.activatedRoute.params.pipe(
@@ -229,33 +235,17 @@ export class VideoPlayerComponent implements OnInit {
)
.subscribe({
next: ({ channels, activeChannel }) => {
- // Find current channel index
- const currentIndex = channels.findIndex(
- (ch) => ch.url === activeChannel.url
+ const nextChannel = getAdjacentChannelItem(
+ channels,
+ activeChannel.url,
+ direction,
+ (channel) => channel.url
);
- if (currentIndex === -1) {
+ if (!nextChannel) {
return;
}
- // Calculate next/previous index with wraparound
- let nextIndex: number;
- if (direction === 'up') {
- // Up = previous channel (decrease index)
- nextIndex =
- currentIndex - 1 < 0
- ? channels.length - 1
- : currentIndex - 1;
- } else {
- // Down = next channel (increase index)
- nextIndex =
- currentIndex + 1 >= channels.length
- ? 0
- : currentIndex + 1;
- }
-
- // Dispatch action to change channel
- const nextChannel = channels[nextIndex];
this.store.dispatch(
ChannelActions.setActiveChannel({
channel: nextChannel,
@@ -268,6 +258,10 @@ export class VideoPlayerComponent implements OnInit {
});
}
+ ngOnDestroy(): void {
+ this.unsubscribeRemoteChannelChange?.();
+ }
+
/**
* Opens a playlist provided as a url param
* e.g. iptvnat.or?url=http://...
diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html
index e5d0cb27c..892d88783 100644
--- a/apps/web/src/app/settings/settings.component.html
+++ b/apps/web/src/app/settings/settings.component.html
@@ -282,7 +282,7 @@
{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}
diff --git a/apps/web/src/app/shared/services/remote-channel-navigation.util.ts b/apps/web/src/app/shared/services/remote-channel-navigation.util.ts new file mode 100644 index 000000000..d5f57eaed --- /dev/null +++ b/apps/web/src/app/shared/services/remote-channel-navigation.util.ts @@ -0,0 +1,34 @@ +export type RemoteChannelDirection = 'up' | 'down'; + +/** + * Returns the adjacent item in the list for remote channel navigation. + * Uses wraparound when reaching the start or end of the list. + */ +export function getAdjacentChannelItem