mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
fix(playback): keep asking for Embedded MPV support while the answer is inconclusive
Keeping the saved player on an inconclusive answer left a mounted player stuck on it: the session controller asked for support once, in its constructor, and the session effect never starts while unsupported, so the player did not recover after the login shell answered. The settings page held its one answer the same way. `watchEmbeddedMpvSupport()` asks again every 3 s until the answer is final or the surface is destroyed. The player controller and the settings page facade load support through it, so playback starts by itself and the Embedded MPV option appears without reopening the page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
417eaf5005
commit
d143fb85f5
8 files changed
+407
-33
No files matched your search
+53
@@ -1,5 +1,6 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS,
|
||||
EmbeddedMpvEngine,
|
||||
EmbeddedMpvSession,
|
||||
ResolvedPortalPlayback,
|
||||
@@ -106,6 +107,58 @@ describe('EmbeddedMpvSessionController (lifecycle & support edges)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('an inconclusive support answer', () => {
|
||||
// A slow login shell: mpv was looked up before its PATH arrived.
|
||||
const inconclusive = {
|
||||
supported: false,
|
||||
platform: 'linux',
|
||||
reason: 'mpv executable missing',
|
||||
inconclusive: true,
|
||||
};
|
||||
|
||||
it('is asked for again, so a mounted player recovers by itself', async () => {
|
||||
const settled = createSupport('native');
|
||||
electron.getEmbeddedMpvSupport
|
||||
.mockResolvedValueOnce(inconclusive)
|
||||
.mockResolvedValue(settled);
|
||||
const controller = TestBed.inject(EmbeddedMpvSessionController);
|
||||
|
||||
await waitFor(
|
||||
() => controller.support() !== null,
|
||||
'the first support answer'
|
||||
);
|
||||
expect(controller.support()).toBe(inconclusive);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS
|
||||
);
|
||||
expect(controller.support()).toBe(settled);
|
||||
expect(electron.getEmbeddedMpvSupport).toHaveBeenCalledTimes(2);
|
||||
|
||||
// A final answer is kept.
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS * 3
|
||||
);
|
||||
expect(electron.getEmbeddedMpvSupport).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('is no longer asked for once the player is gone', async () => {
|
||||
electron.getEmbeddedMpvSupport.mockResolvedValue(inconclusive);
|
||||
const controller = TestBed.inject(EmbeddedMpvSessionController);
|
||||
await waitFor(
|
||||
() => controller.support() !== null,
|
||||
'the first support answer'
|
||||
);
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
EMBEDDED_MPV_SUPPORT_RECHECK_MS * 3
|
||||
);
|
||||
|
||||
expect(electron.getEmbeddedMpvSupport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves the constructor support probe after preparing a session', async () => {
|
||||
const probedSupport = createSupport('native');
|
||||
const preparedSupport = createSupport('frame-copy');
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
EmbeddedMpvSupport,
|
||||
RecordingStartMetadata,
|
||||
ResolvedPortalPlayback,
|
||||
watchEmbeddedMpvSupport,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { EmbeddedMpvCommandRunner } from './embedded-mpv-command-runner';
|
||||
import { measureBounds } from './embedded-mpv-format.utils';
|
||||
@@ -95,8 +96,9 @@ export class EmbeddedMpvSessionController {
|
||||
this.session.set(session);
|
||||
});
|
||||
|
||||
let stopSupportWatch: (() => void) | undefined;
|
||||
if (typeof window.electron?.getEmbeddedMpvSupport === 'function') {
|
||||
void this.loadSupport();
|
||||
stopSupportWatch = this.watchSupport();
|
||||
} else {
|
||||
this.support.set({
|
||||
supported: false,
|
||||
@@ -114,6 +116,7 @@ export class EmbeddedMpvSessionController {
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.unsubscribeSessionUpdate?.();
|
||||
stopSupportWatch?.();
|
||||
this.stalledTracker.cancel();
|
||||
if (this.boundsAnimationFrame !== null) {
|
||||
cancelAnimationFrame(this.boundsAnimationFrame);
|
||||
@@ -381,22 +384,31 @@ export class EmbeddedMpvSessionController {
|
||||
EmbeddedMpvSession['recording'] | null
|
||||
> => this.commands.stopRecording();
|
||||
|
||||
private async loadSupport(): Promise<void> {
|
||||
try {
|
||||
const electron = this.getElectronBridge();
|
||||
if (!electron?.getEmbeddedMpvSupport) {
|
||||
throw new Error(
|
||||
'Embedded MPV requires the Electron desktop build.'
|
||||
);
|
||||
}
|
||||
this.support.set(await electron.getEmbeddedMpvSupport());
|
||||
} catch (error) {
|
||||
this.support.set({
|
||||
supported: false,
|
||||
platform: window.electron?.platform ?? 'unknown',
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Loads support and keeps asking while the answer is inconclusive: the
|
||||
* session effect starts playback as soon as `support` turns supported,
|
||||
* so a player mounted during that window recovers by itself.
|
||||
*/
|
||||
private watchSupport(): () => void {
|
||||
return watchEmbeddedMpvSupport(
|
||||
() => {
|
||||
const electron = this.getElectronBridge();
|
||||
if (!electron?.getEmbeddedMpvSupport) {
|
||||
throw new Error(
|
||||
'Embedded MPV requires the Electron desktop build.'
|
||||
);
|
||||
}
|
||||
return electron.getEmbeddedMpvSupport();
|
||||
},
|
||||
(support) => this.support.set(support),
|
||||
(error) =>
|
||||
this.support.set({
|
||||
supported: false,
|
||||
platform: window.electron?.platform ?? 'unknown',
|
||||
reason:
|
||||
error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getElectronBridge(): ElectronBridge | undefined {
|
||||
|
||||
Reference in new issue
Block a user