Fixed trick play for VLC

This commit is contained in:
Alex Kim
2024-10-28 21:12:42 +11:00
parent 9ca71dc7fc
commit db20fffeb5
4 changed files with 75 additions and 37 deletions

View File

@@ -240,14 +240,18 @@ export default function page() {
const { currentTime } = data.nativeEvent; const { currentTime } = data.nativeEvent;
if (isBuffering) {
setIsBuffering(false);
}
progress.value = currentTime; progress.value = currentTime;
const currentTimeInTicks = msToTicks(currentTime); const currentTimeInTicks = msToTicks(currentTime);
console.log("onProgress ~", { // console.log("onProgress ~", {
currentTime, // currentTime,
currentTimeInTicks, // currentTimeInTicks,
isPlaying, // isPlaying,
}); // });
await getPlaystateApi(api).onPlaybackProgress({ await getPlaystateApi(api).onPlaybackProgress({
itemId: item.Id, itemId: item.Id,
@@ -259,7 +263,6 @@ export default function page() {
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream", playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
playSessionId: stream.sessionId, playSessionId: stream.sessionId,
}); });
console.log("Progress", currentTime);
}, },
[item?.Id, isPlaying, api, isPlaybackStopped] [item?.Id, isPlaying, api, isPlaybackStopped]
); );

View File

