fix(playback): keep MPV/VLC rows of the live tab confirmable by URL

The live tab's session key can only be confirmed by its own inline
players; MPV/VLC confirm the launched URL alone. A row that goes to an
external player (also later, after a double-click) now defers by URL, and
only rows played inline carry the session key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-28 08:25:23 +02:00
1 parent b0467791a4
commit ba1b7a8d73
3 files changed
+41 -9

No files matched your search

@@ -1149,9 +1149,10 @@ reaches history.
- Matching: a write deferred with a session key is confirmed only by that
same key — the same URL in two playlists must not let playback in one
(inline, or in MPV/VLC) record a failed attempt in the other. Writes
without one (portal resolvers, collection tabs) match any confirmation of
their stream URL. The global live tab defers with its own
playlist-scoped session key, which also survives a switch to catch-up.
without one (portal resolvers) match any confirmation of their stream URL.
The global live tab defers with its own playlist-scoped session key, which
also survives a switch to catch-up, when the row plays inline; a row that
goes to MPV/VLC defers by URL, the only thing that launch confirms.
- `WebPlayerViewComponent` confirms its `playbackSessionKey`, `streamUrl`
and `playback.streamUrl` once the owned engine's reported position has
advanced by 2 seconds while playing (`PlaybackProgressConfirmation`).
@@ -248,13 +248,20 @@ export function createUnifiedLiveSelection(options: {
}
// Selecting a channel is not watching it: the row moves to the
// top of Recently Viewed once its stream has really played. The
// tab's playlist-scoped session key — the one its players confirm
// with — survives a switch to catch-up and does not match the
// same URL listed in another playlist.
// top of Recently Viewed once its stream has really played. Played
// inline, the tab's playlist-scoped session key — the one its
// players confirm with — survives a switch to catch-up and does
// not match the same URL in another playlist. MPV/VLC can only
// confirm the launched URL, so an external row defers by URL.
const playsInline = !options.shouldOpenExternalPlayback(
detail,
true
);
historyGate.defer(
{
sessionKey: createUnifiedLivePlaybackSessionKey(item),
sessionKey: playsInline
? createUnifiedLivePlaybackSessionKey(item)
: null,
streamUrls: [detail.playback.streamUrl],
},
() => void recordLivePlayback(item)
@@ -175,7 +175,8 @@ describe('UnifiedLiveTabComponent fullscreen channel panel', () => {
}
);
it('correlates a row with its session key, not its stream URL', async () => {
it('correlates an inline row with its session key, not its stream URL', async () => {
portalPlayer.isEmbeddedPlayer.mockReturnValue(true);
const item = buildM3uLiveItem();
streamResolver.resolveM3uPlaybackDetail.mockResolvedValue({
epgMode: 'm3u',
@@ -206,6 +207,29 @@ describe('UnifiedLiveTabComponent fullscreen channel panel', () => {
expect(recentData.recordLivePlayback).toHaveBeenCalledWith(item);
});
it('records a row played in MPV/VLC from the URL its launch confirms', async () => {
const item = buildM3uLiveItem();
streamResolver.resolveM3uPlaybackDetail.mockResolvedValue({
epgMode: 'm3u',
playback: { streamUrl: item.streamUrl, title: item.name },
epgPrograms: [],
});
recentData.recordLivePlayback.mockResolvedValue(item);
fixture.componentRef.setInput('items', [item]);
fixture.detectChanges();
await fixture.whenStable();
await component.onChannelSelected(component.channelsForList()[0]);
expect(portalPlayer.openResolvedPlayback).toHaveBeenCalled();
// The external session update carries only the launched URL.
TestBed.inject(PlaybackHistoryGate).confirm({
streamUrls: [item.streamUrl],
});
await fixture.whenStable();
expect(recentData.recordLivePlayback).toHaveBeenCalledWith(item);
});
it('moves a row that played to the top of Recently Viewed after another row was selected', async () => {
const first = buildM3uLiveItem();
const second: UnifiedCollectionItem = {