@stepincto/expo-video 1.0.0
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/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- package/src/ts-declarations/react-native-assets.d.ts +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import CoreImage
|
|
4
|
+
import CoreGraphics
|
|
5
|
+
import ExpoModulesCore
|
|
6
|
+
|
|
7
|
+
internal final class VideoThumbnail: SharedRef<UIImage> {
|
|
8
|
+
internal override var nativeRefType: String {
|
|
9
|
+
"image"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
var requestedTime: CMTime
|
|
13
|
+
var actualTime: CMTime
|
|
14
|
+
|
|
15
|
+
internal init(_ image: CGImage, requestedTime: CMTime, actualTime: CMTime) {
|
|
16
|
+
self.requestedTime = requestedTime
|
|
17
|
+
self.actualTime = actualTime
|
|
18
|
+
super.init(UIImage(cgImage: image))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override func getAdditionalMemoryPressure() -> Int {
|
|
22
|
+
guard let cgImage = ref.cgImage else {
|
|
23
|
+
return 0
|
|
24
|
+
}
|
|
25
|
+
return cgImage.bytesPerRow * cgImage.height
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import AVFoundation
|
|
4
|
+
import CoreMedia
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
Generates an array of thumbnails from the given assets and options.
|
|
8
|
+
*/
|
|
9
|
+
internal func generateThumbnails(asset: AVAsset, times: [CMTime], options: VideoThumbnailOptions?) async throws -> [VideoThumbnail] {
|
|
10
|
+
let generator = AVAssetImageGenerator(asset: asset)
|
|
11
|
+
|
|
12
|
+
generator.appliesPreferredTrackTransform = true
|
|
13
|
+
generator.requestedTimeToleranceAfter = .zero
|
|
14
|
+
generator.maximumSize = options?.getMaxSize() ?? .zero
|
|
15
|
+
|
|
16
|
+
// `requestedTimeToleranceBefore` can only be set if times are less
|
|
17
|
+
// than the video duration, otherwise it will fail to generate an image.
|
|
18
|
+
if times.allSatisfy({ $0 < asset.duration }) {
|
|
19
|
+
generator.requestedTimeToleranceBefore = .zero
|
|
20
|
+
}
|
|
21
|
+
return try await generateThumbnails(generator: generator, times: times)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
Generates an array of thumbnails using the given image generator. It uses two different ways to generate the images, based on the platform version.
|
|
26
|
+
*/
|
|
27
|
+
private func generateThumbnails(generator: AVAssetImageGenerator, times: [CMTime]) async throws -> [VideoThumbnail] {
|
|
28
|
+
if #available(iOS 16, tvOS 16, *) {
|
|
29
|
+
return try await generator
|
|
30
|
+
.images(for: times)
|
|
31
|
+
.reduce(into: [VideoThumbnail]()) { thumbnails, result in
|
|
32
|
+
let thumbnail = try VideoThumbnail(result.image, requestedTime: result.requestedTime, actualTime: result.actualTime)
|
|
33
|
+
thumbnails.append(thumbnail)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return try await VideoThumbnailLegacyGenerator(generator: generator, times: times)
|
|
37
|
+
.reduce(into: [VideoThumbnail]()) { thumbnails, thumbnail in
|
|
38
|
+
thumbnails.append(thumbnail)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
A replacement for the `AVAssetImageGenerator.images(for:)` async iterator that is available only as of iOS 16.
|
|
44
|
+
*/
|
|
45
|
+
internal struct VideoThumbnailLegacyGenerator: AsyncSequence, AsyncIteratorProtocol {
|
|
46
|
+
typealias Element = VideoThumbnail
|
|
47
|
+
|
|
48
|
+
let generator: AVAssetImageGenerator
|
|
49
|
+
let times: [CMTime]
|
|
50
|
+
var currentIndex: Int = 0
|
|
51
|
+
|
|
52
|
+
mutating func next() async throws -> Element? {
|
|
53
|
+
guard currentIndex < times.count, !Task.isCancelled else {
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
let requestedTime = times[currentIndex]
|
|
57
|
+
var actualTime = CMTime.zero
|
|
58
|
+
let image = try generator.copyCGImage(at: requestedTime, actualTime: &actualTime)
|
|
59
|
+
|
|
60
|
+
currentIndex += 1
|
|
61
|
+
|
|
62
|
+
return VideoThumbnail(image, requestedTime: requestedTime, actualTime: actualTime)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func makeAsyncIterator() -> Self {
|
|
66
|
+
return self
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Record representing thumbnail generation options.
|
|
5
|
+
*/
|
|
6
|
+
internal struct VideoThumbnailOptions: Record {
|
|
7
|
+
static let `default` = VideoThumbnailOptions()
|
|
8
|
+
|
|
9
|
+
@Field var maxWidth: Int = 0
|
|
10
|
+
@Field var maxHeight: Int = 0
|
|
11
|
+
|
|
12
|
+
func getMaxSize() -> CGSize {
|
|
13
|
+
return CGSize(width: maxWidth, height: maxHeight)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import MobileCoreServices
|
|
5
|
+
import ExpoModulesCore
|
|
6
|
+
|
|
7
|
+
internal class VideoAsset: AVURLAsset, @unchecked Sendable {
|
|
8
|
+
internal let videoSource: VideoSource
|
|
9
|
+
private var resourceLoaderDelegate: ResourceLoaderDelegate?
|
|
10
|
+
private let initialScheme: String?
|
|
11
|
+
private let saveFilePath: String?
|
|
12
|
+
private var customFileExtension: String?
|
|
13
|
+
private let useCaching: Bool
|
|
14
|
+
|
|
15
|
+
var cachingError: Error?
|
|
16
|
+
|
|
17
|
+
internal var urlRequestHeaders: [String: String]?
|
|
18
|
+
|
|
19
|
+
init(url: URL, videoSource: VideoSource) {
|
|
20
|
+
self.videoSource = videoSource
|
|
21
|
+
let cachedMimeType = MediaInfo(forResourceUrl: url)?.mimeType
|
|
22
|
+
let cachedExtension = mimeTypeToExtension(mimeType: cachedMimeType) ?? ""
|
|
23
|
+
let fileExtension = url.pathExtension.isEmpty ? cachedExtension : url.pathExtension
|
|
24
|
+
self.saveFilePath = Self.pathForUrl(url: url, fileExtension: fileExtension)
|
|
25
|
+
self.urlRequestHeaders = videoSource.headers
|
|
26
|
+
self.initialScheme = URLComponents(url: url, resolvingAgainstBaseURL: false)?.scheme
|
|
27
|
+
|
|
28
|
+
// Creates an URL that will delegate it's requests to ResourceLoaderDelegate
|
|
29
|
+
let urlWithCustomScheme = url.withScheme(VideoCacheManager.expoVideoCacheScheme)
|
|
30
|
+
|
|
31
|
+
let assetOptions: [String: Any]? = if let headers = videoSource.headers {
|
|
32
|
+
["AVURLAssetHTTPHeaderFieldsKey": headers]
|
|
33
|
+
} else {
|
|
34
|
+
nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let canCache = Self.canCache(videoSource: videoSource)
|
|
38
|
+
|
|
39
|
+
if saveFilePath == nil && videoSource.useCaching {
|
|
40
|
+
log.warn("Failed to create a cache file path for the provided source with uri: \(videoSource.uri?.absoluteString ?? "null")")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if !canCache && videoSource.useCaching {
|
|
44
|
+
log.warn("Provided source with uri: \(videoSource.uri?.absoluteString ?? "null") cannot be cached. Caching will be disabled")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if urlWithCustomScheme == nil && videoSource.useCaching {
|
|
48
|
+
log.warn("CachingPlayerItem error: Urls without a scheme are not supported, the resource won't be cached")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
guard let saveFilePath, let urlWithCustomScheme, videoSource.useCaching else {
|
|
52
|
+
// Initialize with no caching
|
|
53
|
+
useCaching = false
|
|
54
|
+
super.init(url: url, options: assetOptions)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Enable caching
|
|
59
|
+
useCaching = true
|
|
60
|
+
resourceLoaderDelegate = ResourceLoaderDelegate(url: url, saveFilePath: saveFilePath, fileExtension: fileExtension, urlRequestHeaders: urlRequestHeaders)
|
|
61
|
+
super.init(url: urlWithCustomScheme, options: assetOptions)
|
|
62
|
+
|
|
63
|
+
resourceLoaderDelegate?.onError = { [weak self] error in
|
|
64
|
+
self?.cachingError = error
|
|
65
|
+
}
|
|
66
|
+
self.resourceLoader.setDelegate(resourceLoaderDelegate, queue: VideoCacheManager.shared.cacheQueue)
|
|
67
|
+
self.createCacheDirectoryIfNeeded()
|
|
68
|
+
VideoCacheManager.shared.ensureCacheIntegrity(forSavePath: saveFilePath)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
deinit {
|
|
72
|
+
guard useCaching else {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if let saveFilePath, let cachedFileUrl = URL(string: saveFilePath) {
|
|
76
|
+
VideoCacheManager.shared.unregisterOpenFile(at: cachedFileUrl)
|
|
77
|
+
}
|
|
78
|
+
VideoCacheManager.shared.ensureCacheSize()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static func pathForUrl(url: URL, fileExtension: String) -> String? {
|
|
82
|
+
let hashedData = SHA256.hash(data: Data(url.absoluteString.utf8))
|
|
83
|
+
let hashString = hashedData.compactMap { String(format: "%02x", $0) }.joined()
|
|
84
|
+
let parsedExtension = fileExtension.starts(with: ".") || fileExtension.isEmpty ? fileExtension : ("." + fileExtension)
|
|
85
|
+
let hashFilename = hashString + parsedExtension
|
|
86
|
+
|
|
87
|
+
guard var cachesDirectory = try? FileManager.default.url(
|
|
88
|
+
for: .cachesDirectory,
|
|
89
|
+
in: .userDomainMask,
|
|
90
|
+
appropriateFor: nil,
|
|
91
|
+
create: true)
|
|
92
|
+
else {
|
|
93
|
+
log.warn("CachingPlayerItem error: Can't access default cache directory")
|
|
94
|
+
return nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cachesDirectory.appendPathComponent(VideoCacheManager.expoVideoCacheScheme, isDirectory: true)
|
|
98
|
+
cachesDirectory.appendPathComponent(hashFilename)
|
|
99
|
+
|
|
100
|
+
return cachesDirectory.path
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static func canCache(videoSource: VideoSource) -> Bool {
|
|
104
|
+
guard videoSource.uri?.scheme?.starts(with: "http") == true else {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
return videoSource.drm == nil
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func createCacheDirectoryIfNeeded() {
|
|
111
|
+
guard var cachesDirectory = try? FileManager.default.url(
|
|
112
|
+
for: .cachesDirectory,
|
|
113
|
+
in: .userDomainMask,
|
|
114
|
+
appropriateFor: nil,
|
|
115
|
+
create: true)
|
|
116
|
+
else {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cachesDirectory.appendPathComponent(VideoCacheManager.expoVideoCacheScheme, isDirectory: true)
|
|
121
|
+
try? FileManager.default.createDirectory(at: cachesDirectory, withIntermediateDirectories: true)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Copyright 2023-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
private let defaultCause = "unknown cause"
|
|
6
|
+
|
|
7
|
+
internal final class PictureInPictureUnsupportedException: Exception {
|
|
8
|
+
override var reason: String {
|
|
9
|
+
"Picture in picture is not supported on this device"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
internal final class DRMUnsupportedException: GenericException<DRMType> {
|
|
14
|
+
override var reason: String {
|
|
15
|
+
"DRMType: `\(param)` is unsupported on iOS"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
internal final class DRMLoadException: GenericException<String?> {
|
|
20
|
+
override var reason: String {
|
|
21
|
+
"Failed to decrypt the video stream: \(param ?? defaultCause)"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
internal final class PlayerException: GenericException<String?> {
|
|
26
|
+
override var reason: String {
|
|
27
|
+
"Failed to initialise the player: \(param ?? defaultCause)"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
internal final class PlayerItemLoadException: GenericException<String?> {
|
|
32
|
+
override var reason: String {
|
|
33
|
+
"Failed to load the player item: \(param ?? defaultCause)"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
internal final class CachingAssetInitializationException: GenericException<URL?> {
|
|
38
|
+
override var reason: String {
|
|
39
|
+
"Failed to initialize a caching asset. The provided url: \(param?.absoluteString ?? "nil") doesn't have a valid scheme for caching"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
internal final class VideoCacheException: GenericException<String?> {
|
|
44
|
+
override var reason: String {
|
|
45
|
+
param ?? "Unexpected expo-video cache error"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
internal final class VideoCacheUnsupportedFormatException: GenericException<String> {
|
|
50
|
+
override var reason: String {
|
|
51
|
+
"The server responded with a resource with mimeType: \(param) which cannot be played with caching enabled"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import AVKit
|
|
4
|
+
import Foundation
|
|
5
|
+
import ExpoModulesCore
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper class used to keep track of all existing VideoViews and VideoPlayers and manage their lifecycle
|
|
9
|
+
*/
|
|
10
|
+
class VideoManager {
|
|
11
|
+
static var shared = VideoManager()
|
|
12
|
+
|
|
13
|
+
private static var managerQueue = DispatchQueue(label: "com.expo.video.manager.managerQueue")
|
|
14
|
+
private var videoViews = NSHashTable<VideoView>.weakObjects()
|
|
15
|
+
private var videoPlayers = NSHashTable<VideoPlayer>.weakObjects()
|
|
16
|
+
|
|
17
|
+
var hasRegisteredPlayers: Bool {
|
|
18
|
+
return !videoPlayers.allObjects.isEmpty
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func register(videoPlayer: VideoPlayer) {
|
|
22
|
+
Self.managerQueue.async { [weak self, weak videoPlayer] in
|
|
23
|
+
guard let self = self, let videoPlayer = videoPlayer else {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
self.videoPlayers.add(videoPlayer)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func unregister(videoPlayer: VideoPlayer) {
|
|
31
|
+
Self.managerQueue.async { [weak self, weak videoPlayer] in
|
|
32
|
+
guard let self = self, let videoPlayer = videoPlayer else {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
self.videoPlayers.remove(videoPlayer)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func register(videoView: VideoView) {
|
|
40
|
+
videoViews.add(videoView)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func unregister(videoView: VideoView) {
|
|
44
|
+
videoViews.remove(videoView)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func onAppForegrounded() {
|
|
48
|
+
for videoPlayer in videoPlayers.allObjects {
|
|
49
|
+
videoPlayer.setTracksEnabled(true)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func onAppBackgrounded() {
|
|
54
|
+
for videoView in videoViews.allObjects {
|
|
55
|
+
guard let player = videoView.player else {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if player.staysActiveInBackground == true {
|
|
59
|
+
player.setTracksEnabled(videoView.isInPictureInPicture)
|
|
60
|
+
} else if !videoView.isInPictureInPicture {
|
|
61
|
+
player.ref.pause()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - Audio Session Management
|
|
67
|
+
|
|
68
|
+
// This function usually takes less than 5ms to execute, but in some cases (initial setup) it takes up to 70ms
|
|
69
|
+
// Because of this we dispatch it on another queue to minimize the load on main queue.
|
|
70
|
+
internal func setAppropriateAudioSessionOrWarn() {
|
|
71
|
+
Self.managerQueue.async { [weak self] in
|
|
72
|
+
self?.setAudioSession()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func setAudioSession() {
|
|
77
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
78
|
+
let audioMixingMode = findAudioMixingMode()
|
|
79
|
+
var audioSessionCategoryOptions: AVAudioSession.CategoryOptions = audioSession.categoryOptions
|
|
80
|
+
|
|
81
|
+
let isOutputtingAudio = videoPlayers.allObjects.contains { player in
|
|
82
|
+
player.isPlaying && !player.isMuted
|
|
83
|
+
}
|
|
84
|
+
let anyPlayerShowsNotification = videoPlayers.allObjects.contains { player in
|
|
85
|
+
player.showNowPlayingNotification
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let shouldMixOverride = audioMixingMode == .mixWithOthers
|
|
89
|
+
let doNotMixOverride = audioMixingMode == .doNotMix
|
|
90
|
+
let shouldDuckOthers = audioMixingMode == .duckOthers && isOutputtingAudio
|
|
91
|
+
|
|
92
|
+
// The now playing notification won't be shown if we allow the audio to mix with others
|
|
93
|
+
let autoShouldMix = !isOutputtingAudio && !anyPlayerShowsNotification
|
|
94
|
+
let shouldMixWithOthers = shouldMixOverride || autoShouldMix
|
|
95
|
+
|
|
96
|
+
if shouldMixWithOthers && !shouldDuckOthers && !doNotMixOverride {
|
|
97
|
+
audioSessionCategoryOptions.insert(.mixWithOthers)
|
|
98
|
+
} else {
|
|
99
|
+
audioSessionCategoryOptions.remove(.mixWithOthers)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if shouldDuckOthers && !doNotMixOverride {
|
|
103
|
+
audioSessionCategoryOptions.insert(.duckOthers)
|
|
104
|
+
} else {
|
|
105
|
+
audioSessionCategoryOptions.remove(.duckOthers)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if audioSession.categoryOptions != audioSessionCategoryOptions || audioSession.category != .playback || audioSession.mode != .moviePlayback {
|
|
109
|
+
do {
|
|
110
|
+
try audioSession.setCategory(.playback, mode: .moviePlayback, options: audioSessionCategoryOptions)
|
|
111
|
+
} catch {
|
|
112
|
+
log.warn("Failed to set audio session category. This might cause issues with audio playback and Picture in Picture. \(error.localizedDescription)")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Make sure audio session is active if any video is playing
|
|
117
|
+
if isOutputtingAudio || doNotMixOverride {
|
|
118
|
+
do {
|
|
119
|
+
try audioSession.setActive(true)
|
|
120
|
+
} catch {
|
|
121
|
+
log.warn("Failed to activate the audio session. This might cause issues with audio playback. \(error.localizedDescription)")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func findAudioMixingMode() -> AudioMixingMode? {
|
|
127
|
+
let playingPlayers = videoPlayers.allObjects.filter({ player in
|
|
128
|
+
player.isPlaying
|
|
129
|
+
})
|
|
130
|
+
var audioMixingMode: AudioMixingMode = .mixWithOthers
|
|
131
|
+
|
|
132
|
+
if playingPlayers.isEmpty {
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
for videoPlayer in playingPlayers where (audioMixingMode.priority()) < videoPlayer.audioMixingMode.priority() {
|
|
136
|
+
audioMixingMode = videoPlayer.audioMixingMode
|
|
137
|
+
}
|
|
138
|
+
return audioMixingMode
|
|
139
|
+
}
|
|
140
|
+
}
|