mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
refactor(remote-control): gate live bridge by capability
This commit is contained in:
1 parent
8cfc40f59b
commit
3e398f1d1a
7 files changed
+99
-22
No files matched your search
@@ -97,6 +97,13 @@ Type definitions:
|
||||
|
||||
## Renderer Integrations
|
||||
|
||||
All renderer integrations resolve the Electron remote-control bridge through
|
||||
`RuntimeCapabilitiesService.supportsRemoteControl`. A partial Electron bridge
|
||||
is treated as unsupported unless it exposes all remote-control methods:
|
||||
`updateRemoteControlStatus`, `onChannelChange`, and
|
||||
`onRemoteControlCommand`. This keeps PWA/self-hosted builds and partial test
|
||||
bridges from accidentally activating desktop-only remote-control behavior.
|
||||
|
||||
## Shared helpers
|
||||
|
||||
- File: `libs/portal/shared/util/src/lib/remote-channel-navigation.ts`
|
||||
|
||||
@@ -286,6 +286,9 @@ describe('VideoPlayerComponent', () => {
|
||||
beforeEach(async () => {
|
||||
window.electron = {
|
||||
platform: 'darwin',
|
||||
updateRemoteControlStatus: jest.fn(),
|
||||
onChannelChange: jest.fn(() => jest.fn()),
|
||||
onRemoteControlCommand: jest.fn(() => jest.fn()),
|
||||
} as typeof window.electron;
|
||||
|
||||
syncStoreState(null);
|
||||
@@ -464,6 +467,21 @@ describe('VideoPlayerComponent', () => {
|
||||
expect(headerContext.action()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not publish remote-control status when the bridge is incomplete', () => {
|
||||
fixture.destroy();
|
||||
const updateRemoteControlStatus = jest.fn();
|
||||
window.electron = {
|
||||
updateRemoteControlStatus,
|
||||
} as typeof window.electron;
|
||||
|
||||
fixture = TestBed.createComponent(VideoPlayerComponent);
|
||||
component = fixture.componentInstance;
|
||||
syncStoreState(sampleChannel);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(updateRemoteControlStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens MPV fallback with the active channel headers preserved', () => {
|
||||
syncStoreState({
|
||||
...sampleChannel,
|
||||
|
||||
@@ -383,8 +383,9 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
this.registerHeaderShortcut();
|
||||
|
||||
// Setup remote control channel change listener (Electron only)
|
||||
if (this.isDesktop && window.electron?.onChannelChange) {
|
||||
const unsubscribe = window.electron.onChannelChange(
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (remoteControl?.onChannelChange) {
|
||||
const unsubscribe = remoteControl.onChannelChange(
|
||||
(data: { direction: 'up' | 'down' }) => {
|
||||
this.handleRemoteChannelChange(data.direction);
|
||||
}
|
||||
@@ -393,8 +394,8 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
this.unsubscribeRemoteChannelChange = unsubscribe;
|
||||
}
|
||||
}
|
||||
if (this.isDesktop && window.electron?.onRemoteControlCommand) {
|
||||
const unsubscribe = window.electron.onRemoteControlCommand(
|
||||
if (remoteControl?.onRemoteControlCommand) {
|
||||
const unsubscribe = remoteControl.onRemoteControlCommand(
|
||||
(command) => {
|
||||
this.handleRemoteControlCommand(command);
|
||||
}
|
||||
@@ -409,7 +410,8 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
this.store.select(selectActive),
|
||||
this.store.select(selectCurrentEpgProgram).pipe(startWith(null)),
|
||||
]).subscribe(([channels, activeChannel, epgProgram]) => {
|
||||
if (!window.electron?.updateRemoteControlStatus || !activeChannel) {
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (!remoteControl?.updateRemoteControlStatus || !activeChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -421,7 +423,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
(channel) => channel.url === activeChannel.url
|
||||
);
|
||||
|
||||
window.electron.updateRemoteControlStatus({
|
||||
remoteControl.updateRemoteControlStatus({
|
||||
portal: 'm3u',
|
||||
isLiveView: true,
|
||||
channelName: activeChannel.name ?? activeChannel.tvg?.name,
|
||||
@@ -805,8 +807,9 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
localStorage.setItem('volume', String(clamped));
|
||||
|
||||
if (window.electron?.updateRemoteControlStatus) {
|
||||
window.electron.updateRemoteControlStatus({
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (remoteControl?.updateRemoteControlStatus) {
|
||||
remoteControl.updateRemoteControlStatus({
|
||||
portal: 'm3u',
|
||||
isLiveView: true,
|
||||
supportsVolume: true,
|
||||
@@ -816,6 +819,10 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private get remoteControlBridge(): Window['electron'] | undefined {
|
||||
return this.runtime.supportsRemoteControl ? window.electron : undefined;
|
||||
}
|
||||
|
||||
shouldShowInlinePlayer(channel: Channel | null | undefined): boolean {
|
||||
if (!channel) {
|
||||
return false;
|
||||
|
||||
+17
@@ -243,6 +243,9 @@ describe('StalkerLiveStreamLayoutComponent', () => {
|
||||
beforeEach(async () => {
|
||||
window.electron = {
|
||||
platform: 'darwin',
|
||||
updateRemoteControlStatus: jest.fn(),
|
||||
onChannelChange: jest.fn(() => jest.fn()),
|
||||
onRemoteControlCommand: jest.fn(() => jest.fn()),
|
||||
} as typeof window.electron;
|
||||
|
||||
fetchChannelEpg = stalkerStore.fetchChannelEpg;
|
||||
@@ -505,6 +508,20 @@ describe('StalkerLiveStreamLayoutComponent', () => {
|
||||
expect(finallySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not publish remote-control status when the bridge is incomplete', () => {
|
||||
fixture.destroy();
|
||||
const updateRemoteControlStatus = jest.fn();
|
||||
window.electron = {
|
||||
updateRemoteControlStatus,
|
||||
} as typeof window.electron;
|
||||
|
||||
fixture = TestBed.createComponent(StalkerLiveStreamLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(updateRemoteControlStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('starts external playback from remote channel navigation when double-click opening is enabled', async () => {
|
||||
settingsStore.openStreamOnDoubleClick.set(true);
|
||||
portalPlayer.isEmbeddedPlayer.mockReturnValue(false);
|
||||
|
||||
+13
-7
@@ -335,7 +335,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!window.electron?.updateRemoteControlStatus) {
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (!remoteControl?.updateRemoteControlStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -344,7 +345,7 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
const channels = this.visibleChannels();
|
||||
|
||||
if (selectedType !== 'itv' || !selectedItem?.id) {
|
||||
window.electron.updateRemoteControlStatus({
|
||||
remoteControl.updateRemoteControlStatus({
|
||||
portal: 'stalker',
|
||||
isLiveView: false,
|
||||
supportsVolume: false,
|
||||
@@ -357,7 +358,7 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
);
|
||||
const currentProgram = this.currentProgram();
|
||||
|
||||
window.electron.updateRemoteControlStatus({
|
||||
remoteControl.updateRemoteControlStatus({
|
||||
portal: 'stalker',
|
||||
isLiveView: true,
|
||||
channelName: selectedItem.o_name || selectedItem.name,
|
||||
@@ -369,8 +370,9 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
if (window.electron?.onChannelChange) {
|
||||
const unsubscribe = window.electron.onChannelChange(
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (remoteControl?.onChannelChange) {
|
||||
const unsubscribe = remoteControl.onChannelChange(
|
||||
(data: { direction: 'up' | 'down' }) => {
|
||||
this.handleRemoteChannelChange(data.direction);
|
||||
}
|
||||
@@ -379,8 +381,8 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
this.unsubscribeRemoteChannelChange = unsubscribe;
|
||||
}
|
||||
}
|
||||
if (window.electron?.onRemoteControlCommand) {
|
||||
const unsubscribe = window.electron.onRemoteControlCommand(
|
||||
if (remoteControl?.onRemoteControlCommand) {
|
||||
const unsubscribe = remoteControl.onRemoteControlCommand(
|
||||
(command) => {
|
||||
this.handleRemoteControlCommand(command);
|
||||
}
|
||||
@@ -799,6 +801,10 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
private get remoteControlBridge(): Window['electron'] | undefined {
|
||||
return this.runtime.supportsRemoteControl ? window.electron : undefined;
|
||||
}
|
||||
|
||||
private handleRemoteChannelChange(direction: 'up' | 'down'): void {
|
||||
this.handleAdjacentChannelChange(direction);
|
||||
}
|
||||
|
||||
+16
@@ -182,6 +182,8 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
|
||||
window.electron = {
|
||||
updateRemoteControlStatus: jest.fn(),
|
||||
onChannelChange: jest.fn(() => jest.fn()),
|
||||
onRemoteControlCommand: jest.fn(() => jest.fn()),
|
||||
} as typeof window.electron;
|
||||
|
||||
xtreamStore.constructStreamUrl.mockClear();
|
||||
@@ -487,6 +489,20 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not publish remote-control status when the bridge is incomplete', () => {
|
||||
fixture.destroy();
|
||||
const updateRemoteControlStatus = jest.fn();
|
||||
window.electron = {
|
||||
updateRemoteControlStatus,
|
||||
} as typeof window.electron;
|
||||
|
||||
fixture = TestBed.createComponent(LiveStreamLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(updateRemoteControlStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves a catchup url for archived program activation', async () => {
|
||||
portalPlayer.isEmbeddedPlayer.mockReturnValue(false);
|
||||
|
||||
|
||||
+13
-7
@@ -267,7 +267,8 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (!window.electron?.updateRemoteControlStatus) {
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (!remoteControl?.updateRemoteControlStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,7 +278,7 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
const currentProgram = this.currentEpgItem();
|
||||
|
||||
if (selectedContentType !== 'live' || !selectedItem?.xtream_id) {
|
||||
window.electron.updateRemoteControlStatus({
|
||||
remoteControl.updateRemoteControlStatus({
|
||||
portal: 'xtream',
|
||||
isLiveView: false,
|
||||
supportsVolume: false,
|
||||
@@ -290,7 +291,7 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
Number(item.xtream_id) === Number(selectedItem.xtream_id)
|
||||
);
|
||||
|
||||
window.electron.updateRemoteControlStatus({
|
||||
remoteControl.updateRemoteControlStatus({
|
||||
portal: 'xtream',
|
||||
isLiveView: true,
|
||||
channelName: selectedItem.title ?? selectedItem.name,
|
||||
@@ -304,8 +305,9 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (window.electron?.onChannelChange) {
|
||||
const unsubscribe = window.electron.onChannelChange(
|
||||
const remoteControl = this.remoteControlBridge;
|
||||
if (remoteControl?.onChannelChange) {
|
||||
const unsubscribe = remoteControl.onChannelChange(
|
||||
(data: { direction: 'up' | 'down' }) => {
|
||||
this.handleRemoteChannelChange(data.direction);
|
||||
}
|
||||
@@ -314,8 +316,8 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
this.unsubscribeRemoteChannelChange = unsubscribe;
|
||||
}
|
||||
}
|
||||
if (window.electron?.onRemoteControlCommand) {
|
||||
const unsubscribe = window.electron.onRemoteControlCommand(
|
||||
if (remoteControl?.onRemoteControlCommand) {
|
||||
const unsubscribe = remoteControl.onRemoteControlCommand(
|
||||
(command) => {
|
||||
this.handleRemoteControlCommand(command);
|
||||
}
|
||||
@@ -556,6 +558,10 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
private get remoteControlBridge(): Window['electron'] | undefined {
|
||||
return this.runtime.supportsRemoteControl ? window.electron : undefined;
|
||||
}
|
||||
|
||||
private getProgramTimestampSeconds(
|
||||
dateValue: string,
|
||||
unixTimestampValue?: number | string | null
|
||||
|
||||
Reference in new issue
Block a user