feat(library and playback): Support for serverside channel plugins and hls streaming
This commit is contained in:
+38
-2
@@ -1134,6 +1134,24 @@ async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId:
|
||||
async repositoryGetAudioStreamUrl(handle: string, itemId: string) : Promise<string> {
|
||||
return await TAURI_INVOKE("repository_get_audio_stream_url", { handle, itemId });
|
||||
},
|
||||
/**
|
||||
* Get Live TV channels (broadcast / IPTV) for browsing
|
||||
*/
|
||||
async repositoryGetLiveTvChannels(handle: string) : Promise<MediaItem[]> {
|
||||
return await TAURI_INVOKE("repository_get_live_tv_channels", { handle });
|
||||
},
|
||||
/**
|
||||
* Get the root list of plugin "Channels"
|
||||
*/
|
||||
async repositoryGetChannels(handle: string) : Promise<SearchResult> {
|
||||
return await TAURI_INVOKE("repository_get_channels", { handle });
|
||||
},
|
||||
/**
|
||||
* Open a live stream for a Live TV channel / live item
|
||||
*/
|
||||
async repositoryOpenLiveStream(handle: string, itemId: string) : Promise<LiveStreamInfo> {
|
||||
return await TAURI_INVOKE("repository_open_live_stream", { handle, itemId });
|
||||
},
|
||||
/**
|
||||
* Report playback start
|
||||
*/
|
||||
@@ -1539,6 +1557,14 @@ export type ImageType = "Primary" | "Backdrop" | "Banner" | "Thumb" | "Logo"
|
||||
* Library (media collection)
|
||||
*/
|
||||
export type Library = { id: string; name: string; collectionType: string; imageTag?: string | null }
|
||||
/**
|
||||
* Live stream information returned from opening a Live TV / channel stream.
|
||||
*
|
||||
* Unlike on-demand video, a live channel must be "opened" before it can be
|
||||
* streamed; the server returns a transcoding URL (already absolute) plus a
|
||||
* `live_stream_id` that can later be used to close the stream.
|
||||
*/
|
||||
export type LiveStreamInfo = { streamUrl: string; playSessionId: string | null; liveStreamId: string | null; mediaSourceId: string | null }
|
||||
/**
|
||||
* An LMS multi-room sync group, as returned by JellyLMS `/JellyLms/SyncGroups`.
|
||||
*
|
||||
@@ -1549,7 +1575,12 @@ export type LmsSyncGroup = { masterMac: string; masterName?: string; slaveMacs?:
|
||||
/**
|
||||
* Media item
|
||||
*/
|
||||
export type MediaItem = { id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
|
||||
export type MediaItem = { id: string; name: string; type: string;
|
||||
/**
|
||||
* Whether this item is a folder/container (vs a playable leaf). Used to
|
||||
* decide whether a channel item drills into a list or plays directly.
|
||||
*/
|
||||
isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }
|
||||
/**
|
||||
* Media session type tracking the high-level playback context
|
||||
*/
|
||||
@@ -1927,7 +1958,12 @@ export type PlaylistEntry =
|
||||
/**
|
||||
* The underlying media item
|
||||
*/
|
||||
({ id: string; name: string; type: string; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
|
||||
({ id: string; name: string; type: string;
|
||||
/**
|
||||
* Whether this item is a folder/container (vs a playable leaf). Used to
|
||||
* decide whether a channel item drills into a list or plays directly.
|
||||
*/
|
||||
isFolder?: boolean; serverId: string; parentId?: string | null; libraryId?: string | null; overview?: string | null; genres?: string[] | null; productionYear?: number | null; communityRating?: number | null; officialRating?: string | null; runTimeTicks?: number | null; primaryImageTag?: string | null; backdropImageTags?: string[] | null; parentBackdropImageTags?: string[] | null; albumId?: string | null; albumName?: string | null; albumArtist?: string | null; artists?: string[] | null; artistItems?: ArtistItem[] | null; indexNumber?: number | null; parentIndexNumber?: number | null; seriesId?: string | null; seriesName?: string | null; seasonId?: string | null; seasonName?: string | null; userData?: UserData | null; mediaStreams?: MediaStream[] | null; mediaSources?: MediaSource[] | null; people?: Person[] | null }) & {
|
||||
/**
|
||||
* The playlist-scoped entry ID (Jellyfin's PlaylistItemId)
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
GetItemsOptions,
|
||||
SearchOptions,
|
||||
PlaybackInfo,
|
||||
LiveStreamInfo,
|
||||
ImageType,
|
||||
ImageOptions,
|
||||
Genre,
|
||||
@@ -161,6 +162,23 @@ export class RepositoryClient {
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Live TV / Channels =====
|
||||
|
||||
/** Browse Live TV channels (broadcast / IPTV). */
|
||||
async getLiveTvChannels(): Promise<MediaItem[]> {
|
||||
return commands.repositoryGetLiveTvChannels(this.ensureHandle());
|
||||
}
|
||||
|
||||
/** Browse the root list of plugin "Channels". Drill-down uses getItems(channelId). */
|
||||
async getChannels(): Promise<SearchResult> {
|
||||
return commands.repositoryGetChannels(this.ensureHandle());
|
||||
}
|
||||
|
||||
/** Open a live stream for a Live TV channel / live item before HLS playback. */
|
||||
async openLiveStream(itemId: string): Promise<LiveStreamInfo> {
|
||||
return commands.repositoryOpenLiveStream(this.ensureHandle(), itemId);
|
||||
}
|
||||
|
||||
// ===== URL Construction Methods (sync, no server call) =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ export type {
|
||||
ImageOptions,
|
||||
ImageType,
|
||||
Library,
|
||||
LiveStreamInfo,
|
||||
MediaItem,
|
||||
MediaSource,
|
||||
MediaStream,
|
||||
@@ -51,6 +52,7 @@ export type ItemType =
|
||||
| "CollectionFolder"
|
||||
| "Channel"
|
||||
| "ChannelFolderItem"
|
||||
| "TvChannel"
|
||||
| "Person";
|
||||
|
||||
export type LibraryType =
|
||||
@@ -63,6 +65,7 @@ export type LibraryType =
|
||||
| "boxsets"
|
||||
| "playlists"
|
||||
| "channels"
|
||||
| "livetv"
|
||||
| "unknown";
|
||||
|
||||
export type PersonType =
|
||||
|
||||
@@ -30,9 +30,10 @@
|
||||
onEnded?: () => void; // Called when video playback ends naturally
|
||||
onNext?: () => void; // Called when user clicks next episode button
|
||||
hasNext?: boolean; // Whether there is a next episode available
|
||||
isLive?: boolean; // Live stream (Live TV) - no seek bar, no resume, no progress reporting
|
||||
}
|
||||
|
||||
let { media, streamUrl, mediaSourceId, initialPosition, needsTranscoding = false, onClose, onSeek, onReportProgress, onReportStart, onReportStop, onEnded, onNext, hasNext = false }: Props = $props();
|
||||
let { media, streamUrl, mediaSourceId, initialPosition, needsTranscoding = false, onClose, onSeek, onReportProgress, onReportStart, onReportStop, onEnded, onNext, hasNext = false, isLive = false }: Props = $props();
|
||||
|
||||
// The id this player instance reports progress against. Snapshotted from the
|
||||
// media prop so a late reportStop (e.g. from onDestroy during autoplay
|
||||
@@ -488,12 +489,15 @@
|
||||
// Load series audio preference (for TV shows)
|
||||
await loadSeriesAudioPreference();
|
||||
|
||||
// Report progress every 10 seconds while playing
|
||||
progressInterval = setInterval(() => {
|
||||
if (isPlaying && !isSeeking && onReportProgress) {
|
||||
onReportProgress(currentTime, false, reportMediaId);
|
||||
}
|
||||
}, 10000);
|
||||
// Report progress every 10 seconds while playing. Live streams have no
|
||||
// meaningful position to report, so skip progress reporting entirely.
|
||||
if (!isLive) {
|
||||
progressInterval = setInterval(() => {
|
||||
if (isPlaying && !isSeeking && onReportProgress) {
|
||||
onReportProgress(currentTime, false, reportMediaId);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Debug logging every second
|
||||
debugLogInterval = setInterval(() => {
|
||||
@@ -555,8 +559,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Report stop when component is destroyed
|
||||
if (onReportStop && currentTime > 0) {
|
||||
// Report stop when component is destroyed (skip for live - no resume tracking)
|
||||
if (!isLive && onReportStop && currentTime > 0) {
|
||||
onReportStop(currentTime, reportMediaId);
|
||||
}
|
||||
});
|
||||
@@ -749,8 +753,8 @@
|
||||
function handlePlay() {
|
||||
isPlaying = true;
|
||||
startTimeUpdates(); // Start RAF loop for smooth time updates
|
||||
// Report playback start on first play
|
||||
if (!hasReportedStart && onReportStart) {
|
||||
// Report playback start on first play (skip for live - no resume tracking)
|
||||
if (!isLive && !hasReportedStart && onReportStart) {
|
||||
onReportStart(currentTime, reportMediaId);
|
||||
hasReportedStart = true;
|
||||
}
|
||||
@@ -768,8 +772,8 @@
|
||||
function handleEnded() {
|
||||
isPlaying = false;
|
||||
stopTimeUpdates(); // Stop RAF loop when ended
|
||||
// Report stop when video ends
|
||||
if (onReportStop) {
|
||||
// Report stop when video ends (skip for live - no resume tracking)
|
||||
if (!isLive && onReportStop) {
|
||||
onReportStop(currentTime, reportMediaId);
|
||||
}
|
||||
// Notify parent that video has ended (for next episode popup)
|
||||
@@ -1413,26 +1417,35 @@
|
||||
<h2 class="text-white text-lg font-semibold">{media?.name || "Video"}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-white text-sm w-12">{formatTime(currentTime)}</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 100}
|
||||
value={currentTime}
|
||||
oninput={handleSeekBarInput}
|
||||
onchange={handleSeekBarChange}
|
||||
onmousedown={() => isDraggingSeekBar = true}
|
||||
onmouseup={() => isDraggingSeekBar = false}
|
||||
ontouchstart={() => isDraggingSeekBar = true}
|
||||
ontouchend={() => isDraggingSeekBar = false}
|
||||
class="flex-1 h-1 bg-white/30 rounded-full appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||
[&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full"
|
||||
/>
|
||||
<span class="text-white text-sm w-12 text-right">{formatTime(duration)}</span>
|
||||
</div>
|
||||
<!-- Progress bar (hidden for live streams - no fixed timeline) -->
|
||||
{#if isLive}
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="flex items-center gap-1.5 text-white text-sm font-semibold">
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-red-500"></span>
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-white text-sm w-12">{formatTime(currentTime)}</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 100}
|
||||
value={currentTime}
|
||||
oninput={handleSeekBarInput}
|
||||
onchange={handleSeekBarChange}
|
||||
onmousedown={() => isDraggingSeekBar = true}
|
||||
onmouseup={() => isDraggingSeekBar = false}
|
||||
ontouchstart={() => isDraggingSeekBar = true}
|
||||
ontouchend={() => isDraggingSeekBar = false}
|
||||
class="flex-1 h-1 bg-white/30 rounded-full appearance-none cursor-pointer
|
||||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||||
[&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full"
|
||||
/>
|
||||
<span class="text-white text-sm w-12 text-right">{formatTime(duration)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Control buttons -->
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -152,6 +152,47 @@ function createLibraryStore() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load Live TV channels (broadcast / IPTV) into the items list. Live TV uses a
|
||||
// dedicated Jellyfin endpoint rather than the generic /Items browse.
|
||||
async function loadLiveTvChannels() {
|
||||
update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
const channels = await repo.getLiveTvChannels();
|
||||
update((s) => ({
|
||||
...s,
|
||||
items: channels,
|
||||
totalItems: channels.length,
|
||||
loadingCount: Math.max(0, s.loadingCount - 1),
|
||||
}));
|
||||
return channels;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load Live TV channels";
|
||||
update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the root list of plugin "Channels" into the items list.
|
||||
async function loadChannels() {
|
||||
update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
const result = await repo.getChannels();
|
||||
update((s) => ({
|
||||
...s,
|
||||
items: result.items,
|
||||
totalItems: result.totalRecordCount,
|
||||
loadingCount: Math.max(0, s.loadingCount - 1),
|
||||
}));
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to load channels";
|
||||
update((s) => ({ ...s, loadingCount: Math.max(0, s.loadingCount - 1), error: message }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadItem(itemId: string) {
|
||||
update((s) => ({ ...s, loadingCount: s.loadingCount + 1, error: null }));
|
||||
|
||||
@@ -297,6 +338,8 @@ function createLibraryStore() {
|
||||
subscribe,
|
||||
loadLibraries,
|
||||
loadItems,
|
||||
loadLiveTvChannels,
|
||||
loadChannels,
|
||||
loadItem,
|
||||
search,
|
||||
setCurrentLibrary,
|
||||
|
||||
@@ -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