import * as React from 'react'; import { useState, useCallback, useEffect } from 'react'; import { Dimensions, StyleSheet, View, ScrollView } from 'react-native'; import IVSPlayer, { IVSPlayerRef, LogLevel, PlayerState, Quality, ResizeMode, } from 'amazon-ivs-react-native-player'; import { IconButton, ActivityIndicator, Button, Text, Portal, Title, } from 'react-native-paper'; import { Platform } from 'react-native'; import Slider from '@react-native-community/slider'; import type { StackNavigationProp } from '@react-navigation/stack'; import { useNavigation } from '@react-navigation/native'; import { parseSecondsToString } from '../helpers'; import SettingsItem from '../components/SettingsItem'; import SettingsSliderItem from '../components/SettingsSliderItem'; import LogLevelPicker from '../components/LogLevelPicker'; import { Position, URL } from '../constants'; import SettingsInputItem from '../components/SettingsInputItem'; import SettingsSwitchItem from '../components/SettingsSwitchItem'; import type { RootStackParamList } from '../App'; import OptionPicker from '../components/OptionPicker'; import useAppState from '../useAppState'; const INITIAL_PLAYBACK_RATE = 1; const INITIAL_PROGRESS_INTERVAL = 1; const INITIAL_BREAKPOINTS = [10, 20, 40, 55, 60, 130, 250, 490, 970, 1930]; const UPDATED_BREAKPOINTS = [5, 15, 30, 45, 60, 120, 240, 480, 960, 1920]; type PlaygroundScreenNavigationProp = StackNavigationProp< RootStackParamList, 'PlaygroundExample' >; type ResizeModeOption = { name: string; value: ResizeMode; }; const RESIZE_MODES: ResizeModeOption[] = [ { name: 'Aspect Fill', value: 'aspectFill', }, { name: 'Aspect Fit', value: 'aspectFit', }, { name: 'Aspect Zoom', value: 'aspectZoom', }, ]; export default function PlaygroundExample() { const { setOptions } = useNavigation(); const mediaPlayerRef = React.useRef(null); const [isModalOpened, setIsModalOpened] = useState(false); const [autoplay, setAutoplay] = useState(true); const [paused, setPaused] = useState(false); const [url, setUrl] = useState(URL); const [muted, setMuted] = useState(false); const [pauseInBackground, setPauseInBackground] = useState(false); const [manualQuality, setManualQuality] = useState(null); const [detectedQuality, setDetectedQuality] = useState(null); const [initialBufferDuration, setInitialBufferDuration] = useState(0.1); const [autoMaxQuality, setAutoMaxQuality] = useState(null); const [qualities, setQualities] = useState([]); const [autoQualityMode, setAutoQualityMode] = useState(true); const [buffering, setBuffering] = useState(false); const [duration, setDuration] = useState(null); const [liveLowLatency, setLiveLowLatency] = useState(true); const [playbackRate, setPlaybackRate] = useState(1); const [logLevel, setLogLevel] = useState(LogLevel.IVSLogLevelError); const [progressInterval, setProgressInterval] = useState(1); const [volume, setVolume] = useState(1); const [position, setPosition] = useState(); const [lockPosition, setLockPosition] = useState(false); const [positionSlider, setPositionSlider] = useState(0); const [breakpoints, setBreakpoints] = useState(INITIAL_BREAKPOINTS); const [orientation, setOrientation] = useState(Position.PORTRAIT); const [logs, setLogs] = useState([]); const [resizeMode, setResizeMode] = useState( RESIZE_MODES[1] ); useAppState({ onBackground: () => { pauseInBackground && setPaused(true); }, onForeground: () => { pauseInBackground && setPaused(false); }, }); const log = useCallback( (text: string) => { console.log(text); setLogs((logs) => [text, ...logs.slice(0, 30)]); }, [setLogs] ); const onDimensionChange = useCallback( ({ window: { width, height } }) => { if (width < height) { setOrientation(Position.PORTRAIT); setOptions({ headerShown: true, gestureEnabled: true }); } else { setOrientation(Position.LANDSCAPE); setOptions({ headerShown: false, gestureEnabled: false }); } }, [setOptions] ); useEffect(() => { Dimensions.addEventListener('change', onDimensionChange); return () => { Dimensions.removeEventListener('change', onDimensionChange); }; }, [onDimensionChange]); const slidingCompleteHandler = (value: number) => { mediaPlayerRef?.current?.seekTo(value); }; return ( {/* Note: A buffering indicator is included by default on Android. It's styling is managed in /example/android/app/src/main/res/values/styles.xml by adjusting the 'android:indeterminateTint'. */} {buffering && Platform.OS === 'ios' ? ( ) : null} console.log('new position', newPosition)} onPlayerStateChange={(state) => { if (state === PlayerState.Buffering) { log(`buffering at ${detectedQuality?.name}`); } if (state === PlayerState.Playing || state === PlayerState.Idle) { setBuffering(false); } log(`state changed: ${state}`); }} onDurationChange={(duration) => { setDuration(duration); log(`duration changed: ${parseSecondsToString(duration || 0)}`); }} onQualityChange={(newQuality) => { setDetectedQuality(newQuality); log(`quality changed: ${newQuality?.name}`); }} onRebuffering={() => setBuffering(true)} onLoadStart={() => log(`load started`)} onLoad={(loadedDuration) => log( `loaded duration changed: ${parseSecondsToString( loadedDuration || 0 )}` ) } onLiveLatencyChange={(liveLatency) => console.log(`live latency changed: ${liveLatency}`) } onTextCue={(textCue) => console.log('text cue', textCue)} onTextMetadataCue={(textMetadataCue) => console.log('text metadata cue', textMetadataCue) } onProgress={(newPosition) => { if (!lockPosition) { setPosition(newPosition); setPositionSlider(newPosition); } console.log( `progress changed: ${parseSecondsToString( position ? position : 0 )}` ); }} onData={(data) => setQualities(data.qualities)} onVideoStatistics={(video) => console.log('onVideoStatistics', video)} onError={(error) => console.log('error', error)} onTimePoint={(timePoint) => console.log('time point', timePoint)} > {orientation === Position.PORTRAIT ? ( <> {duration && position !== null ? ( {parseSecondsToString(position ? position : 0)} ) : ( )} {duration ? ( {parseSecondsToString(duration)} ) : null} {duration && !Number.isNaN(duration) ? ( setLockPosition(true)} onTouchEnd={() => { setLockPosition(false); setPositionSlider(position ?? 0); }} /> ) : null} { setPaused((prev) => !prev); }} style={styles.playIcon} /> ) : null} {logs.map((log, index) => ( {log} ))} {isModalOpened && ( Settings { setAutoQualityMode(!quality); setManualQuality(quality); }} /> { setResizeMode(mode); log(`Resize mode changed: ${resizeMode?.value}`); }} /> setPlaybackRate(Number(value.toFixed(1))) } testID="playbackRate" /> setProgressInterval(Number(value)) } testID="progressInterval" /> { if (value) { setManualQuality(null); } setAutoQualityMode(value); }} value={autoQualityMode} testID="autoQuality" /> )} ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 0, backgroundColor: 'black', }, playerContainer: { flex: 1, justifyContent: 'center', }, playButtonContainer: { alignItems: 'center', position: 'absolute', bottom: 10, width: '100%', }, playIcon: { borderWidth: 1, borderColor: 'white', }, positionContainer: { width: '100%', }, durationsContainer: { flexDirection: 'row', justifyContent: 'space-between', }, icon: { position: 'absolute', top: 5, right: 0, }, settings: { padding: 15, flexDirection: 'row', flexWrap: 'wrap', alignItems: 'flex-start', }, settingsHeader: { backgroundColor: '#fff', alignItems: 'center', }, positionText: { color: 'white', }, settingsTitle: { paddingBottom: 8, }, flex1: { flex: 1, }, loader: { position: 'absolute', zIndex: 1, alignSelf: 'center', }, logs: { top: 0, width: '100%', height: 250, borderTopLeftRadius: 8, borderTopRightRadius: 8, backgroundColor: '#e2e2e2', padding: 10, paddingTop: 20, }, log: { fontSize: 7, }, modalContentContainer: { paddingHorizontal: 10, alignItems: 'center', justifyContent: 'center', flex: 1, }, modalContent: { backgroundColor: 'white', borderRadius: 4, height: '80%' }, modalHeader: { justifyContent: 'space-between', flexDirection: 'row', paddingHorizontal: 10, paddingVertical: 5, }, });