@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.
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +46 -4
- package/android/src/main/java/expo/modules/video/VideoModule.kt +6 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +13 -0
- package/android/src/main/java/expo/modules/video/enums/FullscreenOrientation.kt +26 -0
- package/android/src/main/java/expo/modules/video/records/FullscreenOptions.kt +12 -0
- package/android/src/main/java/expo/modules/video/utils/FullscreenActivityOrientationHelper.kt +120 -0
- package/build/VideoModule.d.ts.map +1 -1
- package/build/VideoModule.js +24 -1
- package/build/VideoModule.js.map +1 -1
- package/build/VideoView.d.ts.map +1 -1
- package/build/VideoView.js +10 -3
- package/build/VideoView.js.map +1 -1
- package/build/VideoView.types.d.ts +47 -0
- package/build/VideoView.types.d.ts.map +1 -1
- package/build/VideoView.types.js.map +1 -1
- package/build/VideoView.web.d.ts.map +1 -1
- package/build/VideoView.web.js +3 -2
- package/build/VideoView.web.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/expo-module.config.json +7 -1
- package/ios/Cache/CachableRequest.swift +2 -41
- package/ios/Cache/CachedResource.swift +1 -1
- package/ios/Cache/MediaFileHandle.swift +10 -8
- package/ios/Cache/MediaInfo.swift +41 -6
- package/ios/Cache/VideoCacheManager.swift +53 -6
- package/ios/Enums/FullscreenOrientation.swift +34 -0
- package/ios/OrientationAVPlayerViewController.swift +231 -0
- package/ios/Records/FullscreenOptions.swift +12 -0
- package/ios/VideoAsset.swift +4 -1
- package/ios/VideoModule.swift +8 -0
- package/ios/VideoView.swift +19 -50
- package/package.json +3 -4
- package/src/VideoModule.ts +28 -1
- package/src/VideoView.tsx +28 -3
- package/src/VideoView.types.ts +57 -0
- package/src/VideoView.web.tsx +4 -2
- 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';
|
package/build/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/build/index.js.map
CHANGED
|
@@ -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;
|
|
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"]}
|
package/expo-module.config.json
CHANGED
|
@@ -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
|
-
//
|
|
36
|
-
|
|
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 {
|
|
@@ -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(
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|