import React, { ForwardedRef, forwardRef, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import {Image, ImageResizeMode, Platform, View} from 'react-native'; import ReactVideo, { Drm, OnLoadData, OnProgressData, OnSeekData, PosterResizeModeType, ReactVideoProps, ReactVideoSource, SelectedVideoTrack, SelectedVideoTrackType, TextTracks, VideoRef, } from 'react-native-video'; import useManifest, {ManifestEventsTypes} from '../hooks/use-manifest'; import useMetric from '../hooks/metric/use-metric'; import {MetricMediaTypes} from '../hooks/metric/metric-queue-flush'; import {QualityTypes} from '../types'; import {METRIC_HOST} from '../hooks/metric/contants'; type ReactVideoPropsOmit = Omit< ReactVideoProps, 'source' | 'poster' | 'posterResizeMode' | 'selectedVideoTrack' >; export type ReactNativeKinescopeVideoProps = ReactVideoPropsOmit & ManifestEventsTypes & { preload?: boolean; videoId: string; posterResizeMode?: ImageResizeMode; externalId?: string; quality?: QualityTypes; autoSeekChangeQuality?: boolean; // ios only referer?: string; drmAuthToken?: string; }; function ReactNativeKinescopeVideo( props: ReactNativeKinescopeVideoProps, ref: ForwardedRef, ) { const { preload, videoId, posterResizeMode, externalId, quality = 'auto', autoSeekChangeQuality = true, referer = `https://${METRIC_HOST}`, drmAuthToken = '', selectedTextTrack, textTracks, style, paused = false, volume = 1, muted = false, rate = 1, onLoadStart, onLoad, onSeek, onProgress, onEnd, onFullscreenPlayerDidPresent, onFullscreenPlayerWillPresent, onError, onManifestLoadStart, onManifestLoad, onManifestError, ...rest } = props; const videoRef = useRef(); const seekQuality = useRef(0); const [loadingVideo, setLoadingVideo] = useState(false); const [videoStartLoad, setVideoStartLoad] = useState(false); const {loading, manifest} = useManifest({ videoId, referer, drmAuthToken, onManifestLoadStart, onManifestLoad, onManifestError, }); const media: MetricMediaTypes = { type: manifest?.type, videoId: manifest?.id, workspaceId: manifest?.workspaceId, projectId: manifest?.projectId, folderId: manifest?.folderId, }; const { onMetricLoadStart, onMetricLoad, onMetricSeek, onMetricProgress, onMetricEnd, onMetricEnterFullScreen, onMetricExitFullScreen, onMetricError, } = useMetric({media: media, externalId, paused, muted, volume, rate}); const headers = useMemo(() => { return { Referer: referer, }; }, [referer]); useEffect(() => { seekQuality.current = 0; setLoadingVideo(false); setVideoStartLoad(false); }, [videoId]); const handleRef = useCallback( current => { videoRef.current = current; if (ref) { if (typeof ref === 'function') { ref && ref(current); } else { ref.current = current; } } }, [ref], ); const applySeek = useCallback(() => { if (!autoSeekChangeQuality || Platform.OS !== 'ios') { return; } if (seekQuality.current > 0) { videoRef.current?.seek(seekQuality.current); seekQuality.current = 0; } }, [autoSeekChangeQuality]); const handleLoadStart = useCallback( e => { onLoadStart && onLoadStart(e); onMetricLoadStart(); setLoadingVideo(false); setVideoStartLoad(true); }, [onLoadStart], ); const handleLoad = useCallback( (data: OnLoadData) => { onLoad && onLoad(data); onMetricLoad(data); setLoadingVideo(true); applySeek(); }, [onLoad, applySeek], ); const handleSeek = useCallback( (data: OnSeekData) => { onSeek && onSeek(data); onMetricSeek(data); }, [onSeek], ); const handleProgress = useCallback( (data: OnProgressData) => { seekQuality.current = data.currentTime; onProgress && onProgress(data); onMetricProgress(data); }, [onProgress], ); const handleEnd = useCallback(() => { onEnd && onEnd(); onMetricEnd(); }, [onEnd]); const handleFullscreenPlayerDidPresent = useCallback(() => { onFullscreenPlayerDidPresent && onFullscreenPlayerDidPresent(); onMetricEnterFullScreen(); }, [onFullscreenPlayerDidPresent]); const handleFullscreenPlayerWillPresent = useCallback(() => { onFullscreenPlayerWillPresent && onFullscreenPlayerWillPresent(); onMetricExitFullScreen(); }, [onFullscreenPlayerWillPresent]); const handleError = useCallback( error => { onError && onError(error); onMetricError(error); }, [onError], ); const getSelectedVideoTrack = useCallback((): SelectedVideoTrack => { const resolution = manifest?.qualityMap[quality]?.height; if (resolution) { return { type: SelectedVideoTrackType.RESOLUTION, value: resolution, }; } return { type: SelectedVideoTrackType.AUTO, }; }, [quality, manifest]); if (loading || !manifest) { return ; } if (!preload && !videoStartLoad && paused) { return ( ); } const getTextTracks = (): TextTracks | undefined => { if (Platform.OS === 'android') { return textTracks ?? (manifest.subtitles as unknown as TextTracks); } if (!loadingVideo) { return undefined; } return textTracks ?? (manifest.subtitles as unknown as TextTracks); }; const getPosterResizeMode = (): PosterResizeModeType => { switch (posterResizeMode) { case 'center': return PosterResizeModeType.CENTER; case 'cover': return PosterResizeModeType.COVER; case 'repeat': return PosterResizeModeType.REPEAT; case 'stretch': return PosterResizeModeType.STRETCH; case 'contain': return PosterResizeModeType.CONTAIN; default: return PosterResizeModeType.CONTAIN; } }; const getHlsLink = () => { return manifest.qualityMap[quality]?.uri || manifest.hlsLink; }; const getSource = (): ReactVideoSource => { if (Platform.OS === 'android' && manifest.dashLink) { return { uri: manifest.dashLink, type: 'mpd', headers: { ...headers, 'x-drm-type': 'widevine', }, }; } return { uri: getHlsLink(), type: 'm3u8', headers: headers, }; }; const getDrmDash = (): Drm | undefined => { if (!manifest.dashDrm) { return undefined; } return { ...manifest.dashDrm, headers: { ...headers, 'x-drm-type': 'widevine', }, }; }; const getDrmHLS = (): Drm | undefined => { if (!manifest.hlsDrm) { return undefined; } return { ...manifest.hlsDrm, headers: headers, }; }; const getDrm = (): Drm | undefined => { if (Platform.OS === 'android' && manifest.dashLink) { return getDrmDash(); } return getDrmHLS(); }; return ( ); } export default forwardRef(ReactNativeKinescopeVideo);