mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
fix(xtream): try advertised TS after initial web HLS HTTP failure (#1558)
* fix(xtream): try advertised TS after initial web HLS HTTP failure * refactor(playback): extract fullscreen channel panel state * test(xtream): keep synthetic media within the mock project
This commit is contained in:
1 parent
ef2418a2c5
commit
436825bdec
37 files changed
+1457
-67
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
closeElectronApp,
|
||||
expect,
|
||||
launchElectronApp,
|
||||
openSources,
|
||||
openSourceEditor,
|
||||
updateSourceDialog,
|
||||
saveSourceDialog,
|
||||
sourceRowByTitle,
|
||||
test,
|
||||
} from './electron-test-fixtures';
|
||||
import {
|
||||
addLiveFormatPortal,
|
||||
configureLiveFormat,
|
||||
expectLiveFormatPlaying,
|
||||
liveChannels,
|
||||
observeLiveFormatMedia,
|
||||
} from './xtream-live-format.fixture';
|
||||
|
||||
for (const player of ['html5', 'artplayer'] as const) {
|
||||
test(`@electron @xtream Auto live format ${player}: failing segment to playable TS`, async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const app = await launchElectronApp(dataDir, {
|
||||
args: ['--autoplay-policy=no-user-gesture-required'],
|
||||
});
|
||||
try {
|
||||
const page = app.mainWindow;
|
||||
const media = observeLiveFormatMedia(page);
|
||||
await configureLiveFormat(page, player);
|
||||
await addLiveFormatPortal(page);
|
||||
await openSources(page);
|
||||
const dialog = await openSourceEditor(page, 'Synthetic live');
|
||||
await updateSourceDialog(dialog, {
|
||||
userAgent: 'IPTVnator-1513-fixture',
|
||||
});
|
||||
await saveSourceDialog(page, dialog);
|
||||
await sourceRowByTitle(page, 'Synthetic live').first().click();
|
||||
await page
|
||||
.getByRole('link', { name: 'Live TV', exact: true })
|
||||
.click();
|
||||
await page.locator('.context-panel .category-item').first().click();
|
||||
const [tsResponse] = await Promise.all([
|
||||
page.waitForResponse(
|
||||
(response) => response.url().endsWith('/10000.ts'),
|
||||
{ timeout: 60_000 }
|
||||
),
|
||||
liveChannels(page).first().click(),
|
||||
expectLiveFormatPlaying(page, player),
|
||||
]);
|
||||
expect(await tsResponse.headerValue('x-fixture-user-agent')).toBe(
|
||||
'IPTVnator-1513-fixture'
|
||||
);
|
||||
|
||||
expect(media).toContainEqual({ file: '10000.m3u8', status: 200 });
|
||||
expect(media).toContainEqual({
|
||||
file: 'denied-segment.ts',
|
||||
status: 403,
|
||||
});
|
||||
expect(media.filter((r) => r.file === '10000.ts')).toEqual([
|
||||
{ file: '10000.ts', status: 200 },
|
||||
]);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('@electron @xtream Video.js supports the manual TS workaround', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const app = await launchElectronApp(dataDir, {
|
||||
args: ['--autoplay-policy=no-user-gesture-required'],
|
||||
});
|
||||
try {
|
||||
const page = app.mainWindow;
|
||||
const media = observeLiveFormatMedia(page);
|
||||
await configureLiveFormat(page, 'videojs', 'ts');
|
||||
await addLiveFormatPortal(page);
|
||||
await liveChannels(page).first().click();
|
||||
await expectLiveFormatPlaying(page, 'videojs');
|
||||
expect(media.some((r) => r.file.endsWith('.m3u8'))).toBe(false);
|
||||
expect(media.filter((r) => r.file === '10000.ts')).toHaveLength(1);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export const liveFormatMock = `http://localhost:${process.env['XTREAM_MOCK_PORT'] ?? '3211'}`;
|
||||
export type LiveFormatPlayer = 'html5' | 'videojs' | 'artplayer';
|
||||
|
||||
export async function configureLiveFormat(
|
||||
page: Page,
|
||||
player: LiveFormatPlayer,
|
||||
format = 'auto'
|
||||
) {
|
||||
await page.getByRole('link', { name: 'Open settings' }).click();
|
||||
await page.locator('[data-test-id="settings-section-playback"]').click();
|
||||
await page.locator('[data-test-id="select-video-player"]').click();
|
||||
await page
|
||||
.getByRole('option', {
|
||||
name: {
|
||||
html5: 'HTML5 video player',
|
||||
videojs: 'Video.js player',
|
||||
artplayer: 'ArtPlayer',
|
||||
}[player],
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
await page.locator('[data-test-id="select-stream-format"]').click();
|
||||
await page.getByRole('option', { name: format, exact: true }).click();
|
||||
if (player !== 'videojs' || format !== 'auto') {
|
||||
const save = page.getByRole('button', { name: 'Save changes' });
|
||||
await expect(save).toBeVisible();
|
||||
await save.click();
|
||||
await expect(save).toBeHidden();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Back', exact: true }).click();
|
||||
}
|
||||
|
||||
export async function addLiveFormatPortal(page: Page) {
|
||||
await page.getByRole('button', { name: 'Add playlist' }).click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByRole('radio', { name: /Xtream credentials/i }).click();
|
||||
await dialog.locator('#title').fill('Synthetic live');
|
||||
await dialog.locator('#serverUrl').fill(liveFormatMock);
|
||||
await dialog.locator('#username').fill('live-fallback');
|
||||
await dialog.locator('#password').fill('live-fallback');
|
||||
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
await page.waitForURL(/xtreams.*vod/);
|
||||
await page.getByRole('link', { name: 'Live TV', exact: true }).click();
|
||||
await page.locator('.context-panel .category-item').first().click();
|
||||
await expect(liveChannels(page).first()).toBeVisible();
|
||||
}
|
||||
|
||||
export const liveChannels = (page: Page) =>
|
||||
page.locator('app-live-stream-layout [data-test-id="channel-item"]');
|
||||
export function observeLiveFormatMedia(page: Page) {
|
||||
const media: { file: string; status: number }[] = [];
|
||||
page.on('response', (response) => {
|
||||
if (response.url().startsWith(`${liveFormatMock}/live/`))
|
||||
media.push({
|
||||
file: new URL(response.url()).pathname.split('/').pop() ?? '',
|
||||
status: response.status(),
|
||||
});
|
||||
});
|
||||
return media;
|
||||
}
|
||||
export async function expectLiveFormatPlaying(
|
||||
page: Page,
|
||||
player: LiveFormatPlayer
|
||||
) {
|
||||
await expect(
|
||||
page.locator(
|
||||
`app-${player === 'html5' ? 'html-video' : player === 'videojs' ? 'vjs' : 'art'}-player`
|
||||
)
|
||||
).toBeVisible();
|
||||
const video = page.locator('app-web-player-view video').first();
|
||||
await expect
|
||||
.poll(() => video.evaluate((v: HTMLVideoElement) => v.currentTime), {
|
||||
timeout: 50_000,
|
||||
})
|
||||
.toBeGreaterThan(0.5);
|
||||
return video;
|
||||
}
|
||||
Reference in new issue
Block a user