fix(player): play downloaded video offline (DR-133, DR-134)
Offline video never started: the <video> element reported NETWORK_NO_SOURCE one millisecond after loadstart, which the UI mislabelled as "may need transcoding" even though nothing had been fetched. Two independent causes, both required for playback. The path was doubled. `downloads.file_path` is stored relative to the storage root while a download is queued, but the worker rewrites it to the absolute path it actually wrote once the transfer completes — so a completed row is already rooted. The player's offline branch rooted it a second time, producing /data/user/0/app//data/user/0/app/videos/x.mp4. Audio was unaffected because it resolves the same column through Rust's resolve_local_media_path, which does not re-root. The join is now absolute-aware (POSIX, Windows drive letters, UNC) so rows written before completion still resolve. The asset protocol was never enabled. convertFileSrc rewrites a path to http://asset.localhost/… unconditionally, but Tauri only answers that origin when the protocol-asset cargo feature is compiled in *and* app.security.assetProtocol.enable is set — neither was, so even a correct path resolved to nothing. This also silently defeated the cached-thumbnail path in imageCache, which fails soft to the server copy and so hid the breakage whenever the server was reachable. Scoped to $APPDATA/** — the storage root holding the database, downloads/ and the thumbnail cache — rather than an unrestricted grant. Diagnosed from logcat on device; UT-124 reproduces the doubled path.
This commit is contained in:
@@ -1,5 +1,34 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolveVideoSource } from "./localSource";
|
||||
import { downloadedFilePath, resolveVideoSource } from "./localSource";
|
||||
|
||||
describe("downloadedFilePath", () => {
|
||||
// The download worker rewrites `downloads.file_path` to the absolute path it
|
||||
// actually wrote once the transfer completes, so a completed row is already
|
||||
// rooted. Joining it onto the storage root again produced
|
||||
// `/data/user/0/app//data/user/0/app/videos/x.mp4`, which the asset protocol
|
||||
// cannot open — offline video died with MEDIA_ERR_SRC_NOT_SUPPORTED while
|
||||
// audio, which resolves the same column through Rust, played fine.
|
||||
it("leaves a completed download's absolute path alone", () => {
|
||||
const root = "/data/user/0/com.dtourolle.jellytau";
|
||||
const stored = `${root}/videos/Taming of the Shrew.mp4`;
|
||||
|
||||
expect(downloadedFilePath(root, stored)).toBe(stored);
|
||||
});
|
||||
|
||||
it("roots a path that is still relative to the storage directory", () => {
|
||||
// Rows only hold a relative path before the worker completes them, but a
|
||||
// half-migrated database can still carry one.
|
||||
expect(downloadedFilePath("/var/data/jellytau", "videos/film.mp4")).toBe(
|
||||
"/var/data/jellytau/videos/film.mp4"
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves an absolute Windows path alone", () => {
|
||||
const stored = "C:\\Users\\u\\AppData\\jellytau\\videos\\film.mp4";
|
||||
|
||||
expect(downloadedFilePath("C:\\Users\\u\\AppData\\jellytau", stored)).toBe(stored);
|
||||
});
|
||||
});
|
||||
|
||||
// A stand-in for Tauri's convertFileSrc, so the module stays pure.
|
||||
const toAssetUrl = (p: string) => `asset://localhost/${encodeURIComponent(p)}`;
|
||||
|
||||
@@ -36,6 +36,28 @@ export interface VideoSourceDecision {
|
||||
isLocal: boolean;
|
||||
}
|
||||
|
||||
/** Absolute on POSIX (`/…`), Windows (`C:\…`, `C:/…`) or a UNC share (`\\…`). */
|
||||
function isAbsolute(path: string): boolean {
|
||||
return path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* The on-disk path of a row in the `downloads` store.
|
||||
*
|
||||
* `downloads.file_path` is stored relative to the storage root while a download
|
||||
* is queued, but the worker rewrites it to the absolute path it actually wrote
|
||||
* once the transfer completes — so a *completed* row is already rooted. Joining
|
||||
* it onto the storage root a second time produced
|
||||
* `/data/user/0/app//data/user/0/app/videos/x.mp4`; the asset protocol could not
|
||||
* open that, so offline video failed with `MEDIA_ERR_SRC_NOT_SUPPORTED` while
|
||||
* audio, which resolves the same column through Rust, played fine.
|
||||
*
|
||||
* TRACES: UR-071 | DR-133 | UT-124
|
||||
*/
|
||||
export function downloadedFilePath(storageRoot: string, filePath: string): string {
|
||||
return isAbsolute(filePath) ? filePath : `${storageRoot}/${filePath}`;
|
||||
}
|
||||
|
||||
export function resolveVideoSource(inputs: VideoSourceInputs): VideoSourceDecision {
|
||||
const { localPath, remoteUrl, remoteNeedsTranscoding, toAssetUrl } = inputs;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import { resolveVideoSource } from "$lib/player/localSource";
|
||||
import { downloadedFilePath, resolveVideoSource } from "$lib/player/localSource";
|
||||
import type { PlayQueueRequest } from "$lib/api/bindings";
|
||||
import type { MediaItem, MediaKind } from "$lib/api/types";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
@@ -236,9 +236,12 @@
|
||||
console.log("loadAndPlay: Found local download, using offline playback:", localDownload.filePath);
|
||||
isOfflinePlayback = true;
|
||||
|
||||
// Get the storage path and construct full file path
|
||||
// Get the storage path and resolve the file's location. A completed
|
||||
// row already holds an absolute path (the worker rewrites it on
|
||||
// completion), so it must not be rooted again — see downloadedFilePath.
|
||||
// TRACES: UR-071 | DR-133
|
||||
const storagePath = await commands.storageGetPath();
|
||||
const fullPath = `${storagePath}/${localDownload.filePath}`;
|
||||
const fullPath = downloadedFilePath(storagePath, localDownload.filePath);
|
||||
console.log("loadAndPlay: Full local path:", fullPath);
|
||||
|
||||
// Convert file path to asset URL that can be played in webview
|
||||
|
||||
Reference in New Issue
Block a user