@@ -12,6 +12,7 @@ import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import { import {
formatTimeString, formatTimeString,
msToTicks,
secondsToMs, secondsToMs,
ticksToMs, ticksToMs,
ticksToSeconds, ticksToSeconds,
@@ -234,8 +235,16 @@ export const Controls: React.FC<Props> = ({
[isVlc] [isVlc]
); );
const [time, setTime] = useState({ minutes: 0, seconds: 0 });
const handleSliderChange = (value: number) => { const handleSliderChange = (value: number) => {
calculateTrickplayUrl(value); const progressInTicks = isVlc ? msToTicks(value) : value;
calculateTrickplayUrl(progressInTicks);
const progressInSeconds = Math.floor(progressInTicks / 10000000);
const minutes = Math.floor(progressInSeconds / 60);
const seconds = progressInSeconds % 60;
setTime({ minutes, seconds });
}; };
const handleSliderStart = useCallback(() => { const handleSliderStart = useCallback(() => {
@@ -351,7 +360,6 @@ export const Controls: React.FC<Props> = ({
const uniqueExternalSubs = externalSubs.filter( const uniqueExternalSubs = externalSubs.filter(
(sub) => !embeddedSubNames.has(sub.name) (sub) => !embeddedSubNames.has(sub.name)
); );
// Combine embedded and unique external subs // Combine embedded and unique external subs
return [...embeddedSubs, ...uniqueExternalSubs] as ( return [...embeddedSubs, ...uniqueExternalSubs] as (
| EmbeddedSubtitle | EmbeddedSubtitle
@@ -359,6 +367,11 @@ export const Controls: React.FC<Props> = ({
)[]; )[];
}, [item, isVideoLoaded, subtitleTracks, mediaSource]); }, [item, isVideoLoaded, subtitleTracks, mediaSource]);
// useEffect(() => {
// }, [allSubtitleTracks, setSubtitleTrack]);
const [subtitleTrackSet, setSubtitleTrackSet] = useState(false);
return ( return (
<View <View
style={[ style={[
@@ -715,6 +728,21 @@ export const Controls: React.FC<Props> = ({
source={{ uri: url }} source={{ uri: url }}
contentFit="cover" contentFit="cover"
/> />
<Text
style={{
position: "absolute",
bottom: 5,
left: 5,
color: "white",
backgroundColor: "rgba(0, 0, 0, 0.5)",
padding: 5,
borderRadius: 5,
}}
>
{`${time.minutes}:${
time.seconds < 10 ? `0${time.seconds}` : time.seconds
}`}
</Text>
</View> </View>
); );
}} }}

View File

@@ -1,6 +1,7 @@
// hooks/useTrickplay.ts // hooks/useTrickplay.ts
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { ticksToMs } from "@/utils/time";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
@@ -57,6 +58,7 @@ export const useTrickplay = (item: BaseItemDto, enabled = true) => {
: null; : null;
}, [item, enabled]); }, [item, enabled]);
// Takes in ticks.
const calculateTrickplayUrl = useCallback( const calculateTrickplayUrl = useCallback(
(progress: number) => { (progress: number) => {
if (!enabled) { if (!enabled) {
@@ -74,28 +76,33 @@ export const useTrickplay = (item: BaseItemDto, enabled = true) => {
} }
const { data, resolution } = trickplayInfo; const { data, resolution } = trickplayInfo;
const { Interval, TileWidth, TileHeight } = data; const { Interval, TileWidth, TileHeight, Width, Height } = data;
if (!Interval || !TileWidth || !TileHeight || !resolution) { if (
!Interval ||
!TileWidth ||
!TileHeight ||
!resolution ||
!Width ||
!Height
) {
throw new Error("Invalid trickplay data"); throw new Error("Invalid trickplay data");
} }
const currentSecond = Math.max(0, Math.floor(progress / 10000000)); const currentTimeMs = Math.max(0, ticksToMs(progress));
const currentTile = Math.floor(currentTimeMs / Interval);
const cols = TileWidth; const tileSize = TileWidth * TileHeight;
const rows = TileHeight; const tileOffset = currentTile % tileSize;
const imagesPerTile = cols * rows; const index = Math.floor(currentTile / tileSize);
const imageIndex = Math.floor(currentSecond / (Interval / 1000));
const tileIndex = Math.floor(imageIndex / imagesPerTile);
const positionInTile = imageIndex % imagesPerTile; const tileOffsetX = tileOffset % TileWidth;
const rowInTile = Math.floor(positionInTile / cols); const tileOffsetY = Math.floor(tileOffset / TileWidth);
const colInTile = positionInTile % cols;
const newTrickPlayUrl = { const newTrickPlayUrl = {
x: rowInTile, x: tileOffsetX,
y: colInTile, y: tileOffsetY,
url: `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${tileIndex}.jpg?api_key=${api.accessToken}`, url: `${api.basePath}/Videos/${item.Id}/Trickplay/${resolution}/${index}.jpg?api_key=${api.accessToken}`,
}; };
setTrickPlayUrl(newTrickPlayUrl); setTrickPlayUrl(newTrickPlayUrl);

View File

@@ -16,17 +16,17 @@ public class VlcPlayerModule: Module {
} }
} }
// Prop("muted") { (view: VlcPlayerView, muted: Bool) in // Prop("muted") { (view: VlcPlayerView, muted: Bool) in
// view.setMuted(muted) // view.setMuted(muted)
// } // }
// Prop("volume") { (view: VlcPlayerView, volume: Int) in // Prop("volume") { (view: VlcPlayerView, volume: Int) in
// view.setVolume(volume) // view.setVolume(volume)
// } // }
// Prop("videoAspectRatio") { (view: VlcPlayerView, ratio: String) in // Prop("videoAspectRatio") { (view: VlcPlayerView, ratio: String) in
// view.setVideoAspectRatio(ratio) // view.setVideoAspectRatio(ratio)
// } // }
Events( Events(
"onPlaybackStateChanged", "onPlaybackStateChanged",
@@ -69,13 +69,13 @@ public class VlcPlayerModule: Module {
return view.getSubtitleTracks() return view.getSubtitleTracks()
} }
// AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in // AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in
// view.setVideoCropGeometry(geometry) // view.setVideoCropGeometry(geometry)
// } // }
// AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in // AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in
// return view.getVideoCropGeometry() // return view.getVideoCropGeometry()
// } // }
AsyncFunction("setSubtitleURL") { AsyncFunction("setSubtitleURL") {
(view: VlcPlayerView, url: String, name: String) in (view: VlcPlayerView, url: String, name: String) in