@stepincto/expo-video 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +46 -4
  2. package/android/src/main/java/expo/modules/video/VideoModule.kt +6 -0
  3. package/android/src/main/java/expo/modules/video/VideoView.kt +13 -0
  4. package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +26 -0
  5. package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +12 -0
  6. package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
  7. package/build/VideoModule.d.ts.map +1 -1
  8. package/build/VideoModule.js +24 -1
  9. package/build/VideoModule.js.map +1 -1
  10. package/build/VideoView.d.ts.map +1 -1
  11. package/build/VideoView.js +10 -3
  12. package/build/VideoView.js.map +1 -1
  13. package/build/VideoView.types.d.ts +47 -0
  14. package/build/VideoView.types.d.ts.map +1 -1
  15. package/build/VideoView.types.js.map +1 -1
  16. package/build/VideoView.web.d.ts.map +1 -1
  17. package/build/VideoView.web.js +3 -2
  18. package/build/VideoView.web.js.map +1 -1
  19. package/build/index.d.ts +1 -1
  20. package/build/index.d.ts.map +1 -1
  21. package/build/index.js.map +1 -1
  22. package/expo-module.config.json +7 -1
  23. package/ios/Cache/CachableRequest.swift +2 -41
  24. package/ios/Cache/CachedResource.swift +1 -1
  25. package/ios/Cache/MediaFileHandle.swift +10 -8
  26. package/ios/Cache/MediaInfo.swift +41 -6
  27. package/ios/Cache/VideoCacheManager.swift +53 -6
  28. package/ios/Enums/FullscreenOrientation.swift +34 -0
  29. package/ios/OrientationAVPlayerViewController.swift +231 -0
  30. package/ios/Records/FullscreenOptions.swift +12 -0
  31. package/ios/VideoAsset.swift +4 -1
  32. package/ios/VideoModule.swift +8 -0
  33. package/ios/VideoView.swift +19 -50
  34. package/package.json +3 -4
  35. package/src/VideoModule.ts +28 -1
  36. package/src/VideoView.tsx +28 -3
  37. package/src/VideoView.types.ts +57 -0
  38. package/src/VideoView.web.tsx +4 -2
  39. package/src/index.ts +7 -1
