feat(library and playback): Support for serverside channel plugins and hls streaming
This commit is contained in:
@@ -76,6 +76,22 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Live TV channels use a dedicated endpoint
|
||||
if (lib.collectionType === "livetv") {
|
||||
library.setCurrentLibrary(lib);
|
||||
library.clearGenres();
|
||||
await library.loadLiveTvChannels();
|
||||
return;
|
||||
}
|
||||
|
||||
// Plugin "Channels" root list uses a dedicated endpoint
|
||||
if (lib.collectionType === "channels") {
|
||||
library.setCurrentLibrary(lib);
|
||||
library.clearGenres();
|
||||
await library.loadChannels();
|
||||
return;
|
||||
}
|
||||
|
||||
// For other library types, load items normally
|
||||
library.setCurrentLibrary(lib);
|
||||
library.clearGenres();
|
||||
@@ -97,6 +113,11 @@
|
||||
if ("type" in item) {
|
||||
// It's a MediaItem
|
||||
const mediaItem = item as MediaItem;
|
||||
// A ChannelFolderItem can be a folder (drill in) or a playable leaf.
|
||||
if (mediaItem.type === "ChannelFolderItem" && !mediaItem.isFolder) {
|
||||
goto(`/player/${mediaItem.id}`);
|
||||
return;
|
||||
}
|
||||
switch (mediaItem.type) {
|
||||
case "Series":
|
||||
case "Movie":
|
||||
@@ -111,7 +132,8 @@
|
||||
goto(`/library/${mediaItem.id}`);
|
||||
break;
|
||||
case "Episode":
|
||||
// Episodes play directly
|
||||
case "TvChannel":
|
||||
// Episodes and live TV channels play directly
|
||||
goto(`/player/${mediaItem.id}`);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -187,6 +187,12 @@
|
||||
goto(`/library/${clickedItem.id}`);
|
||||
return;
|
||||
}
|
||||
// A ChannelFolderItem can be either a folder (drill in) or a playable leaf.
|
||||
// Route non-folder channel items straight to the player.
|
||||
if (clickedItem.type === "ChannelFolderItem" && !clickedItem.isFolder) {
|
||||
goto(`/player/${clickedItem.id}`);
|
||||
return;
|
||||
}
|
||||
switch (clickedItem.type) {
|
||||
case "Series":
|
||||
case "Season":
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
let streamUrl = $state<string | null>(null);
|
||||
let mediaSourceId = $state<string | null>(null);
|
||||
let isVideo = $state(false);
|
||||
let isLive = $state(false); // Whether this is a live stream (Live TV channel) - no seek/resume
|
||||
let videoInitialPosition = $state(0); // Position in seconds to seek to after video loads
|
||||
let videoNeedsTranscoding = $state(false); // Whether video needs transcoding (HEVC/10-bit)
|
||||
let isOfflinePlayback = $state(false); // Whether playing from local file
|
||||
@@ -104,6 +105,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
// A playable channel leaf (ChannelFolderItem) has no dedicated item type, so
|
||||
// treat it as video when it carries a video media stream.
|
||||
function isVideoChannelItem(item: MediaItem): boolean {
|
||||
return (
|
||||
item.type === "ChannelFolderItem" &&
|
||||
(item.mediaStreams?.some((s) => s.type === "Video") ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
async function loadAndPlay(id: string, startPosition?: number, forceRestart = false) {
|
||||
loading = true;
|
||||
error = null;
|
||||
@@ -118,8 +128,9 @@
|
||||
currentMedia = item;
|
||||
|
||||
// Check if this is a non-playable collection type that should be viewed in library instead
|
||||
const collectionTypes = ["MusicAlbum", "MusicArtist", "Series", "Season", "Folder", "CollectionFolder", "Playlist"];
|
||||
if (collectionTypes.includes(item.type)) {
|
||||
const collectionTypes = ["MusicAlbum", "MusicArtist", "Series", "Season", "Folder", "CollectionFolder", "Playlist", "Channel"];
|
||||
// A ChannelFolderItem that is itself a folder is a container, not playable.
|
||||
if (collectionTypes.includes(item.type) || (item.type === "ChannelFolderItem" && item.isFolder)) {
|
||||
console.log("loadAndPlay: Redirecting collection type to library:", item.type);
|
||||
goto(`/library/${id}`);
|
||||
return;
|
||||
@@ -132,7 +143,8 @@
|
||||
const alreadyPlayingMedia = get(storeCurrentMedia);
|
||||
if (alreadyPlayingMedia?.id === id && !startPosition && !forceRestart) {
|
||||
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
||||
isVideo = item.type === "Movie" || item.type === "Episode";
|
||||
isLive = item.type === "TvChannel";
|
||||
isVideo = item.type === "Movie" || item.type === "Episode" || isLive || isVideoChannelItem(item);
|
||||
isPlaying = true;
|
||||
loading = false;
|
||||
// hasNext/hasPrevious come from the event-driven queue store.
|
||||
@@ -143,8 +155,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if this is video content (Movie and Episode are video types)
|
||||
isVideo = item.type === "Movie" || item.type === "Episode";
|
||||
// Determine if this is video content (Movie, Episode, live TV channels, and
|
||||
// channel leaf items that carry a video stream).
|
||||
isLive = item.type === "TvChannel";
|
||||
isVideo = item.type === "Movie" || item.type === "Episode" || isLive || isVideoChannelItem(item);
|
||||
|
||||
// When switching to video, stop audio playback and clear the queue
|
||||
// This prevents audio from continuing in the background and clears stale state
|
||||
@@ -164,7 +178,8 @@
|
||||
const userId = auth.getUserId();
|
||||
console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition, "forceRestart:", forceRestart);
|
||||
|
||||
if (!startPosition && !forceRestart && userId) {
|
||||
// Live streams have no fixed position - never resume.
|
||||
if (!startPosition && !forceRestart && userId && !isLive) {
|
||||
try {
|
||||
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
||||
console.log("Resume check - retrieved progress:", progress);
|
||||
@@ -251,6 +266,22 @@
|
||||
// Online playback - get playback info from server
|
||||
isOfflinePlayback = false;
|
||||
const repo = auth.getRepository();
|
||||
|
||||
if (isLive) {
|
||||
// Live TV channels must be "opened" before streaming; the server returns
|
||||
// a ready-to-play HLS transcoding URL. No resume, no seek, no progress.
|
||||
console.log("loadAndPlay: Opening live stream for channel:", id);
|
||||
const liveInfo = await repo.openLiveStream(id);
|
||||
console.log("loadAndPlay: Live stream URL:", liveInfo.streamUrl);
|
||||
mediaSourceId = liveInfo.mediaSourceId;
|
||||
streamUrl = liveInfo.streamUrl;
|
||||
videoNeedsTranscoding = true;
|
||||
videoInitialPosition = 0;
|
||||
isPlaying = true;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("loadAndPlay: Getting playback info");
|
||||
const playbackInfo = await repo.getPlaybackInfo(id);
|
||||
console.log("loadAndPlay: Got playback info, mediaSourceId:", playbackInfo.mediaSourceId);
|
||||
@@ -613,6 +644,7 @@
|
||||
mediaSourceId={mediaSourceId ?? undefined}
|
||||
initialPosition={videoInitialPosition}
|
||||
needsTranscoding={videoNeedsTranscoding}
|
||||
{isLive}
|
||||
onClose={handleClose}
|
||||
onSeek={handleVideoSeek}
|
||||
onReportStart={handleReportStart}
|
||||
|
||||
Reference in New Issue
Block a user