feat: KSPlayer as an option for iOS + other improvements (#1266)

This commit is contained in:
Fredrik Burmester
2026-01-03 13:05:50 +01:00
committed by GitHub
parent d1795c9df8
commit 74d86b5d12
191 changed files with 88479 additions and 2316 deletions

View File

@@ -0,0 +1,105 @@
import type { StyleProp, ViewStyle } from "react-native";
export type OnLoadEventPayload = {
url: string;
};
export type OnPlaybackStateChangePayload = {
isPaused?: boolean;
isPlaying?: boolean;
isLoading?: boolean;
isReadyToSeek?: boolean;
};
export type OnProgressEventPayload = {
position: number;
duration: number;
progress: number;
};
export type OnErrorEventPayload = {
error: string;
};
export type OnTracksReadyEventPayload = Record<string, never>;
export type MpvPlayerModuleEvents = {
onChange: (params: ChangeEventPayload) => void;
};
export type ChangeEventPayload = {
value: string;
};
export type VideoSource = {
url: string;
headers?: Record<string, string>;
externalSubtitles?: string[];
startPosition?: number;
autoplay?: boolean;
/** MPV subtitle track ID to select on start (1-based, -1 to disable) */
initialSubtitleId?: number;
/** MPV audio track ID to select on start (1-based) */
initialAudioId?: number;
};
export type MpvPlayerViewProps = {
source?: VideoSource;
style?: StyleProp<ViewStyle>;
onLoad?: (event: { nativeEvent: OnLoadEventPayload }) => void;
onPlaybackStateChange?: (event: {
nativeEvent: OnPlaybackStateChangePayload;
}) => void;
onProgress?: (event: { nativeEvent: OnProgressEventPayload }) => void;
onError?: (event: { nativeEvent: OnErrorEventPayload }) => void;
onTracksReady?: (event: { nativeEvent: OnTracksReadyEventPayload }) => void;
};
export interface MpvPlayerViewRef {
play: () => Promise<void>;
pause: () => Promise<void>;
seekTo: (position: number) => Promise<void>;
seekBy: (offset: number) => Promise<void>;
setSpeed: (speed: number) => Promise<void>;
getSpeed: () => Promise<number>;
isPaused: () => Promise<boolean>;
getCurrentPosition: () => Promise<number>;
getDuration: () => Promise<number>;
startPictureInPicture: () => Promise<void>;
stopPictureInPicture: () => Promise<void>;
isPictureInPictureSupported: () => Promise<boolean>;
isPictureInPictureActive: () => Promise<boolean>;
// Subtitle controls
getSubtitleTracks: () => Promise<SubtitleTrack[]>;
setSubtitleTrack: (trackId: number) => Promise<void>;
disableSubtitles: () => Promise<void>;
getCurrentSubtitleTrack: () => Promise<number>;
addSubtitleFile: (url: string, select?: boolean) => Promise<void>;
// Subtitle positioning
setSubtitlePosition: (position: number) => Promise<void>;
setSubtitleScale: (scale: number) => Promise<void>;
setSubtitleMarginY: (margin: number) => Promise<void>;
setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise<void>;
setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => Promise<void>;
setSubtitleFontSize: (size: number) => Promise<void>;
// Audio controls
getAudioTracks: () => Promise<AudioTrack[]>;
setAudioTrack: (trackId: number) => Promise<void>;
getCurrentAudioTrack: () => Promise<number>;
}
export type SubtitleTrack = {
id: number;
title?: string;
lang?: string;
selected?: boolean;
};
export type AudioTrack = {
id: number;
title?: string;
lang?: string;
codec?: string;
channels?: number;
selected?: boolean;
};

View File

@@ -0,0 +1,11 @@
import { NativeModule, requireNativeModule } from "expo";
import { MpvPlayerModuleEvents } from "./MpvPlayer.types";
declare class MpvPlayerModule extends NativeModule<MpvPlayerModuleEvents> {
hello(): string;
setValueAsync(value: string): Promise<void>;
}
// This call loads the native module object from the JSI.
export default requireNativeModule<MpvPlayerModule>("MpvPlayer");

View File

@@ -0,0 +1,19 @@
import { NativeModule, registerWebModule } from "expo";
import { ChangeEventPayload } from "./MpvPlayer.types";
type MpvPlayerModuleEvents = {
onChange: (params: ChangeEventPayload) => void;
};
class MpvPlayerModule extends NativeModule<MpvPlayerModuleEvents> {
PI = Math.PI;
async setValueAsync(value: string): Promise<void> {
this.emit("onChange", { value });
}
hello() {
return "Hello world! 👋";
}
}
export default registerWebModule(MpvPlayerModule, "MpvPlayerModule");

View File

@@ -0,0 +1,101 @@
import { requireNativeView } from "expo";
import * as React from "react";
import { useImperativeHandle, useRef } from "react";
import { MpvPlayerViewProps, MpvPlayerViewRef } from "./MpvPlayer.types";
const NativeView: React.ComponentType<MpvPlayerViewProps & { ref?: any }> =
requireNativeView("MpvPlayer");
export default React.forwardRef<MpvPlayerViewRef, MpvPlayerViewProps>(
function MpvPlayerView(props, ref) {
const nativeRef = useRef<any>(null);
useImperativeHandle(ref, () => ({
play: async () => {
await nativeRef.current?.play();
},
pause: async () => {
await nativeRef.current?.pause();
},
seekTo: async (position: number) => {
await nativeRef.current?.seekTo(position);
},
seekBy: async (offset: number) => {
await nativeRef.current?.seekBy(offset);
},
setSpeed: async (speed: number) => {
await nativeRef.current?.setSpeed(speed);
},
getSpeed: async () => {
return await nativeRef.current?.getSpeed();
},
isPaused: async () => {
return await nativeRef.current?.isPaused();
},
getCurrentPosition: async () => {
return await nativeRef.current?.getCurrentPosition();
},
getDuration: async () => {
return await nativeRef.current?.getDuration();
},
startPictureInPicture: async () => {
await nativeRef.current?.startPictureInPicture();
},
stopPictureInPicture: async () => {
await nativeRef.current?.stopPictureInPicture();
},
isPictureInPictureSupported: async () => {
return await nativeRef.current?.isPictureInPictureSupported();
},
isPictureInPictureActive: async () => {
return await nativeRef.current?.isPictureInPictureActive();
},
getSubtitleTracks: async () => {
return await nativeRef.current?.getSubtitleTracks();
},
setSubtitleTrack: async (trackId: number) => {
await nativeRef.current?.setSubtitleTrack(trackId);
},
disableSubtitles: async () => {
await nativeRef.current?.disableSubtitles();
},
getCurrentSubtitleTrack: async () => {
return await nativeRef.current?.getCurrentSubtitleTrack();
},
addSubtitleFile: async (url: string, select = true) => {
await nativeRef.current?.addSubtitleFile(url, select);
},
setSubtitlePosition: async (position: number) => {
await nativeRef.current?.setSubtitlePosition(position);
},
setSubtitleScale: async (scale: number) => {
await nativeRef.current?.setSubtitleScale(scale);
},
setSubtitleMarginY: async (margin: number) => {
await nativeRef.current?.setSubtitleMarginY(margin);
},
setSubtitleAlignX: async (alignment: "left" | "center" | "right") => {
await nativeRef.current?.setSubtitleAlignX(alignment);
},
setSubtitleAlignY: async (alignment: "top" | "center" | "bottom") => {
await nativeRef.current?.setSubtitleAlignY(alignment);
},
setSubtitleFontSize: async (size: number) => {
await nativeRef.current?.setSubtitleFontSize(size);
},
// Audio controls
getAudioTracks: async () => {
return await nativeRef.current?.getAudioTracks();
},
setAudioTrack: async (trackId: number) => {
await nativeRef.current?.setAudioTrack(trackId);
},
getCurrentAudioTrack: async () => {
return await nativeRef.current?.getCurrentAudioTrack();
},
}));
return <NativeView ref={nativeRef} {...props} />;
},
);

View File

@@ -0,0 +1,15 @@
import { MpvPlayerViewProps } from "./MpvPlayer.types";
export default function MpvPlayerView(props: MpvPlayerViewProps) {
const url = props.source?.url;
return (
<div>
<iframe
title='MPV Player'
style={{ flex: 1 }}
src={url}
onLoad={() => props.onLoad?.({ nativeEvent: { url: url ?? "" } })}
/>
</div>
);
}

View File

@@ -0,0 +1,3 @@
export * from "./MpvPlayer.types";
export { default as MpvPlayerModule } from "./MpvPlayerModule";
export { default as MpvPlayerView } from "./MpvPlayerView";