forked from Ninjalama/streamyfin_mirror
Compare commits
2 Commits
develop
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b6c7a7603 | ||
|
|
5a07eccd9b |
@@ -231,7 +231,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
|
|||||||
if (usingOptimizedServer) {
|
if (usingOptimizedServer) {
|
||||||
await startBackgroundDownload(url, item, source);
|
await startBackgroundDownload(url, item, source);
|
||||||
} else {
|
} else {
|
||||||
await startRemuxing(item, url, source);
|
await startRemuxing(item, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ import { Chromecast } from "./Chromecast";
|
|||||||
import { ItemHeader } from "./ItemHeader";
|
import { ItemHeader } from "./ItemHeader";
|
||||||
import { MediaSourceSelector } from "./MediaSourceSelector";
|
import { MediaSourceSelector } from "./MediaSourceSelector";
|
||||||
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
|
||||||
|
import {
|
||||||
|
brightness,
|
||||||
|
ColorMatrix,
|
||||||
|
colorTone,
|
||||||
|
concatColorMatrices,
|
||||||
|
contrast,
|
||||||
|
saturate,
|
||||||
|
sepia,
|
||||||
|
tint,
|
||||||
|
} from "react-native-color-matrix-image-filters";
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
bitrate: Bitrate;
|
bitrate: Bitrate;
|
||||||
@@ -49,7 +59,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
useImageColors({ item });
|
useImageColors({ item });
|
||||||
|
|
||||||
const [loadingLogo, setLoadingLogo] = useState(true);
|
const [loadingLogo, setLoadingLogo] = useState(false);
|
||||||
const [headerHeight, setHeaderHeight] = useState(350);
|
const [headerHeight, setHeaderHeight] = useState(350);
|
||||||
|
|
||||||
const [selectedOptions, setSelectedOptions] = useState<
|
const [selectedOptions, setSelectedOptions] = useState<
|
||||||
@@ -139,6 +149,34 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
logo={
|
logo={
|
||||||
<>
|
<>
|
||||||
{logoUrl ? (
|
{logoUrl ? (
|
||||||
|
<ColorMatrix
|
||||||
|
matrix={[
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0, // Red channel remains unchanged
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0, // Green channel remains unchanged
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0, // Blue channel remains unchanged
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
-1, // Make black (R=0, G=0, B=0) transparent
|
||||||
|
]}
|
||||||
|
style={{
|
||||||
|
height: 130,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{
|
source={{
|
||||||
uri: logoUrl,
|
uri: logoUrl,
|
||||||
@@ -148,9 +186,8 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
resizeMode: "contain",
|
resizeMode: "contain",
|
||||||
}}
|
}}
|
||||||
onLoad={() => setLoadingLogo(false)}
|
|
||||||
onError={() => setLoadingLogo(false)}
|
|
||||||
/>
|
/>
|
||||||
|
</ColorMatrix>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const createFFmpegCommand = (url: string, output: string) => [
|
|||||||
"-c copy", // streamcopy, preventing transcoding
|
"-c copy", // streamcopy, preventing transcoding
|
||||||
"-bufsize 25M", // amount of data processed before calculating current bitrate
|
"-bufsize 25M", // amount of data processed before calculating current bitrate
|
||||||
"-max_muxing_queue_size 4096", // sets the size of stream buffer in packets for output
|
"-max_muxing_queue_size 4096", // sets the size of stream buffer in packets for output
|
||||||
output
|
output,
|
||||||
]
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook for remuxing HLS to MP4 using FFmpeg.
|
* Custom hook for remuxing HLS to MP4 using FFmpeg.
|
||||||
@@ -66,9 +66,10 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await saveImage(item.Id, itemImage?.uri);
|
await saveImage(item.Id, itemImage?.uri);
|
||||||
}
|
};
|
||||||
|
|
||||||
const completeCallback = useCallback(async (session: FFmpegSession, item: BaseItemDto) => {
|
const completeCallback = useCallback(
|
||||||
|
async (session: FFmpegSession, item: BaseItemDto) => {
|
||||||
try {
|
try {
|
||||||
let endTime;
|
let endTime;
|
||||||
const returnCode = await session.getReturnCode();
|
const returnCode = await session.getReturnCode();
|
||||||
@@ -77,15 +78,20 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
if (returnCode.isValueSuccess()) {
|
if (returnCode.isValueSuccess()) {
|
||||||
endTime = new Date();
|
endTime = new Date();
|
||||||
const stat = await session.getLastReceivedStatistics();
|
const stat = await session.getLastReceivedStatistics();
|
||||||
await queryClient.invalidateQueries({queryKey: ["downloadedItems"]});
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["downloadedItems"],
|
||||||
|
});
|
||||||
|
|
||||||
saveDownloadedItemInfo(item, stat.getSize());
|
saveDownloadedItemInfo(item, stat.getSize());
|
||||||
writeInfoLog(
|
writeInfoLog(
|
||||||
`useRemuxHlsToMp4 ~ remuxing completed successfully for item: ${item.Name},
|
`useRemuxHlsToMp4 ~ remuxing completed successfully for item: ${
|
||||||
|
item.Name
|
||||||
|
},
|
||||||
start time: ${startTime.toISOString()}, end time: ${endTime.toISOString()},
|
start time: ${startTime.toISOString()}, end time: ${endTime.toISOString()},
|
||||||
duration: ${(endTime.getTime() - startTime.getTime()) / 1000}s`
|
duration: ${
|
||||||
.replace(/^ +/g, '')
|
(endTime.getTime() - startTime.getTime()) / 1000
|
||||||
)
|
}s`.replace(/^ +/g, "")
|
||||||
|
);
|
||||||
toast.success("Download completed");
|
toast.success("Download completed");
|
||||||
} else if (returnCode.isValueError()) {
|
} else if (returnCode.isValueError()) {
|
||||||
endTime = new Date();
|
endTime = new Date();
|
||||||
@@ -93,17 +99,19 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
writeErrorLog(
|
writeErrorLog(
|
||||||
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
|
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
|
||||||
start time: ${startTime.toISOString()}, end time: ${endTime.toISOString()},
|
start time: ${startTime.toISOString()}, end time: ${endTime.toISOString()},
|
||||||
duration: ${(endTime.getTime() - startTime.getTime()) / 1000}s. All logs: ${allLogs}`
|
duration: ${
|
||||||
.replace(/^ +/g, '')
|
(endTime.getTime() - startTime.getTime()) / 1000
|
||||||
)
|
}s. All logs: ${allLogs}`.replace(/^ +/g, "")
|
||||||
|
);
|
||||||
} else if (returnCode.isValueCancel()) {
|
} else if (returnCode.isValueCancel()) {
|
||||||
endTime = new Date();
|
endTime = new Date();
|
||||||
writeInfoLog(
|
writeInfoLog(
|
||||||
`useRemuxHlsToMp4 ~ remuxing was canceled for item: ${item.Name},
|
`useRemuxHlsToMp4 ~ remuxing was canceled for item: ${item.Name},
|
||||||
start time: ${startTime.toISOString()}, end time: ${endTime.toISOString()},
|
start time: ${startTime.toISOString()}, end time: ${endTime.toISOString()},
|
||||||
duration: ${(endTime.getTime() - startTime.getTime()) / 1000}s`
|
duration: ${
|
||||||
.replace(/^ +/g, '')
|
(endTime.getTime() - startTime.getTime()) / 1000
|
||||||
)
|
}s`.replace(/^ +/g, "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProcesses((prev) => {
|
setProcesses((prev) => {
|
||||||
@@ -113,23 +121,24 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
const error = e as Error;
|
const error = e as Error;
|
||||||
writeErrorLog(
|
writeErrorLog(
|
||||||
`useRemuxHlsToMp4 ~ Exception during remuxing for item: ${item.Name},
|
`useRemuxHlsToMp4 ~ Exception during remuxing for item: ${item.Name},
|
||||||
Error: ${error.message}, Stack: ${error.stack}`
|
Error: ${error.message}, Stack: ${error.stack}`.replace(/^ +/g, "")
|
||||||
.replace(/^ +/g, '')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [processes, setProcesses]);
|
},
|
||||||
|
[processes, setProcesses]
|
||||||
|
);
|
||||||
|
|
||||||
const statisticsCallback = useCallback((statistics: Statistics, item: BaseItemDto) => {
|
const statisticsCallback = useCallback(
|
||||||
const videoLength = (item.MediaSources?.[0]?.RunTimeTicks || 0) / 10000000; // In seconds
|
(statistics: Statistics, item: BaseItemDto) => {
|
||||||
|
const videoLength =
|
||||||
|
(item.MediaSources?.[0]?.RunTimeTicks || 0) / 10000000; // In seconds
|
||||||
const fps = item.MediaStreams?.[0]?.RealFrameRate || 25;
|
const fps = item.MediaStreams?.[0]?.RealFrameRate || 25;
|
||||||
const totalFrames = videoLength * fps;
|
const totalFrames = videoLength * fps;
|
||||||
const processedFrames = statistics.getVideoFrameNumber();
|
const processedFrames = statistics.getVideoFrameNumber();
|
||||||
const speed = statistics.getSpeed();
|
const speed = statistics.getSpeed();
|
||||||
|
|
||||||
const percentage =
|
const percentage =
|
||||||
totalFrames > 0
|
totalFrames > 0 ? Math.floor((processedFrames / totalFrames) * 100) : 0;
|
||||||
? Math.floor((processedFrames / totalFrames) * 100)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (!item.Id) throw new Error("Item is undefined");
|
if (!item.Id) throw new Error("Item is undefined");
|
||||||
setProcesses((prev) => {
|
setProcesses((prev) => {
|
||||||
@@ -145,10 +154,12 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
return process;
|
return process;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [setProcesses, completeCallback]);
|
},
|
||||||
|
[setProcesses, completeCallback]
|
||||||
|
);
|
||||||
|
|
||||||
const startRemuxing = useCallback(
|
const startRemuxing = useCallback(
|
||||||
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
|
async (item: BaseItemDto, url: string) => {
|
||||||
const output = `${FileSystem.documentDirectory}${item.Id}.mp4`;
|
const output = `${FileSystem.documentDirectory}${item.Id}.mp4`;
|
||||||
if (!api) throw new Error("API is not defined");
|
if (!api) throw new Error("API is not defined");
|
||||||
if (!item.Id) throw new Error("Item must have an Id");
|
if (!item.Id) throw new Error("Item must have an Id");
|
||||||
@@ -177,17 +188,17 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
status: "downloading",
|
status: "downloading",
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}
|
};
|
||||||
|
|
||||||
writeInfoLog(`useRemuxHlsToMp4 ~ startRemuxing for item ${item.Name}`);
|
writeInfoLog(`useRemuxHlsToMp4 ~ startRemuxing for item ${item.Name}`);
|
||||||
setProcesses((prev) => [...prev, job]);
|
setProcesses((prev) => [...prev, job]);
|
||||||
|
|
||||||
await FFmpegKit.executeAsync(
|
await FFmpegKit.executeAsync(
|
||||||
createFFmpegCommand(url, output).join(" "),
|
createFFmpegCommand(url, output).join(" "),
|
||||||
session => completeCallback(session, item),
|
(session) => completeCallback(session, item),
|
||||||
undefined,
|
undefined,
|
||||||
s => statisticsCallback(s, item)
|
(s) => statisticsCallback(s, item)
|
||||||
)
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as Error;
|
const error = e as Error;
|
||||||
console.error("Failed to remux:", error);
|
console.error("Failed to remux:", error);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
"react-native-awesome-slider": "^2.5.6",
|
"react-native-awesome-slider": "^2.5.6",
|
||||||
"react-native-bottom-tabs": "^0.7.3",
|
"react-native-bottom-tabs": "^0.7.3",
|
||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
|
"react-native-color-matrix-image-filters": "^7.0.1",
|
||||||
"react-native-compressor": "^1.9.0",
|
"react-native-compressor": "^1.9.0",
|
||||||
"react-native-device-info": "^14.0.1",
|
"react-native-device-info": "^14.0.1",
|
||||||
"react-native-edge-to-edge": "^1.1.1",
|
"react-native-edge-to-edge": "^1.1.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user