mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-25 07:45:56 +00:00
feat: add drm interfaces (#4657)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@react-native', '../config/.eslintrc.js'],
|
||||
extends: ["../config/.eslintrc.js"],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ PODS:
|
||||
- hermes-engine (0.77.2):
|
||||
- hermes-engine/Pre-built (= 0.77.2)
|
||||
- hermes-engine/Pre-built (0.77.2)
|
||||
- NitroModules (0.27.2):
|
||||
- NitroModules (0.28.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1565,7 +1565,7 @@ PODS:
|
||||
- React-logger (= 0.77.2)
|
||||
- React-perflogger (= 0.77.2)
|
||||
- React-utils (= 0.77.2)
|
||||
- ReactNativeVideo (7.0.0-alpha.2):
|
||||
- ReactNativeVideo (7.0.0-alpha.3):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1587,6 +1587,31 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- ReactNativeVideoDrm (0.1.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- NitroModules
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-callinvoker
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- ReactNativeVideo
|
||||
- Yoga
|
||||
- SocketRocket (0.7.1)
|
||||
- Yoga (0.0.0)
|
||||
|
||||
@@ -1661,6 +1686,7 @@ DEPENDENCIES:
|
||||
- ReactCodegen (from `build/generated/ios`)
|
||||
- ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`)
|
||||
- ReactNativeVideo (from `../../node_modules/react-native-video`)
|
||||
- "ReactNativeVideoDrm (from `../../node_modules/@twg/react-native-video-drm`)"
|
||||
- Yoga (from `../../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -1805,6 +1831,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/react-native/ReactCommon"
|
||||
ReactNativeVideo:
|
||||
:path: "../../node_modules/react-native-video"
|
||||
ReactNativeVideoDrm:
|
||||
:path: "../../node_modules/@twg/react-native-video-drm"
|
||||
Yoga:
|
||||
:path: "../../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
@@ -1816,7 +1844,7 @@ SPEC CHECKSUMS:
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
|
||||
hermes-engine: 8eb265241fa1d7095d3a40d51fd90f7dce68217c
|
||||
NitroModules: 7ed5fd8f6f1e814810b9df26830b78f3355690bf
|
||||
NitroModules: 1e4150c3e3676e05209234a8a5e0e8886fc0311a
|
||||
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
|
||||
RCTDeprecation: 85b72250b63cfb54f29ca96ceb108cb9ef3c2079
|
||||
RCTRequired: 567cb8f5d42b990331bfd93faad1d8999b1c1736
|
||||
@@ -1876,7 +1904,8 @@ SPEC CHECKSUMS:
|
||||
ReactAppDependencyProvider: f334cebc0beed0a72490492e978007082c03d533
|
||||
ReactCodegen: 474fbb3e4bb0f1ee6c255d1955db76e13d509269
|
||||
ReactCommon: 7763e59534d58e15f8f22121cdfe319040e08888
|
||||
ReactNativeVideo: db800650d21aab71c457ea1408c4f46e853b5886
|
||||
ReactNativeVideo: 213235288864ce876c68a64cc1481fe8a3eae5d5
|
||||
ReactNativeVideoDrm: 62840ae0e184f711a2e6495c18e342a74cb598f8
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Yoga: 31a098f74c16780569aebd614a0f37a907de0189
|
||||
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc",
|
||||
"start": "react-native start"
|
||||
"start": "react-native start --client-logs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-community/slider": "^4.5.6",
|
||||
"react": "18.3.1",
|
||||
"react-native": "^0.77.0",
|
||||
"react-native-nitro-modules": "^0.27.0",
|
||||
"react-native-video": "*"
|
||||
"react-native-nitro-modules": "^0.28.0",
|
||||
"react-native-video": "*",
|
||||
"@twg/react-native-video-drm": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
@@ -28,10 +29,8 @@
|
||||
"@react-native/metro-config": "^0.77.0",
|
||||
"@react-native/typescript-config": "^0.77.0",
|
||||
"@types/react": "^18.2.44",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"eslint": "^8.51.0",
|
||||
"prettier": "^3.0.3",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+36
-627
@@ -1,67 +1,28 @@
|
||||
import React from 'react';
|
||||
import { Alert, SafeAreaView, ScrollView, Text, View } from 'react-native';
|
||||
import { styles } from './styles';
|
||||
import {
|
||||
ActionButton,
|
||||
ControlButton,
|
||||
SwitchControl,
|
||||
ToggleButton,
|
||||
} from './components/Controls';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {
|
||||
useEvent,
|
||||
useVideoPlayer,
|
||||
VideoPlayer,
|
||||
VideoView,
|
||||
type IgnoreSilentSwitchMode,
|
||||
type MixAudioMode,
|
||||
type VideoViewRef,
|
||||
type onLoadData,
|
||||
type onProgressData,
|
||||
type onVolumeChangeData,
|
||||
type ResizeMode,
|
||||
type TextTrack,
|
||||
type VideoConfig,
|
||||
type VideoPlayerStatus,
|
||||
type VideoViewRef,
|
||||
type onVolumeChangeData,
|
||||
useVideoPlayer,
|
||||
useEvent,
|
||||
VideoView,
|
||||
type VideoConfig,
|
||||
} from 'react-native-video';
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
if (isNaN(seconds)) return '--:--';
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
||||
};
|
||||
|
||||
// Consolidated state interface
|
||||
interface VideoSettings {
|
||||
show: boolean;
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
rate: number;
|
||||
loop: boolean;
|
||||
showNativeControls: boolean;
|
||||
resizeMode: ResizeMode;
|
||||
mixAudioMode: MixAudioMode;
|
||||
ignoreSilentSwitchMode: IgnoreSilentSwitchMode;
|
||||
playInBackground: boolean;
|
||||
playWhenInactive: boolean;
|
||||
}
|
||||
|
||||
const defaultSettings: VideoSettings = {
|
||||
show: false,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
rate: 1,
|
||||
loop: false,
|
||||
showNativeControls: false,
|
||||
resizeMode: 'contain',
|
||||
mixAudioMode: 'auto',
|
||||
ignoreSilentSwitchMode: 'auto',
|
||||
playInBackground: true,
|
||||
playWhenInactive: false,
|
||||
};
|
||||
import TextTrackManager from './components/TextTrackManager';
|
||||
import { type VideoSettings, defaultSettings } from './types/videoSettings';
|
||||
import { formatTime } from './utils/time';
|
||||
import { getVideoSource } from './utils/videoSource';
|
||||
|
||||
const VideoDemo = () => {
|
||||
const videoViewRef = React.useRef<VideoViewRef>(null);
|
||||
@@ -72,7 +33,6 @@ const VideoDemo = () => {
|
||||
Array<{ id: string; message: string; timestamp: string }>
|
||||
>([]);
|
||||
|
||||
// Helper function to update settings
|
||||
const updateSetting = <K extends keyof VideoSettings>(
|
||||
key: K,
|
||||
value: VideoSettings[K]
|
||||
@@ -80,17 +40,15 @@ const VideoDemo = () => {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
// Helper function to add events
|
||||
const addEvent = React.useCallback((message: string) => {
|
||||
const newEvent = {
|
||||
id: `${Date.now()}-${Math.random()}`,
|
||||
message,
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
};
|
||||
setEvents((prev) => [newEvent, ...prev.slice(0, 49)]); // Keep latest 50 events
|
||||
setEvents((prev) => [newEvent, ...prev.slice(0, 49)]);
|
||||
}, []);
|
||||
|
||||
// Event handlers
|
||||
const handleFullscreenChange = React.useCallback(
|
||||
(fullscreen: boolean) => {
|
||||
addEvent(
|
||||
@@ -161,20 +119,9 @@ const VideoDemo = () => {
|
||||
[addEvent]
|
||||
);
|
||||
|
||||
// Setup player
|
||||
const player = useVideoPlayer(
|
||||
{
|
||||
uri: 'https://www.w3schools.com/html/movie.mp4',
|
||||
externalSubtitles: [
|
||||
{
|
||||
uri: 'https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt',
|
||||
label: 'External',
|
||||
},
|
||||
],
|
||||
},
|
||||
(_player) => {
|
||||
// Setup player
|
||||
}
|
||||
getVideoSource(defaultSettings.videoType),
|
||||
(_player) => {}
|
||||
);
|
||||
|
||||
useEvent(player, 'onEnd', handlePlayerEnd);
|
||||
@@ -186,7 +133,6 @@ const VideoDemo = () => {
|
||||
useEvent(player, 'onPlaybackStateChange', handlePlayerStateChange);
|
||||
useEvent(player, 'onVolumeChange', handleVolumeChange);
|
||||
|
||||
// Sync settings with player
|
||||
React.useEffect(() => {
|
||||
if (!settings.show) return;
|
||||
|
||||
@@ -207,7 +153,6 @@ const VideoDemo = () => {
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
{/* Video Player */}
|
||||
<View style={styles.videoContainer}>
|
||||
{settings.show ? (
|
||||
<VideoView
|
||||
@@ -228,7 +173,6 @@ const VideoDemo = () => {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Progress Controls */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.progressRow}>
|
||||
<Text style={styles.timeText}>{formatTime(progress)}</Text>
|
||||
@@ -250,7 +194,6 @@ const VideoDemo = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Transport Controls */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.transportRow}>
|
||||
<ControlButton icon="⏮" onPress={() => player.seekTo(0)} />
|
||||
@@ -262,9 +205,22 @@ const VideoDemo = () => {
|
||||
/>
|
||||
<ControlButton icon="⏩" onPress={() => player.seekBy(10)} />
|
||||
</View>
|
||||
<Text style={styles.subSectionTitle}>Video Type</Text>
|
||||
<View style={styles.buttonGroup}>
|
||||
{(['hls', 'mp4', 'drm'] as const).map((mode) => (
|
||||
<ToggleButton
|
||||
key={mode}
|
||||
label={mode}
|
||||
active={settings.videoType === mode}
|
||||
onPress={async () => {
|
||||
updateSetting('videoType', mode);
|
||||
await player.replaceSourceAsync(getVideoSource(mode));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Display Settings */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Display Settings</Text>
|
||||
|
||||
@@ -296,7 +252,6 @@ const VideoDemo = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Audio Controls */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Audio Controls</Text>
|
||||
<View style={styles.audioControls}>
|
||||
@@ -349,7 +304,6 @@ const VideoDemo = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Background Playback Controls */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Background Playback</Text>
|
||||
<View style={styles.switchColumn}>
|
||||
@@ -366,7 +320,6 @@ const VideoDemo = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Audio Settings */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Audio Settings</Text>
|
||||
|
||||
@@ -397,13 +350,11 @@ const VideoDemo = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Text Track Controls */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Text Tracks</Text>
|
||||
<TextTrackManager player={player} />
|
||||
</View>
|
||||
|
||||
{/* Advanced Controls */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Advanced Controls</Text>
|
||||
|
||||
@@ -471,7 +422,6 @@ const VideoDemo = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Event Log */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Event Log</Text>
|
||||
<View style={styles.eventLog}>
|
||||
@@ -496,237 +446,6 @@ const VideoDemo = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Reusable Components
|
||||
const ControlButton = ({
|
||||
icon,
|
||||
onPress,
|
||||
size = 'normal',
|
||||
}: {
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
size?: 'normal' | 'large';
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.controlButton,
|
||||
size === 'large' && styles.controlButtonLarge,
|
||||
]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={[styles.controlIcon, size === 'large' && styles.controlIconLarge]}
|
||||
>
|
||||
{icon}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const SwitchControl = ({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onValueChange: (value: boolean) => void;
|
||||
}) => (
|
||||
<View style={styles.switchControl}>
|
||||
<Text style={styles.switchLabel}>{label}</Text>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ false: '#e1e1e1', true: '#007aff' }}
|
||||
thumbColor={value ? '#ffffff' : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const ToggleButton = ({
|
||||
label,
|
||||
active,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
active: boolean;
|
||||
onPress: () => void;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.toggleButton, active && styles.toggleButtonActive]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={[styles.toggleButtonText, active && styles.toggleButtonTextActive]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const ActionButton = ({
|
||||
label,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
}) => (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={onPress}>
|
||||
<Text style={styles.actionButtonText}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const TextTrackManager = ({ player }: { player: VideoPlayer }) => {
|
||||
const [textTracks, setTextTracks] = React.useState<TextTrack[]>([]);
|
||||
const [selectedTrackId, setSelectedTrackId] = React.useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [currentSelectedTrack, setCurrentSelectedTrack] =
|
||||
React.useState<TextTrack | null>(null);
|
||||
const [trackChangeEvents, setTrackChangeEvents] = React.useState<string[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const loadTextTracks = React.useCallback(() => {
|
||||
try {
|
||||
const tracks = player.getAvailableTextTracks();
|
||||
setTextTracks(tracks);
|
||||
|
||||
// Get currently selected track using the new property
|
||||
const selectedTrack = player.selectedTrack;
|
||||
setSelectedTrackId(selectedTrack?.id || null);
|
||||
setCurrentSelectedTrack(selectedTrack || null);
|
||||
|
||||
console.log('Available text tracks:', tracks);
|
||||
console.log('Currently selected track:', selectedTrack);
|
||||
} catch (error) {
|
||||
console.error('Error loading text tracks:', error);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
const selectTrack = React.useCallback(
|
||||
(track: TextTrack) => {
|
||||
try {
|
||||
player.selectTextTrack(track);
|
||||
console.log('Selected text track:', track);
|
||||
// onTrackChange event will update the state automatically
|
||||
} catch (error) {
|
||||
console.error('Error selecting text track:', error);
|
||||
}
|
||||
},
|
||||
[player]
|
||||
);
|
||||
|
||||
const disableTextTracks = React.useCallback(() => {
|
||||
try {
|
||||
// Pass null to disable text tracks
|
||||
player.selectTextTrack(null);
|
||||
console.log('Disabled text tracks');
|
||||
// onTrackChange event will update the state automatically
|
||||
} catch (error) {
|
||||
console.error('Error disabling text tracks:', error);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
useEvent(player, 'onReadyToDisplay', () => {
|
||||
loadTextTracks();
|
||||
});
|
||||
|
||||
useEvent(player, 'onTrackChange', (track) => {
|
||||
// Update state when track changes through any means (API or native controls)
|
||||
setCurrentSelectedTrack(track);
|
||||
setSelectedTrackId(track?.id || null);
|
||||
|
||||
// Add to event log
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const eventMessage = track
|
||||
? `${timestamp}: Track changed to "${track.label}" (${track.id})${track.id.startsWith('external-') ? ' [External]' : ''}`
|
||||
: `${timestamp}: All tracks disabled`;
|
||||
|
||||
setTrackChangeEvents((prev) => [eventMessage, ...prev.slice(0, 4)]); // Keep last 5 events
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.buttonGroup}>
|
||||
<ActionButton label="Refresh Tracks" onPress={loadTextTracks} />
|
||||
<ActionButton label="Disable Tracks" onPress={disableTextTracks} />
|
||||
<ActionButton
|
||||
label="Sync Selection"
|
||||
onPress={() => {
|
||||
try {
|
||||
const selectedTrack = player.selectedTrack;
|
||||
setCurrentSelectedTrack(selectedTrack || null);
|
||||
setSelectedTrackId(selectedTrack?.id || null);
|
||||
} catch (error) {
|
||||
console.error('Error syncing selection:', error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{currentSelectedTrack && (
|
||||
<View style={styles.selectedTrackInfo}>
|
||||
<Text style={styles.selectedTrackLabel}>Currently Selected:</Text>
|
||||
<Text style={styles.selectedTrackText}>
|
||||
{currentSelectedTrack.label}
|
||||
{currentSelectedTrack.language &&
|
||||
` (${currentSelectedTrack.language})`}
|
||||
{currentSelectedTrack.id.startsWith('external-') && ' [External]'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{trackChangeEvents.length > 0 && (
|
||||
<View style={styles.eventLogContainer}>
|
||||
<Text style={styles.eventLogTitle}>Track Change Events:</Text>
|
||||
{trackChangeEvents.map((event, index) => (
|
||||
<Text key={index} style={styles.eventLogText}>
|
||||
{event}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{textTracks.length > 0 ? (
|
||||
<View style={styles.trackList}>
|
||||
<Text style={styles.subSectionTitle}>
|
||||
Available Tracks ({textTracks.length})
|
||||
</Text>
|
||||
{textTracks.map((track) => (
|
||||
<TouchableOpacity
|
||||
key={track.id}
|
||||
style={[
|
||||
styles.trackButton,
|
||||
selectedTrackId === track.id && styles.trackButtonSelected,
|
||||
]}
|
||||
onPress={() => selectTrack(track)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.trackButtonText,
|
||||
selectedTrackId === track.id &&
|
||||
styles.trackButtonTextSelected,
|
||||
]}
|
||||
>
|
||||
{track.label} {track.language && `(${track.language})`}
|
||||
{track.selected && ' ✓'}
|
||||
{track.id.startsWith('external-') && ' [External]'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<Text style={styles.noTracksText}>No text tracks available</Text>
|
||||
<Text style={styles.noTracksSubText}>
|
||||
Make sure the video is loaded. External subtitles are loaded but not
|
||||
automatically enabled - you need to select them manually.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [mounted, setMounted] = React.useState(true);
|
||||
|
||||
@@ -742,313 +461,3 @@ export default function App() {
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
app: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
videoContainer: {
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 9,
|
||||
backgroundColor: '#000',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 16,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
hiddenVideo: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#333',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
hiddenVideoText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
section: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
elevation: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1a1a1a',
|
||||
marginBottom: 12,
|
||||
},
|
||||
subSectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
progressRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
progressSlider: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
width: 50,
|
||||
textAlign: 'center',
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
transportRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
controlButton: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: '#f0f0f0',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
controlButtonLarge: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: '#007aff',
|
||||
},
|
||||
controlIcon: {
|
||||
fontSize: 20,
|
||||
color: '#333',
|
||||
},
|
||||
controlIconLarge: {
|
||||
fontSize: 28,
|
||||
color: '#fff',
|
||||
},
|
||||
audioControls: {
|
||||
gap: 16,
|
||||
},
|
||||
sliderControl: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
controlLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
width: 60,
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
},
|
||||
valueText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
width: 40,
|
||||
textAlign: 'right',
|
||||
},
|
||||
switchRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 20,
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
switchColumn: {
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
},
|
||||
switchControl: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
},
|
||||
switchLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
},
|
||||
buttonGroup: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
},
|
||||
toggleButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
},
|
||||
toggleButtonActive: {
|
||||
backgroundColor: '#007aff',
|
||||
borderColor: '#007aff',
|
||||
},
|
||||
toggleButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#666',
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
toggleButtonTextActive: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
actionGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
},
|
||||
actionButton: {
|
||||
backgroundColor: '#007aff',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
actionButtonText: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
},
|
||||
eventLog: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#007aff',
|
||||
height: 200,
|
||||
},
|
||||
eventLogScroll: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
},
|
||||
eventItem: {
|
||||
paddingVertical: 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e1e1e1',
|
||||
},
|
||||
eventTime: {
|
||||
fontSize: 10,
|
||||
color: '#999',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 2,
|
||||
},
|
||||
eventText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
mountControl: {
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
trackList: {
|
||||
marginTop: 12,
|
||||
},
|
||||
trackButton: {
|
||||
padding: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
trackButtonSelected: {
|
||||
backgroundColor: '#007aff',
|
||||
borderColor: '#007aff',
|
||||
},
|
||||
trackButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
trackButtonTextSelected: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
noTracksText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
noTracksSubText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
textAlign: 'center',
|
||||
},
|
||||
selectedTrackInfo: {
|
||||
backgroundColor: '#e7f4ff',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#007aff',
|
||||
marginBottom: 12,
|
||||
},
|
||||
selectedTrackLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginBottom: 4,
|
||||
},
|
||||
selectedTrackText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#007aff',
|
||||
},
|
||||
eventLogContainer: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#28a745',
|
||||
marginBottom: 12,
|
||||
},
|
||||
eventLogTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginBottom: 8,
|
||||
},
|
||||
eventLogText: {
|
||||
fontSize: 11,
|
||||
color: '#333',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { styles } from '../styles';
|
||||
|
||||
export const ControlButton = ({
|
||||
icon,
|
||||
onPress,
|
||||
size = 'normal',
|
||||
}: {
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
size?: 'normal' | 'large';
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.controlButton,
|
||||
size === 'large' && styles.controlButtonLarge,
|
||||
]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={[styles.controlIcon, size === 'large' && styles.controlIconLarge]}
|
||||
>
|
||||
{icon}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
export const SwitchControl = ({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onValueChange: (value: boolean) => void;
|
||||
}) => (
|
||||
<View style={styles.switchControl}>
|
||||
<Text style={styles.switchLabel}>{label}</Text>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ false: '#e1e1e1', true: '#007aff' }}
|
||||
thumbColor={value ? '#ffffff' : '#f4f3f4'}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleButton = ({
|
||||
label,
|
||||
active,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
active: boolean;
|
||||
onPress: () => void;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.toggleButton, active && styles.toggleButtonActive]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text
|
||||
style={[styles.toggleButtonText, active && styles.toggleButtonTextActive]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
export const ActionButton = ({
|
||||
label,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
}) => (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={onPress}>
|
||||
<Text style={styles.actionButtonText}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -0,0 +1,149 @@
|
||||
import React from 'react';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useEvent, type TextTrack, type VideoPlayer } from 'react-native-video';
|
||||
import { styles } from '../styles';
|
||||
import { ActionButton } from './Controls';
|
||||
|
||||
export const TextTrackManager = ({ player }: { player: VideoPlayer }) => {
|
||||
const [textTracks, setTextTracks] = React.useState<TextTrack[]>([]);
|
||||
const [selectedTrackId, setSelectedTrackId] = React.useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [currentSelectedTrack, setCurrentSelectedTrack] =
|
||||
React.useState<TextTrack | null>(null);
|
||||
const [trackChangeEvents, setTrackChangeEvents] = React.useState<string[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const loadTextTracks = React.useCallback(() => {
|
||||
try {
|
||||
const tracks = player.getAvailableTextTracks();
|
||||
setTextTracks(tracks);
|
||||
|
||||
const selectedTrack = player.selectedTrack;
|
||||
setSelectedTrackId(selectedTrack?.id || null);
|
||||
setCurrentSelectedTrack(selectedTrack || null);
|
||||
} catch (error) {
|
||||
console.error('Error loading text tracks:', error);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
const selectTrack = React.useCallback(
|
||||
(track: TextTrack) => {
|
||||
try {
|
||||
player.selectTextTrack(track);
|
||||
} catch (error) {
|
||||
console.error('Error selecting text track:', error);
|
||||
}
|
||||
},
|
||||
[player]
|
||||
);
|
||||
|
||||
const disableTextTracks = React.useCallback(() => {
|
||||
try {
|
||||
player.selectTextTrack(null);
|
||||
} catch (error) {
|
||||
console.error('Error disabling text tracks:', error);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
useEvent(player, 'onReadyToDisplay', () => {
|
||||
loadTextTracks();
|
||||
});
|
||||
|
||||
useEvent(player, 'onTrackChange', (track) => {
|
||||
setCurrentSelectedTrack(track);
|
||||
setSelectedTrackId(track?.id || null);
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const eventMessage = track
|
||||
? `${timestamp}: Track changed to "${track.label}" (${track.id})${track.id.startsWith('external-') ? ' [External]' : ''}`
|
||||
: `${timestamp}: All tracks disabled`;
|
||||
|
||||
setTrackChangeEvents((prev) => [eventMessage, ...prev.slice(0, 4)]);
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.buttonGroup}>
|
||||
<ActionButton label="Refresh Tracks" onPress={loadTextTracks} />
|
||||
<ActionButton label="Disable Tracks" onPress={disableTextTracks} />
|
||||
<ActionButton
|
||||
label="Sync Selection"
|
||||
onPress={() => {
|
||||
try {
|
||||
const selectedTrack = player.selectedTrack;
|
||||
setCurrentSelectedTrack(selectedTrack || null);
|
||||
setSelectedTrackId(selectedTrack?.id || null);
|
||||
} catch (error) {
|
||||
console.error('Error syncing selection:', error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{currentSelectedTrack && (
|
||||
<View style={styles.selectedTrackInfo}>
|
||||
<Text style={styles.selectedTrackLabel}>Currently Selected:</Text>
|
||||
<Text style={styles.selectedTrackText}>
|
||||
{currentSelectedTrack.label}
|
||||
{currentSelectedTrack.language &&
|
||||
` (${currentSelectedTrack.language})`}
|
||||
{currentSelectedTrack.id.startsWith('external-') && ' [External]'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{trackChangeEvents.length > 0 && (
|
||||
<View style={styles.eventLogContainer}>
|
||||
<Text style={styles.eventLogTitle}>Track Change Events:</Text>
|
||||
{trackChangeEvents.map((event, index) => (
|
||||
<Text key={index} style={styles.eventLogText}>
|
||||
{event}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{textTracks.length > 0 ? (
|
||||
<View style={styles.trackList}>
|
||||
<Text style={styles.subSectionTitle}>
|
||||
Available Tracks ({textTracks.length})
|
||||
</Text>
|
||||
{textTracks.map((track) => (
|
||||
<TouchableOpacity
|
||||
key={track.id}
|
||||
style={[
|
||||
styles.trackButton,
|
||||
selectedTrackId === track.id && styles.trackButtonSelected,
|
||||
]}
|
||||
onPress={() => selectTrack(track)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.trackButtonText,
|
||||
selectedTrackId === track.id &&
|
||||
styles.trackButtonTextSelected,
|
||||
]}
|
||||
>
|
||||
{track.label} {track.language && `(${track.language})`}
|
||||
{track.selected && ' ✓'}
|
||||
{track.id.startsWith('external-') && ' [External]'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View>
|
||||
<Text style={styles.noTracksText}>No text tracks available</Text>
|
||||
<Text style={styles.noTracksSubText}>
|
||||
Make sure the video is loaded. External subtitles are loaded but not
|
||||
automatically enabled - you need to select them manually.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextTrackManager;
|
||||
@@ -0,0 +1,311 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
app: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8f9fa',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
videoContainer: {
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 9,
|
||||
backgroundColor: '#000',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 16,
|
||||
elevation: 3,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
hiddenVideo: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#333',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
hiddenVideoText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
section: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
elevation: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1a1a1a',
|
||||
marginBottom: 12,
|
||||
},
|
||||
subSectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
progressRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
progressSlider: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
width: 50,
|
||||
textAlign: 'center',
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
transportRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
controlButton: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: '#f0f0f0',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
controlButtonLarge: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: '#007aff',
|
||||
},
|
||||
controlIcon: {
|
||||
fontSize: 20,
|
||||
color: '#333',
|
||||
},
|
||||
controlIconLarge: {
|
||||
fontSize: 28,
|
||||
color: '#fff',
|
||||
},
|
||||
audioControls: {
|
||||
gap: 16,
|
||||
},
|
||||
sliderControl: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
controlLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
width: 60,
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
},
|
||||
valueText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
width: 40,
|
||||
textAlign: 'right',
|
||||
},
|
||||
switchRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 20,
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
switchColumn: {
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
},
|
||||
switchControl: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
},
|
||||
switchLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
},
|
||||
buttonGroup: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
},
|
||||
toggleButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
},
|
||||
toggleButtonActive: {
|
||||
backgroundColor: '#007aff',
|
||||
borderColor: '#007aff',
|
||||
},
|
||||
toggleButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#666',
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
toggleButtonTextActive: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
actionGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
},
|
||||
actionButton: {
|
||||
backgroundColor: '#007aff',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
actionButtonText: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
},
|
||||
eventLog: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#007aff',
|
||||
height: 200,
|
||||
},
|
||||
eventLogScroll: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
},
|
||||
eventItem: {
|
||||
paddingVertical: 4,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e1e1e1',
|
||||
},
|
||||
eventTime: {
|
||||
fontSize: 10,
|
||||
color: '#999',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 2,
|
||||
},
|
||||
eventText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
fontFamily: 'monospace',
|
||||
},
|
||||
mountControl: {
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
trackList: {
|
||||
marginTop: 12,
|
||||
},
|
||||
trackButton: {
|
||||
padding: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
trackButtonSelected: {
|
||||
backgroundColor: '#007aff',
|
||||
borderColor: '#007aff',
|
||||
},
|
||||
trackButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
trackButtonTextSelected: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
noTracksText: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
noTracksSubText: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
textAlign: 'center',
|
||||
},
|
||||
selectedTrackInfo: {
|
||||
backgroundColor: '#e7f4ff',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#007aff',
|
||||
marginBottom: 12,
|
||||
},
|
||||
selectedTrackLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginBottom: 4,
|
||||
},
|
||||
selectedTrackText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#007aff',
|
||||
},
|
||||
eventLogContainer: {
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: '#28a745',
|
||||
marginBottom: 12,
|
||||
},
|
||||
eventLogTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#666',
|
||||
marginBottom: 8,
|
||||
},
|
||||
eventLogText: {
|
||||
fontSize: 11,
|
||||
color: '#333',
|
||||
fontFamily: 'monospace',
|
||||
marginBottom: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type {
|
||||
IgnoreSilentSwitchMode,
|
||||
MixAudioMode,
|
||||
ResizeMode,
|
||||
} from 'react-native-video';
|
||||
|
||||
export interface VideoSettings {
|
||||
show: boolean;
|
||||
videoType: 'hls' | 'mp4' | 'drm';
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
rate: number;
|
||||
loop: boolean;
|
||||
showNativeControls: boolean;
|
||||
resizeMode: ResizeMode;
|
||||
mixAudioMode: MixAudioMode;
|
||||
ignoreSilentSwitchMode: IgnoreSilentSwitchMode;
|
||||
playInBackground: boolean;
|
||||
playWhenInactive: boolean;
|
||||
}
|
||||
|
||||
export const defaultSettings: VideoSettings = {
|
||||
show: false,
|
||||
videoType: 'hls',
|
||||
volume: 1,
|
||||
muted: false,
|
||||
rate: 1,
|
||||
loop: false,
|
||||
showNativeControls: false,
|
||||
resizeMode: 'contain',
|
||||
mixAudioMode: 'auto',
|
||||
ignoreSilentSwitchMode: 'auto',
|
||||
playInBackground: true,
|
||||
playWhenInactive: false,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const formatTime = (seconds: number) => {
|
||||
if (isNaN(seconds)) return '--:--';
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${m}:${s < 10 ? '0' : ''}${s}`;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Platform } from 'react-native';
|
||||
import type { VideoConfig } from 'react-native-video';
|
||||
import {
|
||||
enable as enableDRMPlugin,
|
||||
disable as disableDRMPlugin,
|
||||
isEnabled as isDRMPluginEnabled,
|
||||
} from '@twg/react-native-video-drm';
|
||||
|
||||
const getDRMSource = (): VideoConfig => {
|
||||
const HLS =
|
||||
'https://d2e67eijd6imrw.cloudfront.net/559c7a7e-960d-4cd8-9dba-bc4e59890177/assets/47cfca69-91b5-4311-bf6c-b9b1f297ed9b/videokit-720p-dash-hls-drm/hls/index.m3u8';
|
||||
const DASH =
|
||||
'https://d2e67eijd6imrw.cloudfront.net/559c7a7e-960d-4cd8-9dba-bc4e59890177/assets/47cfca69-91b5-4311-bf6c-b9b1f297ed9b/videokit-720p-dash-hls-drm/dash/index.mpd';
|
||||
const CERT =
|
||||
'https://thewidlarzgroup.la.drm.cloud/certificate/fairplay?brandGuid=559c7a7e-960d-4cd8-9dba-bc4e59890177';
|
||||
const FAIRPLAY_LICENSE =
|
||||
'https://thewidlarzgroup.la.drm.cloud/acquire-license/fairplay?brandGuid=559c7a7e-960d-4cd8-9dba-bc4e59890177';
|
||||
const WIDEVINE_LICENSE =
|
||||
'https://thewidlarzgroup.la.drm.cloud/acquire-license/widevine?brandGuid=559c7a7e-960d-4cd8-9dba-bc4e59890177';
|
||||
const USER_TOKEN =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTU3NzQzMTQsImtpZCI6WyIqIl0sImR1cmF0aW9uIjo4NjQwMCwicGVyc2lzdGVudCI6dHJ1ZSwid2lkZXZpbmUiOnsibGljZW5zZV9kdXJhdGlvbiI6OTk5OTk5OSwicGxheWJhY2tfZHVyYXRpb24iOjk5OTk5OTksInJlbnRpYWxfZHVyYXRpb24iOjk5OTk5OTl9LCJmYWlycGxheSI6eyJzdG9yYWdlX2R1cmF0aW9uIjo5OTk5OTk5LCJwbGF5YmFja19kdXJhdGlvbiI6OTk5OTk5OX19.Gm5caVyq_pSTJIy8mZ-vrCeATKueRATmubirh-ajqVg';
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
return {
|
||||
uri: HLS,
|
||||
drm: {
|
||||
type: 'fairplay',
|
||||
licenseUrl: FAIRPLAY_LICENSE,
|
||||
certificateUrl: CERT,
|
||||
getLicense: async ({ spc, keyUrl }) => {
|
||||
const formData = new FormData();
|
||||
formData.append('spc', spc);
|
||||
|
||||
const fixedLicenseUrl = keyUrl.replace('skd://', 'https://');
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${fixedLicenseUrl}&userToken=${USER_TOKEN}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData.ckc;
|
||||
} catch (error) {
|
||||
console.error('Error fetching license:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
} as VideoConfig;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return {
|
||||
uri: DASH,
|
||||
headers: {
|
||||
'x-drm-userToken': USER_TOKEN,
|
||||
},
|
||||
drm: {
|
||||
type: 'widevine',
|
||||
licenseUrl: WIDEVINE_LICENSE,
|
||||
},
|
||||
} as VideoConfig;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'DRM is not Supported or Configured on Platform not supported'
|
||||
);
|
||||
};
|
||||
|
||||
export type VideoType = 'hls' | 'mp4' | 'drm';
|
||||
|
||||
export const getVideoSource = (type: VideoType): VideoConfig => {
|
||||
if (type === 'drm') {
|
||||
if (!isDRMPluginEnabled) {
|
||||
enableDRMPlugin();
|
||||
}
|
||||
return getDRMSource();
|
||||
}
|
||||
|
||||
if (isDRMPluginEnabled) {
|
||||
disableDRMPlugin();
|
||||
}
|
||||
|
||||
const HLS = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
|
||||
const MP4 =
|
||||
'https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_30MB.mp4';
|
||||
|
||||
return {
|
||||
uri: type === 'hls' ? HLS : MP4,
|
||||
externalSubtitles: [
|
||||
{
|
||||
label: 'External',
|
||||
uri: 'https://gist.githubusercontent.com/samdutton/ca37f3adaf4e23679957b8083e061177/raw/e19399fbccbc069a2af4266e5120ae6bad62699a/sample.vtt',
|
||||
language: 'en',
|
||||
type: 'vtt',
|
||||
},
|
||||
],
|
||||
} as VideoConfig;
|
||||
};
|
||||
Reference in New Issue
Block a user