Add JRay support
This commit is contained in:
@@ -1068,6 +1068,14 @@ async repositoryGetItems(handle: string, parentId: string, options: GetItemsOpti
|
||||
async repositoryGetItem(handle: string, itemId: string) : Promise<MediaItem> {
|
||||
return await TAURI_INVOKE("repository_get_item", { handle, itemId });
|
||||
},
|
||||
/**
|
||||
* Query the optional JRay plugin for the actors on screen at time `t`
|
||||
* (seconds) in an item. Returns an empty list when JRay isn't installed or
|
||||
* has no data for the item, so the caller can render nothing without error.
|
||||
*/
|
||||
async repositoryJrayActorsAt(handle: string, itemId: string, t: number) : Promise<JRayActor[]> {
|
||||
return await TAURI_INVOKE("repository_jray_actors_at", { handle, itemId, t });
|
||||
},
|
||||
/**
|
||||
* Get latest items in a library
|
||||
*/
|
||||
@@ -1553,6 +1561,14 @@ export type ImageOptions = { maxWidth?: number | null; maxHeight?: number | null
|
||||
* Image type
|
||||
*/
|
||||
export type ImageType = "Primary" | "Backdrop" | "Banner" | "Thumb" | "Logo"
|
||||
/**
|
||||
* A single actor returned by the JRay plugin's "context at time t" endpoint.
|
||||
*
|
||||
* Mirrors the `actors[]` objects from `GET /Plugins/JRay/Items/{id}/jray?t=`.
|
||||
* `jellyfin_id` (a Jellyfin Person item GUID) is preferred for navigation;
|
||||
* the IMDb/TMDb ids are informational fallbacks. Unknown ids are `""`.
|
||||
*/
|
||||
export type JRayActor = { name: string; imdb_id?: string; tmdb_id?: string; jellyfin_id?: string }
|
||||
/**
|
||||
* Library (media collection)
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// NO direct HTTP calls - everything routes through Rust backend
|
||||
|
||||
import { commands } from "./bindings";
|
||||
import type { JRayActor } from "./bindings";
|
||||
import type { QualityPreset } from "./quality-presets";
|
||||
import type {
|
||||
Library,
|
||||
@@ -90,6 +91,15 @@ export class RepositoryClient {
|
||||
return commands.repositoryGetItem(this.ensureHandle(), itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the optional JRay plugin for the actors on screen at time `t`
|
||||
* (seconds) in an item. Resolves to an empty array when JRay isn't installed
|
||||
* or has no data for the item.
|
||||
*/
|
||||
async jrayActorsAt(itemId: string, t: number): Promise<JRayActor[]> {
|
||||
return commands.repositoryJrayActorsAt(this.ensureHandle(), itemId, t);
|
||||
}
|
||||
|
||||
async getLatestItems(parentId: string, limit?: number): Promise<MediaItem[]> {
|
||||
return commands.repositoryGetLatestItems(this.ensureHandle(), parentId, limit ?? null);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, untrack } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import type { JRayActor } from "$lib/api/bindings";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import Hls from "hls.js";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
@@ -750,6 +752,58 @@
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ===== JRay "who's on screen" pause overlay (optional plugin) =====
|
||||
// On pause, ask the JRay Jellyfin plugin which actors are on screen at the
|
||||
// current timestamp and show them as a tappable overlay. If the plugin isn't
|
||||
// installed (or has no data for this item), the call resolves to [] and the
|
||||
// overlay simply doesn't render. Tapping an actor with a resolved Jellyfin
|
||||
// Person id navigates to that person's library page.
|
||||
let jrayActors = $state<JRayActor[]>([]);
|
||||
// Monotonic token so a slow in-flight request can't overwrite a newer pause
|
||||
// (or a resume that cleared the list).
|
||||
let jrayRequestId = 0;
|
||||
|
||||
async function fetchJrayActors() {
|
||||
const itemId = media?.id;
|
||||
if (!itemId) return;
|
||||
const token = ++jrayRequestId;
|
||||
const t = currentTime;
|
||||
try {
|
||||
const actors = await auth.getRepository().jrayActorsAt(itemId, t);
|
||||
// Discard if a newer pause/resume happened while we were waiting.
|
||||
if (token === jrayRequestId) {
|
||||
jrayActors = actors;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[VideoPlayer] JRay lookup failed:", err);
|
||||
if (token === jrayRequestId) jrayActors = [];
|
||||
}
|
||||
}
|
||||
|
||||
function clearJrayActors() {
|
||||
jrayRequestId++; // invalidate any in-flight request
|
||||
jrayActors = [];
|
||||
}
|
||||
|
||||
function openJrayActor(actor: JRayActor) {
|
||||
if (actor.jellyfin_id) {
|
||||
goto(`/library/${actor.jellyfin_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Drive the JRay overlay off the single `isPlaying` flag so it works for both
|
||||
// the HTML5 <video> (desktop) and the native ExoPlayer path (Android, which
|
||||
// updates isPlaying via the player://state-changed event). Fetch when we go
|
||||
// paused, clear when we resume. untrack() keeps this from re-running on every
|
||||
// currentTime tick — only isPlaying transitions matter.
|
||||
$effect(() => {
|
||||
if (isPlaying) {
|
||||
untrack(clearJrayActors);
|
||||
} else if (isMediaReady) {
|
||||
untrack(fetchJrayActors);
|
||||
}
|
||||
});
|
||||
|
||||
function handlePlay() {
|
||||
isPlaying = true;
|
||||
startTimeUpdates(); // Start RAF loop for smooth time updates
|
||||
@@ -1404,6 +1458,29 @@
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- JRay "who's on screen" overlay: shown while paused when the JRay plugin
|
||||
returned actors for the current timestamp. Tapping an actor with a
|
||||
resolved Jellyfin Person id opens their library page. -->
|
||||
{#if !isPlaying && !isSeeking && jrayActors.length > 0}
|
||||
<div class="absolute top-4 right-4 max-w-xs bg-black/70 rounded-lg p-3 backdrop-blur-sm pointer-events-auto">
|
||||
<div class="text-white/60 text-xs font-medium uppercase tracking-wide mb-2">On screen</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each jrayActors as actor (actor.name + actor.jellyfin_id)}
|
||||
{#if actor.jellyfin_id}
|
||||
<button
|
||||
class="text-left text-white text-sm hover:text-blue-300 transition-colors"
|
||||
onclick={() => openJrayActor(actor)}
|
||||
>
|
||||
{actor.name}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="text-white/80 text-sm">{actor.name}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
|
||||
Reference in New Issue
Block a user