mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-07 10:14:13 -04:00
Check if the browser can play codecs before trying to direct play (#486)
This commit is contained in:
commit
bbe88382e5
51
biome.json
Normal file
51
biome.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts", "**/back/**"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"noParameterAssign": "off",
|
||||
"useEnumInitializers": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": "off"
|
||||
}
|
||||
},
|
||||
"ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts", "**/back/**"]
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingComma": "all",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "double",
|
||||
"attributePosition": "auto"
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,8 @@ const suboctopus = path.resolve(path.dirname(require.resolve("jassub")), "../dis
|
||||
*/
|
||||
const nextConfig = {
|
||||
swcMinify: true,
|
||||
reactStrictMode: true,
|
||||
// can't be true since we would run hls cleanup twice and run on race conditions
|
||||
reactStrictMode: false,
|
||||
output: "standalone",
|
||||
webpack: (config) => {
|
||||
config.plugins = [
|
||||
|
@ -1,51 +1,3 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 100,
|
||||
"attributePosition": "auto",
|
||||
"ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": "off",
|
||||
"noParameterAssign": "off",
|
||||
"useEnumInitializers": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
},
|
||||
"security": {
|
||||
"noDangerouslySetInnerHtml": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": "off"
|
||||
}
|
||||
},
|
||||
"ignore": ["**/.yarn/**", "**/.next/**", "**/.expo/**", "**/next-env.d.ts"]
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingComma": "all",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "double",
|
||||
"attributePosition": "auto"
|
||||
}
|
||||
}
|
||||
"extends": ["../biome.json"]
|
||||
}
|
||||
|
@ -149,6 +149,11 @@ export const WatchInfoP = z
|
||||
* The extension used to store this video file.
|
||||
*/
|
||||
extension: z.string(),
|
||||
/**
|
||||
* The whole mimetype (defined as the RFC 6381).
|
||||
* ex: `video/mp4; codecs="avc1.640028, mp4a.40.2"`
|
||||
*/
|
||||
mimeCodec: z.string(),
|
||||
/**
|
||||
* The file size of the video file.
|
||||
*/
|
||||
|
@ -153,6 +153,7 @@ export const Player = ({
|
||||
links={data?.links}
|
||||
audios={info?.audios}
|
||||
subtitles={info?.subtitles}
|
||||
codec={info?.mimeCodec}
|
||||
setError={setPlaybackError}
|
||||
fonts={info?.fonts}
|
||||
startTime={startTime}
|
||||
|
@ -33,7 +33,7 @@ import {
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform } from "react-native";
|
||||
import NativeVideo, { type VideoProps } from "./video";
|
||||
import NativeVideo, { canPlay, type VideoProps } from "./video";
|
||||
|
||||
export const playAtom = atom(true);
|
||||
export const loadAtom = atom(false);
|
||||
@ -100,6 +100,7 @@ export const Video = memo(function Video({
|
||||
links,
|
||||
subtitles,
|
||||
audios,
|
||||
codec,
|
||||
setError,
|
||||
fonts,
|
||||
startTime: startTimeP,
|
||||
@ -108,6 +109,7 @@ export const Video = memo(function Video({
|
||||
links?: Episode["links"];
|
||||
subtitles?: Subtitle[];
|
||||
audios?: Audio[];
|
||||
codec?: string;
|
||||
setError: (error: string | undefined) => void;
|
||||
fonts?: string[];
|
||||
startTime?: number | null;
|
||||
@ -132,21 +134,31 @@ export const Video = memo(function Video({
|
||||
}, [publicProgress]);
|
||||
|
||||
const getProgress = useAtomCallback(useCallback((get) => get(progressAtom), []));
|
||||
const oldLinks = useRef<typeof links | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
// Reset the state when a new video is loaded.
|
||||
setSource((mode === PlayMode.Direct ? links?.direct : links?.hls) ?? null);
|
||||
setLoad(true);
|
||||
if (oldLinks.current !== links) {
|
||||
setPrivateProgress(startTime.current ?? 0);
|
||||
setPublicProgress(startTime.current ?? 0);
|
||||
} else {
|
||||
// keep current time when changing between direct and hls.
|
||||
startTime.current = getProgress();
|
||||
|
||||
let newMode = getLocalSetting("playmode", "direct") !== "auto" ? PlayMode.Direct : PlayMode.Hls;
|
||||
// Only allow direct play if the device supports it
|
||||
if (newMode === PlayMode.Direct && codec && !canPlay(codec)) {
|
||||
console.log(`Browser can't natively play ${codec}, switching to hls stream.`);
|
||||
newMode = PlayMode.Hls;
|
||||
}
|
||||
oldLinks.current = links;
|
||||
setPlayMode(newMode);
|
||||
|
||||
setSource((newMode === PlayMode.Direct ? links?.direct : links?.hls) ?? null);
|
||||
setLoad(true);
|
||||
setPrivateProgress(startTime.current ?? 0);
|
||||
setPublicProgress(startTime.current ?? 0);
|
||||
setPlay(true);
|
||||
}, [mode, links, setLoad, setPrivateProgress, setPublicProgress, setPlay, getProgress]);
|
||||
}, [links, codec, setLoad, setPrivateProgress, setPublicProgress, setPlay, setPlayMode]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: do not change source when links change, this is done above
|
||||
useEffect(() => {
|
||||
setSource((mode === PlayMode.Direct ? links?.direct : links?.hls) ?? null);
|
||||
// keep current time when changing between direct and hls.
|
||||
startTime.current = getProgress();
|
||||
setPlay(true);
|
||||
}, [mode, getProgress, setPlay]);
|
||||
|
||||
const account = useAccount();
|
||||
const defaultSubLanguage = account?.settings.subtitleLanguage;
|
||||
|
@ -129,6 +129,9 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
||||
|
||||
export default Video;
|
||||
|
||||
// mobile should be able to play everything
|
||||
export const canPlay = (codec: string) => true;
|
||||
|
||||
type CustomMenu = ComponentProps<typeof Menu<ComponentProps<typeof IconButton>>>;
|
||||
export const AudiosMenu = ({ audios, ...props }: CustomMenu & { audios?: Audio[] }) => {
|
||||
const info = useAtomValue(infoAtom);
|
||||
|
@ -50,7 +50,7 @@ function uuidv4(): string {
|
||||
const client_id = typeof window === "undefined" ? "ssr" : uuidv4();
|
||||
|
||||
const initHls = (): Hls => {
|
||||
if (hls !== null) return hls;
|
||||
if (hls) hls.destroy();
|
||||
const loadPolicy: LoadPolicy = {
|
||||
default: {
|
||||
maxTimeToFirstByteMs: Number.POSITIVE_INFINITY,
|
||||
@ -124,6 +124,8 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const oldHls = useRef<string | null>(null);
|
||||
const { css } = useYoshiki();
|
||||
const errorHandler = useRef<typeof onError>(onError);
|
||||
errorHandler.current = onError;
|
||||
|
||||
useImperativeHandle(
|
||||
forwaredRef,
|
||||
@ -148,13 +150,11 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
||||
const subtitle = useAtomValue(subtitleAtom);
|
||||
useSubtitle(ref, subtitle, fonts);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: onError changes should not restart the playback.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: do not restart on startPosition change
|
||||
useLayoutEffect(() => {
|
||||
if (!ref?.current || !source.uri) return;
|
||||
if (!hls || oldHls.current !== source.hls) {
|
||||
// Reinit the hls player when we change track.
|
||||
if (hls) hls.destroy();
|
||||
hls = null;
|
||||
hls = initHls();
|
||||
hls.loadSource(source.hls!);
|
||||
oldHls.current = source.hls;
|
||||
@ -168,13 +168,21 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
||||
hls.on(Hls.Events.ERROR, (_, d) => {
|
||||
if (!d.fatal || !hls?.media) return;
|
||||
console.warn("Hls error", d);
|
||||
onError?.call(null, {
|
||||
errorHandler.current?.({
|
||||
error: { errorString: d.reason ?? d.error?.message ?? "Unknown hls error" },
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [source.uri, source.hls]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.log("hls cleanup");
|
||||
if (hls) hls.destroy();
|
||||
hls = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const mode = useAtomValue(playModeAtom);
|
||||
const audio = useAtomValue(audioAtom);
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: also change when the mode change
|
||||
@ -244,6 +252,16 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
||||
|
||||
export default Video;
|
||||
|
||||
export const canPlay = (codec: string) => {
|
||||
// most chrome based browser (and safari I think) supports matroska but reports they do not.
|
||||
// for those browsers, only check the codecs and not the container.
|
||||
if (navigator.userAgent.search("Firefox") === -1)
|
||||
codec = codec.replace("video/x-matroska", "video/mp4");
|
||||
const videos = document.getElementsByTagName("video");
|
||||
const video = videos.item(0) ?? document.createElement("video");
|
||||
return !!video.canPlayType(codec);
|
||||
};
|
||||
|
||||
const useSubtitle = (
|
||||
player: RefObject<HTMLVideoElement>,
|
||||
value: Subtitle | null,
|
||||
@ -348,7 +366,7 @@ export const AudiosMenu = ({
|
||||
useEffect(() => {
|
||||
if (!hls) return;
|
||||
hls.on(Hls.Events.AUDIO_TRACK_LOADED, rerender);
|
||||
return () => hls!.off(Hls.Events.AUDIO_TRACK_LOADED, rerender);
|
||||
return () => hls?.off(Hls.Events.AUDIO_TRACK_LOADED, rerender);
|
||||
});
|
||||
|
||||
if (!hls) return <Menu {...props} disabled {...tooltip(t("player.notInPristine"))} />;
|
||||
@ -373,11 +391,14 @@ export const QualitiesMenu = (props: ComponentProps<typeof Menu>) => {
|
||||
const [mode, setPlayMode] = useAtom(playModeAtom);
|
||||
const rerender = useForceRerender();
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Inculde hls in dependency array
|
||||
useEffect(() => {
|
||||
if (!hls) return;
|
||||
// Also rerender when hls instance changes
|
||||
rerender();
|
||||
hls.on(Hls.Events.LEVEL_SWITCHED, rerender);
|
||||
return () => hls!.off(Hls.Events.LEVEL_SWITCHED, rerender);
|
||||
});
|
||||
return () => hls?.off(Hls.Events.LEVEL_SWITCHED, rerender);
|
||||
}, [hls]);
|
||||
|
||||
const levelName = (label: Level, auto?: boolean): string => {
|
||||
const height = `${label.height}p`;
|
||||
|
@ -42,7 +42,8 @@ export const WatchStatusObserver = ({
|
||||
await queryClient.invalidateQueries({ queryKey: [type === "episode" ? "show" : type, slug] }),
|
||||
});
|
||||
const mutate = useCallback(
|
||||
(type: string, slug: string, seconds: number) =>
|
||||
(type: string, slug: string, seconds: number) => {
|
||||
if (seconds < 0 || duration <= 0) return;
|
||||
_mutate({
|
||||
method: "POST",
|
||||
path: [type, slug, "watchStatus"],
|
||||
@ -51,7 +52,8 @@ export const WatchStatusObserver = ({
|
||||
watchedTime: Math.round(seconds),
|
||||
percent: Math.round((seconds / duration) * 100),
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[_mutate, duration],
|
||||
);
|
||||
const readProgress = useAtomCallback(
|
||||
|
@ -55,10 +55,7 @@ export const PlaybackSettings = () => {
|
||||
<Select
|
||||
label={t("settings.playback.playmode.label")}
|
||||
value={playMode}
|
||||
onValueChange={(value) => {
|
||||
setDefaultPlayMode(value);
|
||||
setCurrentPlayMode(value === "direct" ? PlayMode.Direct : PlayMode.Hls);
|
||||
}}
|
||||
onValueChange={(value) => setDefaultPlayMode(value)}
|
||||
values={["direct", "auto"]}
|
||||
getLabel={(key) => t(`player.${key}`)}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ package src
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/zoriya/go-mediainfo"
|
||||
@ -118,11 +119,16 @@ func GetMimeCodec(mi *mediainfo.File, kind mediainfo.StreamKind, i int) *string
|
||||
ret := "mp4a.a5"
|
||||
return &ret
|
||||
|
||||
case "audio/eac3", "E-AC-3":
|
||||
ret := "mp4a.a6"
|
||||
return &ret
|
||||
|
||||
case "audio/x-flac", "FLAC":
|
||||
ret := "fLaC"
|
||||
return &ret
|
||||
|
||||
default:
|
||||
log.Printf("No known mime format for: %s", codec)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -23,6 +24,8 @@ type MediaInfo struct {
|
||||
Path string `json:"path"`
|
||||
/// The extension currently used to store this video file
|
||||
Extension string `json:"extension"`
|
||||
/// The whole mimetype (defined as the RFC 6381). ex: `video/mp4; codecs="avc1.640028, mp4a.40.2"`
|
||||
MimeCodec *string `json:"mimeCodec"`
|
||||
/// The file size of the video file.
|
||||
Size uint64 `json:"size"`
|
||||
/// The length of the media in seconds.
|
||||
@ -311,6 +314,24 @@ func getInfo(path string) (*MediaInfo, error) {
|
||||
return fmt.Sprintf("%s/%s/attachment/%s", Settings.RoutePrefix, base64.StdEncoding.EncodeToString([]byte(path)), font)
|
||||
}),
|
||||
}
|
||||
var codecs []string
|
||||
if len(ret.Videos) > 0 && ret.Videos[0].MimeCodec != nil {
|
||||
codecs = append(codecs, *ret.Videos[0].MimeCodec)
|
||||
}
|
||||
if len(ret.Audios) > 0 && ret.Audios[0].MimeCodec != nil {
|
||||
codecs = append(codecs, *ret.Audios[0].MimeCodec)
|
||||
}
|
||||
container := mime.TypeByExtension(fmt.Sprintf(".%s", ret.Extension))
|
||||
if container != "" {
|
||||
if len(codecs) > 0 {
|
||||
codecs_str := strings.Join(codecs, ", ")
|
||||
mime := fmt.Sprintf("%s; codecs=\"%s\"", container, codecs_str)
|
||||
ret.MimeCodec = &mime
|
||||
} else {
|
||||
ret.MimeCodec = &container
|
||||
}
|
||||
}
|
||||
|
||||
if len(ret.Videos) > 0 {
|
||||
ret.Video = &ret.Videos[0]
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ var safe_path = src.GetEnvOr("GOCODER_SAFE_PATH", "/video")
|
||||
// Encode the version in the hash path to update cached values.
|
||||
// Older versions won't be deleted (needed to allow multiples versions of the transcoder to run at the same time)
|
||||
// If the version changes a lot, we might want to automatically delete older versions.
|
||||
var version = "v1."
|
||||
var version = "v2-"
|
||||
|
||||
func GetPath(c echo.Context) (string, string, error) {
|
||||
key := c.Param("path")
|
||||
|
Loading…
x
Reference in New Issue
Block a user