@@ -1 +1 @@
1
- {"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport VideoPlayer, { getSourceUri } from './VideoPlayer.web';\nimport type { VideoViewProps } from './VideoView.types';\n\nfunction createAudioContext(): AudioContext | null {\n return typeof window !== 'undefined' ? new window.AudioContext() : null;\n}\n\nfunction createZeroGainNode(audioContext: AudioContext | null): GainNode | null {\n const zeroGainNode = audioContext?.createGain() ?? null;\n\n if (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n }\n return zeroGainNode;\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport function isPictureInPictureSupported(): boolean {\n return typeof document === 'object' && typeof document.exitPictureInPicture === 'function';\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {\n const videoRef = useRef<null | HTMLVideoElement>(null);\n const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);\n const hasToSetupAudioContext = useRef(false);\n const fullscreenChangeListener = useRef<null | (() => void)>(null);\n const isWaitingForFirstFrame = useRef(false);\n\n /**\n * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called\n * for the second time with another context and there is no way to unbind the video and audio context afterward.\n */\n const audioContextRef = useRef<null | AudioContext>(null);\n const zeroGainNodeRef = useRef<null | GainNode>(null);\n\n useImperativeHandle(ref, () => ({\n enterFullscreen: async () => {\n if (!props.allowsFullscreen) {\n return;\n }\n await videoRef.current?.requestFullscreen();\n },\n exitFullscreen: async () => {\n await document.exitFullscreen();\n },\n startPictureInPicture: async () => {\n await videoRef.current?.requestPictureInPicture();\n },\n stopPictureInPicture: async () => {\n try {\n await document.exitPictureInPicture();\n } catch (e) {\n if (e instanceof DOMException && e.name === 'InvalidStateError') {\n console.warn('The VideoView is not in Picture-in-Picture mode.');\n } else {\n throw e;\n }\n }\n },\n }));\n\n useEffect(() => {\n const onEnter = () => {\n props.onPictureInPictureStart?.();\n };\n const onLeave = () => {\n props.onPictureInPictureStop?.();\n };\n const onLoadStart = () => {\n isWaitingForFirstFrame.current = true;\n };\n const onCanPlay = () => {\n if (isWaitingForFirstFrame.current) {\n props.onFirstFrameRender?.();\n }\n isWaitingForFirstFrame.current = false;\n };\n videoRef.current?.addEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.addEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.addEventListener('loadstart', onLoadStart);\n videoRef.current?.addEventListener('loadeddata', onCanPlay);\n\n return () => {\n videoRef.current?.removeEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.removeEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.removeEventListener('loadstart', onLoadStart);\n videoRef.current?.removeEventListener('loadeddata', onCanPlay);\n };\n }, [videoRef, props.onPictureInPictureStop, props.onPictureInPictureStart]);\n\n // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one\n // player only one will emit audio).\n function attachAudioNodes() {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (audioContext && zeroGainNode && mediaNode) {\n props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);\n } else {\n console.warn(\n \"Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player.\"\n );\n }\n }\n\n function detachAudioNodes() {\n const audioContext = audioContextRef.current;\n const mediaNode = mediaNodeRef.current;\n if (audioContext && mediaNode && videoRef.current) {\n props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n }\n\n function maybeSetupAudioContext() {\n if (\n !hasToSetupAudioContext.current ||\n !navigator.userActivation.hasBeenActive ||\n !videoRef.current\n ) {\n return;\n }\n const audioContext = createAudioContext();\n\n detachAudioNodes();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(videoRef.current)\n : null;\n attachAudioNodes();\n hasToSetupAudioContext.current = false;\n }\n\n function fullscreenListener() {\n if (document.fullscreenElement === videoRef.current) {\n props.onFullscreenEnter?.();\n } else {\n props.onFullscreenExit?.();\n }\n }\n\n function setupFullscreenListener() {\n fullscreenChangeListener.current = fullscreenListener;\n videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);\n }\n\n function cleanupFullscreenListener() {\n if (fullscreenChangeListener.current) {\n videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);\n fullscreenChangeListener.current = null;\n }\n }\n\n useEffect(() => {\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\n setupFullscreenListener();\n attachAudioNodes();\n\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n cleanupFullscreenListener();\n detachAudioNodes();\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls ?? true}\n controlsList={props.allowsFullscreen ? undefined : 'nofullscreen'}\n crossOrigin={props.crossOrigin}\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\n }}\n onPlay={() => {\n maybeSetupAudioContext();\n }}\n // The player can autoplay when muted, unmuting by a user should create the audio context\n onVolumeChange={() => {\n maybeSetupAudioContext();\n }}\n ref={(newRef) => {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef && !newRef.isEqualNode(videoRef.current)) {\n videoRef.current = newRef;\n hasToSetupAudioContext.current = true;\n maybeSetupAudioContext();\n }\n }}\n disablePictureInPicture={!props.allowsPictureInPicture}\n playsInline={props.playsInline}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
1
+ {"version":3,"file":"VideoView.web.js","sourceRoot":"","sources":["../src/VideoView.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport VideoPlayer, { getSourceUri } from './VideoPlayer.web';\nimport type { VideoViewProps } from './VideoView.types';\n\nfunction createAudioContext(): AudioContext | null {\n return typeof window !== 'undefined' ? new window.AudioContext() : null;\n}\n\nfunction createZeroGainNode(audioContext: AudioContext | null): GainNode | null {\n const zeroGainNode = audioContext?.createGain() ?? null;\n\n if (audioContext && zeroGainNode) {\n zeroGainNode.gain.value = 0;\n zeroGainNode.connect(audioContext.destination);\n }\n return zeroGainNode;\n}\n\nfunction mapStyles(style: VideoViewProps['style']): React.CSSProperties {\n const flattenedStyles = StyleSheet.flatten(style);\n // Looking through react-native-web source code they also just pass styles directly without further conversions, so it's just a cast.\n return flattenedStyles as React.CSSProperties;\n}\n\nexport function isPictureInPictureSupported(): boolean {\n return typeof document === 'object' && typeof document.exitPictureInPicture === 'function';\n}\n\nexport const VideoView = forwardRef((props: { player?: VideoPlayer } & VideoViewProps, ref) => {\n const videoRef = useRef<null | HTMLVideoElement>(null);\n const fullscreenEnabled =\n props.fullscreenOptions?.enable ?? props.allowsFullscreen ?? true;\n const mediaNodeRef = useRef<null | MediaElementAudioSourceNode>(null);\n const hasToSetupAudioContext = useRef(false);\n const fullscreenChangeListener = useRef<null | (() => void)>(null);\n const isWaitingForFirstFrame = useRef(false);\n\n /**\n * Audio context is used to mute all but one video when multiple video views are playing from one player simultaneously.\n * Using audio context nodes allows muting videos without displaying the mute icon in the video player.\n * We have to keep the context that called createMediaElementSource(videoRef), as the method can't be called\n * for the second time with another context and there is no way to unbind the video and audio context afterward.\n */\n const audioContextRef = useRef<null | AudioContext>(null);\n const zeroGainNodeRef = useRef<null | GainNode>(null);\n\n useImperativeHandle(ref, () => ({\n enterFullscreen: async () => {\n if (!fullscreenEnabled) {\n return;\n }\n await videoRef.current?.requestFullscreen();\n },\n exitFullscreen: async () => {\n await document.exitFullscreen();\n },\n startPictureInPicture: async () => {\n await videoRef.current?.requestPictureInPicture();\n },\n stopPictureInPicture: async () => {\n try {\n await document.exitPictureInPicture();\n } catch (e) {\n if (e instanceof DOMException && e.name === 'InvalidStateError') {\n console.warn('The VideoView is not in Picture-in-Picture mode.');\n } else {\n throw e;\n }\n }\n },\n }));\n\n useEffect(() => {\n const onEnter = () => {\n props.onPictureInPictureStart?.();\n };\n const onLeave = () => {\n props.onPictureInPictureStop?.();\n };\n const onLoadStart = () => {\n isWaitingForFirstFrame.current = true;\n };\n const onCanPlay = () => {\n if (isWaitingForFirstFrame.current) {\n props.onFirstFrameRender?.();\n }\n isWaitingForFirstFrame.current = false;\n };\n videoRef.current?.addEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.addEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.addEventListener('loadstart', onLoadStart);\n videoRef.current?.addEventListener('loadeddata', onCanPlay);\n\n return () => {\n videoRef.current?.removeEventListener('enterpictureinpicture', onEnter);\n videoRef.current?.removeEventListener('leavepictureinpicture', onLeave);\n videoRef.current?.removeEventListener('loadstart', onLoadStart);\n videoRef.current?.removeEventListener('loadeddata', onCanPlay);\n };\n }, [videoRef, props.onPictureInPictureStop, props.onPictureInPictureStart]);\n\n // Adds the video view as a candidate for being the audio source for the player (when multiple views play from one\n // player only one will emit audio).\n function attachAudioNodes() {\n const audioContext = audioContextRef.current;\n const zeroGainNode = zeroGainNodeRef.current;\n const mediaNode = mediaNodeRef.current;\n\n if (audioContext && zeroGainNode && mediaNode) {\n props.player.mountAudioNode(audioContext, zeroGainNode, mediaNode);\n } else {\n console.warn(\n \"Couldn't mount audio node, this might affect the audio playback when using multiple video views with the same player.\"\n );\n }\n }\n\n function detachAudioNodes() {\n const audioContext = audioContextRef.current;\n const mediaNode = mediaNodeRef.current;\n if (audioContext && mediaNode && videoRef.current) {\n props.player.unmountAudioNode(videoRef.current, audioContext, mediaNode);\n }\n }\n\n function maybeSetupAudioContext() {\n if (\n !hasToSetupAudioContext.current ||\n !navigator.userActivation.hasBeenActive ||\n !videoRef.current\n ) {\n return;\n }\n const audioContext = createAudioContext();\n\n detachAudioNodes();\n audioContextRef.current = audioContext;\n zeroGainNodeRef.current = createZeroGainNode(audioContextRef.current);\n mediaNodeRef.current = audioContext\n ? audioContext.createMediaElementSource(videoRef.current)\n : null;\n attachAudioNodes();\n hasToSetupAudioContext.current = false;\n }\n\n function fullscreenListener() {\n if (document.fullscreenElement === videoRef.current) {\n props.onFullscreenEnter?.();\n } else {\n props.onFullscreenExit?.();\n }\n }\n\n function setupFullscreenListener() {\n fullscreenChangeListener.current = fullscreenListener;\n videoRef.current?.addEventListener('fullscreenchange', fullscreenChangeListener.current);\n }\n\n function cleanupFullscreenListener() {\n if (fullscreenChangeListener.current) {\n videoRef.current?.removeEventListener('fullscreenchange', fullscreenChangeListener.current);\n fullscreenChangeListener.current = null;\n }\n }\n\n useEffect(() => {\n if (videoRef.current) {\n props.player?.mountVideoView(videoRef.current);\n }\n setupFullscreenListener();\n attachAudioNodes();\n\n return () => {\n if (videoRef.current) {\n props.player?.unmountVideoView(videoRef.current);\n }\n cleanupFullscreenListener();\n detachAudioNodes();\n };\n }, [props.player]);\n\n return (\n <video\n controls={props.nativeControls ?? true}\n controlsList={fullscreenEnabled ? undefined : 'nofullscreen'}\n crossOrigin={props.crossOrigin}\n style={{\n ...mapStyles(props.style),\n objectFit: props.contentFit,\n }}\n onPlay={() => {\n maybeSetupAudioContext();\n }}\n // The player can autoplay when muted, unmuting by a user should create the audio context\n onVolumeChange={() => {\n maybeSetupAudioContext();\n }}\n ref={(newRef) => {\n // This is called with a null value before `player.unmountVideoView` is called,\n // we can't assign null to videoRef if we want to unmount it from the player.\n if (newRef && !newRef.isEqualNode(videoRef.current)) {\n videoRef.current = newRef;\n hasToSetupAudioContext.current = true;\n maybeSetupAudioContext();\n }\n }}\n disablePictureInPicture={!props.allowsPictureInPicture}\n playsInline={props.playsInline}\n src={getSourceUri(props.player?.src) ?? ''}\n />\n );\n});\n\nexport default VideoView;\n"]}
package/build/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { isPictureInPictureSupported, clearVideoCacheAsync, setVideoCacheSizeAsync, getCurrentVideoCacheSize, preCacheVideoPartialAsync, preCacheVideoAsync, isVideoCachedAsync, } from './VideoModule';
2
2
  export { VideoView } from './VideoView';
3
3
  export { useVideoPlayer } from './VideoPlayer';
4
- export { VideoContentFit, VideoViewProps, SurfaceType } from './VideoView.types';
4
+ export { VideoContentFit, VideoViewProps, SurfaceType, FullscreenOptions, FullscreenOrientation, } from './VideoView.types';
5
5
  export { VideoThumbnail } from './VideoThumbnail';
6
6
  export { createVideoPlayer } from './VideoPlayer';
7
7
  export { VideoPlayer, VideoPlayerStatus, VideoSource, PlayerError, VideoMetadata, DRMType, DRMOptions, BufferOptions, AudioMixingMode, VideoThumbnailOptions, VideoSize, SubtitleTrack, AudioTrack, VideoTrack, ContentType, } from './VideoPlayer.types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,UAAU,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,SAAS,EACT,aAAa,EACb,UAAU,EACV,UAAU,EACV,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,8BAA8B,EAC9B,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EACL,eAAe,EACf,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,aAAa,EACb,OAAO,EACP,UAAU,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,SAAS,EACT,aAAa,EACb,UAAU,EACV,UAAU,EACV,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,yBAAyB,EACzB,8BAA8B,EAC9B,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EACL,WAAW,GAeZ,MAAM,qBAAqB,CAAC","sourcesContent":["export {\n isPictureInPictureSupported,\n clearVideoCacheAsync,\n setVideoCacheSizeAsync,\n getCurrentVideoCacheSize,\n preCacheVideoPartialAsync,\n preCacheVideoAsync,\n isVideoCachedAsync,\n} from './VideoModule';\nexport { VideoView } from './VideoView';\nexport { useVideoPlayer } from './VideoPlayer';\n\nexport { VideoContentFit, VideoViewProps, SurfaceType } from './VideoView.types';\nexport { VideoThumbnail } from './VideoThumbnail';\n\nexport { createVideoPlayer } from './VideoPlayer';\n\nexport {\n VideoPlayer,\n VideoPlayerStatus,\n VideoSource,\n PlayerError,\n VideoMetadata,\n DRMType,\n DRMOptions,\n BufferOptions,\n AudioMixingMode,\n VideoThumbnailOptions,\n VideoSize,\n SubtitleTrack,\n AudioTrack,\n VideoTrack,\n ContentType,\n} from './VideoPlayer.types';\n\nexport {\n VideoPlayerEvents,\n StatusChangeEventPayload,\n PlayingChangeEventPayload,\n PlaybackRateChangeEventPayload,\n VolumeChangeEventPayload,\n MutedChangeEventPayload,\n TimeUpdateEventPayload,\n SourceChangeEventPayload,\n SourceLoadEventPayload,\n} from './VideoPlayerEvents.types';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAS/C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EACL,WAAW,GAeZ,MAAM,qBAAqB,CAAC","sourcesContent":["export {\n isPictureInPictureSupported,\n clearVideoCacheAsync,\n setVideoCacheSizeAsync,\n getCurrentVideoCacheSize,\n preCacheVideoPartialAsync,\n preCacheVideoAsync,\n isVideoCachedAsync,\n} from './VideoModule';\nexport { VideoView } from './VideoView';\nexport { useVideoPlayer } from './VideoPlayer';\n\nexport {\n VideoContentFit,\n VideoViewProps,\n SurfaceType,\n FullscreenOptions,\n FullscreenOrientation,\n} from './VideoView.types';\nexport { VideoThumbnail } from './VideoThumbnail';\n\nexport { createVideoPlayer } from './VideoPlayer';\n\nexport {\n VideoPlayer,\n VideoPlayerStatus,\n VideoSource,\n PlayerError,\n VideoMetadata,\n DRMType,\n DRMOptions,\n BufferOptions,\n AudioMixingMode,\n VideoThumbnailOptions,\n VideoSize,\n SubtitleTrack,\n AudioTrack,\n VideoTrack,\n ContentType,\n} from './VideoPlayer.types';\n\nexport {\n VideoPlayerEvents,\n StatusChangeEventPayload,\n PlayingChangeEventPayload,\n PlaybackRateChangeEventPayload,\n VolumeChangeEventPayload,\n MutedChangeEventPayload,\n TimeUpdateEventPayload,\n SourceChangeEventPayload,\n SourceLoadEventPayload,\n} from './VideoPlayerEvents.types';\n"]}
@@ -4,6 +4,12 @@
4
4
  "modules": ["VideoModule"]
5
5
  },
6
6
  "android": {
7
- "modules": ["expo.modules.video.VideoModule"]
7
+ "modules": ["expo.modules.video.VideoModule"],
8
+ "publication": {
9
+ "groupId": "host.exp.exponent",
10
+ "artifactId": "expo.modules.video",
11
+ "version": "1.0.6",
12
+ "repository": "local-maven-repo"
13
+ }
8
14
  }
9
15
  }
@@ -17,8 +17,6 @@ class CachableRequest: Equatable, Hashable {
17
17
  var response: URLResponse?
18
18
  private(set) var receivedData = Data()
19
19
  private let dataOffset: Int64
20
- private static var fileLocks: [String: NSLock] = [:]
21
- private static let fileLocksQueue = DispatchQueue(label: "expo-video-cache-file-locks")
22
20
 
23
21
  init(loadingRequest: AVAssetResourceLoadingRequest, dataTask: URLSessionDataTask, dataRequest: AVAssetResourceLoadingDataRequest) {
24
22
  self.loadingRequest = loadingRequest
@@ -32,45 +30,8 @@ class CachableRequest: Equatable, Hashable {
32
30
  }
33
31
 
34
32
  func saveData(to cachedResource: CachedResource) {
35
- // 1. Check if file is fully cached
36
- if let mediaInfo = cachedResource.mediaInfo {
37
- let expectedLength = Int(mediaInfo.expectedContentLength)
38
- let ranges = mediaInfo.loadedDataRanges
39
- if ranges.count == 1, ranges[0].0 == 0, ranges[0].1 == expectedLength - 1 {
40
- // Already fully cached, do not write
41
- return
42
- }
43
- }
44
-
45
- // 2. Acquire per-file lock (non-blocking)
46
- let filePath = cachedResource.dataPath
47
- let lock: NSLock = Self.fileLocksQueue.sync {
48
- if let l = Self.fileLocks[filePath] { return l }
49
- let l = NSLock()
50
- Self.fileLocks[filePath] = l
51
- return l
52
- }
53
- if !lock.try() {
54
- // Another writer is active, skip this write
55
- return
56
- }
57
-
58
- // 3. Perform async write with final range check inside the task
59
- let offset = dataOffset
60
- let length = receivedData.count
61
- let end = offset + Int64(length) - 1
62
-
63
- Task.detached(priority: .userInitiated) { [receivedData] in
64
- // Final check inside the async task to prevent race conditions
65
- if cachedResource.canRespondWithData(from: offset, to: end) {
66
- // This range is already cached, skip writing
67
- lock.unlock()
68
- return
69
- }
70
-
71
- await cachedResource.writeData(data: receivedData, offset: offset)
72
- lock.unlock()
73
- }
33
+ // Disabled: Player is now read-only with respect to the cache.
34
+ // Do nothing.
74
35
  }
75
36
 
76
37
  static func == (lhs: CachableRequest, rhs: CachableRequest) -> Bool {
@@ -10,7 +10,7 @@ import ExpoModulesCore
10
10
  */
11
11
  class CachedResource {
12
12
  private let url: URL
13
- internal let dataPath: String
13
+ private let dataPath: String
14
14
  private let fileHandle: MediaFileHandle
15
15
  private(set) var mediaInfo: MediaInfo?
16
16
 
@@ -26,16 +26,17 @@ internal class MediaFileHandle {
26
26
  return attributes?[.size] as? Int ?? 0
27
27
  }
28
28
 
29
- private var fileUrl: URL? {
30
- URL(string: filePath)
29
+ private var fileUrl: URL {
30
+ URL(fileURLWithPath: filePath)
31
31
  }
32
32
 
33
33
  init(filePath: String) {
34
34
  self.filePath = filePath
35
35
 
36
- if let fileUrl {
37
- VideoCacheManager.shared.registerOpenFile(at: fileUrl)
38
- }
36
+ // Use fileURLWithPath for proper file URL creation
37
+ let url = fileUrl
38
+ VideoCacheManager.shared.registerOpenFile(at: url)
39
+ print("[MediaFileHandle] Registering file URL: \(url.path)")
39
40
 
40
41
  if !FileManager.default.fileExists(atPath: filePath) {
41
42
  FileManager.default.createFile(atPath: filePath, contents: nil, attributes: nil)
@@ -43,9 +44,10 @@ internal class MediaFileHandle {
43
44
  }
44
45
 
45
46
  deinit {
46
- if let fileUrl {
47
- VideoCacheManager.shared.unregisterOpenFile(at: fileUrl)
48
- }
47
+ // Use fileURLWithPath for proper file URL creation
48
+ let url = fileUrl
49
+ VideoCacheManager.shared.unregisterOpenFile(at: url)
50
+ print("[MediaFileHandle] Unregistering file URL: \(url.path)")
49
51
  guard FileManager.default.fileExists(atPath: filePath) else {
50
52
  return
51
53
  }
@@ -7,6 +7,7 @@ class MediaInfo: Codable {
7
7
  var mimeType: String?
8
8
  var headerFields: [String: String]?
9
9
  var savePath: String
10
+ private var shouldAutoUnregister: Bool = true
10
11
 
11
12
  // Tuples can't be encoded/decoded, so we workaround that with an array
12
13
  private var loadedDataRangesArr: [[Int]] = []
@@ -21,24 +22,57 @@ class MediaInfo: Codable {
21
22
 
22
23
  private enum CodingKeys: String, CodingKey {
23
24
  case expectedContentLength, supportsByteRangeAccess, mimeType, loadedDataRangesArr, headerFields, savePath
25
+ // Note: shouldAutoUnregister is not encoded/decoded as it's runtime-only
26
+ }
27
+
28
+ // Custom decoding to handle shouldAutoUnregister
29
+ required init(from decoder: Decoder) throws {
30
+ let container = try decoder.container(keyedBy: CodingKeys.self)
31
+ expectedContentLength = try container.decode(Int64.self, forKey: .expectedContentLength)
32
+ supportsByteRangeAccess = try container.decode(Bool.self, forKey: .supportsByteRangeAccess)
33
+ mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType)
34
+ loadedDataRangesArr = try container.decode([[Int]].self, forKey: .loadedDataRangesArr)
35
+ headerFields = try container.decodeIfPresent([String: String].self, forKey: .headerFields)
36
+ savePath = try container.decode(String.self, forKey: .savePath)
37
+ shouldAutoUnregister = true // Default to true for decoded objects
38
+ }
39
+
40
+ // Custom encoding to exclude shouldAutoUnregister
41
+ func encode(to encoder: Encoder) throws {
42
+ var container = encoder.container(keyedBy: CodingKeys.self)
43
+ try container.encode(expectedContentLength, forKey: .expectedContentLength)
44
+ try container.encode(supportsByteRangeAccess, forKey: .supportsByteRangeAccess)
45
+ try container.encodeIfPresent(mimeType, forKey: .mimeType)
46
+ try container.encode(loadedDataRangesArr, forKey: .loadedDataRangesArr)
47
+ try container.encodeIfPresent(headerFields, forKey: .headerFields)
48
+ try container.encode(savePath, forKey: .savePath)
24
49
  }
25
50
 
26
- init(expectedContentLength: Int64, mimeType: String?, supportsByteRangeAccess: Bool, headerFields: [String: String]?, savePath: String) {
51
+ init(expectedContentLength: Int64, mimeType: String?, supportsByteRangeAccess: Bool, headerFields: [String: String]?, savePath: String, autoRegister: Bool = true) {
27
52
  self.mimeType = mimeType
28
53
  self.supportsByteRangeAccess = supportsByteRangeAccess
29
54
  self.expectedContentLength = expectedContentLength
30
55
  self.headerFields = headerFields
31
56
  self.savePath = savePath
57
+ self.shouldAutoUnregister = autoRegister
32
58
  self.loadedDataRanges = loadedDataRanges
33
59
 
34
- if let url = URL(string: savePath) {
35
- VideoCacheManager.shared.registerOpenFile(at: url)
60
+ // Only auto-register if requested (prevents double registration)
61
+ if autoRegister {
62
+ // Use fileURLWithPath for proper file URL creation instead of URL(string:)
63
+ let fileUrl = URL(fileURLWithPath: savePath)
64
+ VideoCacheManager.shared.registerOpenFile(at: fileUrl)
65
+ print("[MediaInfo] Registering file URL: \(fileUrl.path)")
36
66
  }
37
67
  }
38
68
 
39
69
  deinit {
40
- if let url = URL(string: savePath) {
41
- VideoCacheManager.shared.unregisterOpenFile(at: url)
70
+ // Only unregister if we auto-registered (prevents double unregistration)
71
+ if shouldAutoUnregister {
72
+ // Use fileURLWithPath for proper file URL creation instead of URL(string:)
73
+ let fileUrl = URL(fileURLWithPath: savePath)
74
+ VideoCacheManager.shared.unregisterOpenFile(at: fileUrl)
75
+ print("[MediaInfo] Unregistering file URL: \(fileUrl.path)")
42
76
  }
43
77
  }
44
78
 
@@ -50,7 +84,8 @@ class MediaInfo: Codable {
50
84
  mimeType: mediaInfo.mimeType,
51
85
  supportsByteRangeAccess: mediaInfo.supportsByteRangeAccess,
52
86
  headerFields: mediaInfo.headerFields,
53
- savePath: mediaInfo.savePath)
87
+ savePath: mediaInfo.savePath,
88
+ autoRegister: true) // Default behavior for loaded MediaInfo
54
89
  self.loadedDataRanges = mediaInfo.loadedDataRanges
55
90
  } catch {
56
91
  return nil
@@ -13,7 +13,17 @@ class VideoCacheManager {
13
13
  private let maxCacheSizeKey = "\(VideoCacheManager.expoVideoCacheScheme)/maxCacheSize"
14
14
 
15
15
  // Files currently being used/modified by the player - they will be skipped when clearing the cache
16
- private var openFiles: Set<URL> = Set()
16
+ private let openFilesQueue = DispatchQueue(label: "\(VideoCacheManager.expoVideoCacheScheme)-openFiles-queue", attributes: .concurrent)
17
+ private var _openFiles: Set<URL> = Set()
18
+
19
+ private var openFiles: Set<URL> {
20
+ get {
21
+ return openFilesQueue.sync { _openFiles }
22
+ }
23
+ set {
24
+ openFilesQueue.async(flags: .barrier) { self._openFiles = newValue }
25
+ }
26
+ }
17
27
 
18
28
  // All cache commands such as clean or adding new data should be run on this queue
19
29
  let cacheQueue = DispatchQueue(label: "\(VideoCacheManager.expoVideoCacheScheme)-dispatch-queue")
@@ -23,11 +33,29 @@ class VideoCacheManager {
23
33
  }
24
34
 
25
35
  func registerOpenFile(at url: URL) {
26
- openFiles.insert(url)
36
+ guard url.isFileURL else {
37
+ print("[VideoCacheManager] Warning: Attempting to register non-file URL: \(url)")
38
+ return
39
+ }
40
+
41
+ openFilesQueue.async(flags: .barrier) { [weak self] in
42
+ guard let self = self else { return }
43
+ self._openFiles.insert(url)
44
+ print("[VideoCacheManager] Registered file: \(url.path)")
45
+ }
27
46
  }
28
47
 
29
48
  func unregisterOpenFile(at url: URL) {
30
- openFiles.remove(url)
49
+ guard url.isFileURL else {
50
+ print("[VideoCacheManager] Warning: Attempting to unregister non-file URL: \(url)")
51
+ return
52
+ }
53
+
54
+ openFilesQueue.async(flags: .barrier) { [weak self] in
55
+ guard let self = self else { return }
56
+ self._openFiles.remove(url)
57
+ print("[VideoCacheManager] Unregistered file: \(url.path)")
58
+ }
31
59
  }
32
60
 
33
61
  func setMaxCacheSize(newSize: Int) throws {
@@ -177,7 +205,9 @@ class VideoCacheManager {
177
205
  }
178
206
 
179
207
  private func fileIsOpen(url: URL) -> Bool {
180
- return openFiles.contains(url) || openFiles.contains { $0.relativePath == url.relativePath }
208
+ return openFilesQueue.sync {
209
+ return _openFiles.contains(url) || _openFiles.contains { $0.relativePath == url.relativePath }
210
+ }
181
211
  }
182
212
  }
183
213
 
@@ -210,6 +240,14 @@ extension VideoCacheManager {
210
240
  let fileUrl = URL(fileURLWithPath: saveFilePath)
211
241
  let mediaInfoPath = saveFilePath + VideoCacheManager.mediaInfoSuffix
212
242
 
243
+ // CRITICAL FIX: Register file as open BEFORE any operations to prevent cache cleanup race condition
244
+ VideoCacheManager.shared.registerOpenFile(at: fileUrl)
245
+
246
+ defer {
247
+ // Always unregister when done, even if there's an error
248
+ VideoCacheManager.shared.unregisterOpenFile(at: fileUrl)
249
+ }
250
+
213
251
  // Ensure parent directory exists before writing file
214
252
  let parentDir = fileUrl.deletingLastPathComponent()
215
253
  do {
@@ -226,14 +264,23 @@ extension VideoCacheManager {
226
264
  let mimeType = response.mimeType
227
265
  let supportsByteRangeAccess = true
228
266
  let headerFields: [String: String]? = nil
229
- let mediaInfo = MediaInfo(expectedContentLength: expectedLength, mimeType: mimeType, supportsByteRangeAccess: supportsByteRangeAccess, headerFields: headerFields, savePath: mediaInfoPath)
267
+
268
+ // Create MediaInfo without auto-registration (we already registered above)
269
+ let mediaInfo = MediaInfo(
270
+ expectedContentLength: expectedLength,
271
+ mimeType: mimeType,
272
+ supportsByteRangeAccess: supportsByteRangeAccess,
273
+ headerFields: headerFields,
274
+ savePath: mediaInfoPath,
275
+ autoRegister: false
276
+ )
230
277
  mediaInfo.setLoadedDataRanges([(0, data.count - 1)])
231
278
  mediaInfo.saveToFile()
232
279
 
233
280
  // Print current cache size
234
281
  let cacheSize = VideoCacheManager.shared.getCacheDirectorySize()
235
282
  print("[expo-video] Cache size after caching: \(cacheSize) bytes")
236
- return false
283
+ return true
237
284
  }
238
285
 
239
286
  static func preCacheVideoPartialAsync(from urlString: String, chunkSize: Int = 1_048_576) async throws -> Bool {
@@ -0,0 +1,34 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+
5
+ internal enum FullscreenOrientation: String, Enumerable {
6
+ case landscape
7
+ case portrait
8
+ case landscapeLeft
9
+ case landscapeRight
10
+ case portraitUp
11
+ case portraitDown
12
+ case `default`
13
+
14
+ #if !os(tvOS)
15
+ func toUIInterfaceOrientationMask() -> UIInterfaceOrientationMask {
16
+ switch self {
17
+ case .landscape:
18
+ return .landscape
19
+ case .portrait:
20
+ return [.portrait, .portraitUpsideDown]
21
+ case .landscapeLeft:
22
+ return .landscapeLeft
23
+ case .landscapeRight:
24
+ return .landscapeRight
25
+ case .portraitUp:
26
+ return .portrait
27
+ case .portraitDown:
28
+ return .portraitUpsideDown
29
+ case .default:
30
+ return .all
31
+ }
32
+ }
33
+ #endif
34
+ }
@@ -0,0 +1,231 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import AVKit
4
+ import ExpoModulesCore
5
+
6
+ /**
7
+ * AVPlayerViewController with support for locking the fullscreen orientation, and other expo-video utility methods such as `enterPictureInPicture`
8
+ */
9
+ internal class OrientationAVPlayerViewController: AVPlayerViewController, AVPlayerViewControllerDelegate {
10
+ weak var forwardDelegate: AVPlayerViewControllerDelegate?
11
+ #if !os(tvOS)
12
+ var fullscreenOrientation: UIInterfaceOrientationMask = UIDevice.current.userInterfaceIdiom == .phone ? .allButUpsideDown : .all
13
+ #endif
14
+ var autoExitOnRotate: Bool = false
15
+
16
+ // Used to determine whether the user has rotated the device to the target orientation. Useful for auto-exit for example:
17
+ // Device portrait, fullscreenOrientation - landscape
18
+ // We could auto-exit but we would do it right away causing ugly animations and confusion, instead we wait for the user to rotate to landscape.
19
+ // When they do, we know they acknowledged the orientaiton of the app and we can auto-exit once they rotate to a different orientation.
20
+ // In case of: device landscape, fullscreenOrientation - landscape
21
+ // We can set this to `true` right away.
22
+ private var hasRotatedToTargetOrientation = false
23
+ var isInPictureInPicture = false
24
+
25
+ var isFullscreen: Bool = false {
26
+ didSet {
27
+ guard oldValue == isFullscreen else {
28
+ return
29
+ }
30
+ if !isFullscreen {
31
+ hasRotatedToTargetOrientation = false
32
+ }
33
+ #if os(tvOS)
34
+ hasRotatedToTargetOrientation = true
35
+ #else
36
+ // Check if the current device orientation lines up with target orientation right away after entering fullscreen
37
+ guard let deviceOrientationMask = UIDevice.current.orientation.toInterfaceOrientationMask(), isFullscreen else {
38
+ return
39
+ }
40
+ hasRotatedToTargetOrientation = fullscreenOrientation.contains(deviceOrientationMask)
41
+ #endif
42
+ }
43
+ }
44
+
45
+ #if !os(tvOS)
46
+ override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
47
+ // Always remove the observer to avoid adding it multiple times
48
+ NotificationCenter.default.removeObserver(
49
+ self,
50
+ name: UIDevice.orientationDidChangeNotification,
51
+ object: nil
52
+ )
53
+
54
+ if isFullscreen {
55
+ // Only add the observer when fullscreen, it's useful only for auto-exit
56
+ NotificationCenter.default.addObserver(
57
+ self,
58
+ selector: #selector(deviceOrientationDidChange(_:)),
59
+ name: UIDevice.orientationDidChangeNotification,
60
+ object: nil
61
+ )
62
+ return fullscreenOrientation
63
+ }
64
+ return super.supportedInterfaceOrientations
65
+ }
66
+ #endif
67
+
68
+ convenience init(delegate: AVPlayerViewControllerDelegate?) {
69
+ self.init()
70
+ self.forwardDelegate = delegate
71
+ }
72
+
73
+ deinit {
74
+ #if !os(tvOS)
75
+ NotificationCenter.default.removeObserver(
76
+ self,
77
+ name: UIDevice.orientationDidChangeNotification,
78
+ object: nil
79
+ )
80
+ #endif
81
+ }
82
+
83
+ func enterFullscreen(selectorUnsupportedFallback: (() -> Void)?) {
84
+ let selectorName = "enterFullScreenAnimated:completionHandler:"
85
+ let selectorToEnterFullScreenMode = NSSelectorFromString(selectorName)
86
+
87
+ if self.responds(to: selectorToEnterFullScreenMode) {
88
+ self.perform(selectorToEnterFullScreenMode, with: true, with: nil)
89
+ } else {
90
+ selectorUnsupportedFallback?()
91
+ }
92
+ }
93
+
94
+ func exitFullscreen() {
95
+ if !isFullscreen {
96
+ return
97
+ }
98
+
99
+ let selectorName = "exitFullScreenAnimated:completionHandler:"
100
+ let selectorToExitFullScreenMode = NSSelectorFromString(selectorName)
101
+
102
+ if self.responds(to: selectorToExitFullScreenMode) {
103
+ self.perform(selectorToExitFullScreenMode, with: true, with: nil)
104
+ }
105
+ }
106
+
107
+ func startPictureInPicture() throws {
108
+ if isInPictureInPicture {
109
+ return
110
+ }
111
+ if !AVPictureInPictureController.isPictureInPictureSupported() {
112
+ throw PictureInPictureUnsupportedException()
113
+ }
114
+
115
+ let selectorName = "startPictureInPicture"
116
+ let selectorToStartPictureInPicture = NSSelectorFromString(selectorName)
117
+
118
+ if self.responds(to: selectorToStartPictureInPicture) {
119
+ self.perform(selectorToStartPictureInPicture)
120
+ }
121
+ }
122
+
123
+ func stopPictureInPicture() {
124
+ if !isInPictureInPicture {
125
+ return
126
+ }
127
+ let selectorName = "stopPictureInPicture"
128
+ let selectorToStopPictureInPicture = NSSelectorFromString(selectorName)
129
+
130
+ if self.responds(to: selectorToStopPictureInPicture) {
131
+ self.perform(selectorToStopPictureInPicture)
132
+ }
133
+ }
134
+
135
+ override func viewDidLoad() {
136
+ super.viewDidLoad()
137
+ self.delegate = self
138
+ }
139
+
140
+ #if !os(tvOS)
141
+ @objc private func deviceOrientationDidChange(_ notification: Notification) {
142
+ guard let deviceOrientationMask = UIDevice.current.orientation.toInterfaceOrientationMask(), isFullscreen else {
143
+ return
144
+ }
145
+ // IPhones generally don't support portraitUpsideDown, in that case we never want to exit, becasuse we would exit into an invalid app UI orientation
146
+ let isPortraitUpsideDownAndUnsupported = UIDevice.current.orientation == .portraitUpsideDown && UIDevice.current.userInterfaceIdiom == .phone
147
+ if isPortraitUpsideDownAndUnsupported {
148
+ return
149
+ }
150
+
151
+ hasRotatedToTargetOrientation = fullscreenOrientation.contains(deviceOrientationMask) || hasRotatedToTargetOrientation
152
+
153
+ if autoExitOnRotate && !fullscreenOrientation.contains(deviceOrientationMask) && hasRotatedToTargetOrientation {
154
+ self.exitFullscreen()
155
+ }
156
+ }
157
+
158
+ // MARK: - AVPlayerViewControllerDelegate
159
+ // TODO: Forward more methods to the forward delegate as needed
160
+ func playerViewController(
161
+ _ playerViewController: AVPlayerViewController,
162
+ willBeginFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
163
+ ) {
164
+ forwardDelegate?.playerViewController?(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
165
+ coordinator.animate(alongsideTransition: nil) { [weak self] context in
166
+ if !context.isCancelled {
167
+ self?.isFullscreen = true
168
+ self?.forceRotationUpdate()
169
+ }
170
+ }
171
+ }
172
+
173
+ func playerViewController(
174
+ _ playerViewController: AVPlayerViewController,
175
+ willEndFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
176
+ ) {
177
+ forwardDelegate?.playerViewController?(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
178
+ coordinator.animate(alongsideTransition: nil) { [weak self] context in
179
+ if !context.isCancelled {
180
+ self?.isFullscreen = false
181
+ }
182
+ }
183
+ }
184
+ #endif
185
+
186
+ func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) {
187
+ isInPictureInPicture = true
188
+ forwardDelegate?.playerViewControllerDidStartPictureInPicture?(playerViewController)
189
+ }
190
+
191
+ func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) {
192
+ isInPictureInPicture = false
193
+ forwardDelegate?.playerViewControllerDidStopPictureInPicture?(playerViewController)
194
+ }
195
+
196
+ #if os(tvOS)
197
+ func playerViewControllerWillBeginDismissalTransition(_ playerViewController: AVPlayerViewController) {
198
+ forwardDelegate?.playerViewControllerWillBeginDismissalTransition?(playerViewController)
199
+ }
200
+
201
+ func playerViewControllerDidEndDismissalTransition(_ playerViewController: AVPlayerViewController) {
202
+ forwardDelegate?.playerViewControllerDidEndDismissalTransition?(playerViewController)
203
+ }
204
+ #endif
205
+
206
+ #if !os(tvOS)
207
+ private func forceRotationUpdate() {
208
+ if #available(iOS 16.0, *) {
209
+ let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
210
+ windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: fullscreenOrientation))
211
+ } else {
212
+ UIViewController.attemptRotationToDeviceOrientation()
213
+ }
214
+ }
215
+ #endif
216
+ }
217
+
218
+ #if !os(tvOS)
219
+ fileprivate extension UIDeviceOrientation {
220
+ func toInterfaceOrientationMask() -> UIInterfaceOrientationMask? {
221
+ switch self {
222
+ case .portrait: return .portrait
223
+ case .portraitUpsideDown: return .portraitUpsideDown
224
+ case .landscapeLeft: return .landscapeLeft
225
+ case .landscapeRight: return .landscapeRight
226
+ case .unknown, .faceUp, .faceDown: return nil
227
+ @unknown default: return nil
228
+ }
229
+ }
230
+ }
231
+ #endif