@stepincto/expo-video 2.2.2-sicto.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 +41 -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 +13 -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 +35 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +44 -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 +46 -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 +142 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +192 -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 +357 -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 +58 -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 +16 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +47 -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 +43 -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,24 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
internal struct DRMOptions: Record {
|
|
7
|
+
@Field
|
|
8
|
+
var type: DRMType = .fairplay
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var licenseServer: String?
|
|
12
|
+
|
|
13
|
+
@Field
|
|
14
|
+
var headers: [String: Any]?
|
|
15
|
+
|
|
16
|
+
@Field
|
|
17
|
+
var contentId: String?
|
|
18
|
+
|
|
19
|
+
@Field
|
|
20
|
+
var certificateUrl: URL?
|
|
21
|
+
|
|
22
|
+
@Field
|
|
23
|
+
var base64CertificateData: String?
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import ExpoModulesCore
|
|
5
|
+
|
|
6
|
+
internal struct PlaybackError: Record {
|
|
7
|
+
@Field
|
|
8
|
+
// swiftlint:disable:next redundant_optional_initialization - Initialization with nil is necessary
|
|
9
|
+
var message: String? = nil
|
|
10
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
import AVKit
|
|
5
|
+
|
|
6
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
7
|
+
internal struct SubtitleTrack: Record {
|
|
8
|
+
@Field
|
|
9
|
+
var language: String? = nil
|
|
10
|
+
|
|
11
|
+
@Field
|
|
12
|
+
var label: String? = nil
|
|
13
|
+
|
|
14
|
+
static func from(mediaSelectionOption option: AVMediaSelectionOption) -> SubtitleTrack? {
|
|
15
|
+
guard let identifier = option.locale?.identifier else {
|
|
16
|
+
return nil
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return SubtitleTrack(language: identifier, label: option.displayName)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
internal struct AudioTrack: Record {
|
|
24
|
+
@Field var language: String? = nil
|
|
25
|
+
@Field var label: String? = nil
|
|
26
|
+
|
|
27
|
+
static func from(mediaSelectionOption option: AVMediaSelectionOption) -> AudioTrack? {
|
|
28
|
+
guard let identifier = option.locale?.identifier else {
|
|
29
|
+
return nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return AudioTrack(language: identifier, label: option.displayName)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal struct VideoTrack: Record, Equatable {
|
|
37
|
+
@Field var id: String? = nil
|
|
38
|
+
@Field var size: VideoSize? = nil
|
|
39
|
+
@Field var mimeType: String? = nil
|
|
40
|
+
@Field var bitrate: Int? = nil
|
|
41
|
+
@Field var isSupported: Bool = true
|
|
42
|
+
@Field var frameRate: Float? = nil
|
|
43
|
+
|
|
44
|
+
static func == (lhs: VideoTrack, rhs: VideoTrack) -> Bool {
|
|
45
|
+
guard lhs.id != nil, rhs.id != nil else {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return lhs.id == rhs.id
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static func from(assetTrack: AVAssetTrack) async -> VideoTrack {
|
|
52
|
+
var bitrate: Int?
|
|
53
|
+
var size: VideoSize?
|
|
54
|
+
let supported = (try? await assetTrack.load(.isPlayable)) ?? true
|
|
55
|
+
let mediaFormat = try? await assetTrack.mediaFormat
|
|
56
|
+
let frameRate = try? await assetTrack.load(.nominalFrameRate)
|
|
57
|
+
|
|
58
|
+
if let bitrateFloat = try? await assetTrack.load(.estimatedDataRate) {
|
|
59
|
+
bitrate = Int(bitrateFloat)
|
|
60
|
+
}
|
|
61
|
+
if let cgSize = try? await assetTrack.load(.naturalSize) {
|
|
62
|
+
size = VideoSize.from(cgSize)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return VideoTrack(id: "\(assetTrack.trackID)", size: size, mimeType: mediaFormat, bitrate: bitrate, isSupported: supported, frameRate: frameRate)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static func from(hlsHeaderLine: String, idLine: String) -> VideoTrack? {
|
|
69
|
+
// The minimum information we require from a video track is it's resolution
|
|
70
|
+
guard hlsHeaderLine.starts(with: "#EXT-X-STREAM-INF"), hlsHeaderLine.contains("RESOLUTION") else {
|
|
71
|
+
return nil
|
|
72
|
+
}
|
|
73
|
+
// The information about the track is separated with ,
|
|
74
|
+
let details = hlsHeaderLine.split(separator: ",")
|
|
75
|
+
.reduce(into: [String: String]()) { dict, detail in
|
|
76
|
+
let pair = detail.split(separator: "=", maxSplits: 1).map {
|
|
77
|
+
String($0).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
78
|
+
}
|
|
79
|
+
if pair.count == 2 {
|
|
80
|
+
let (key, value) = (pair[0], pair[1])
|
|
81
|
+
// Remove possible double quotes
|
|
82
|
+
dict[key] = value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
guard let resolution = details["RESOLUTION"] else {
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let dimensions = resolution.split(separator: "x").map { Int($0) }
|
|
90
|
+
guard dimensions.count == 2, let width = dimensions[0], let height = dimensions[1] else {
|
|
91
|
+
return nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let size = VideoSize(width: width, height: height)
|
|
95
|
+
let mimeType = codecsToMimeType(codecs: details["CODECS"])
|
|
96
|
+
var bitrate: Int? = nil
|
|
97
|
+
var frameRate: Float? = nil
|
|
98
|
+
|
|
99
|
+
// Use the default Andorid behavior for reporting the bitrate
|
|
100
|
+
if let bitrateString = details["BANDWIDTH"] ?? details["AVERAGE-BANDWIDTH"] {
|
|
101
|
+
bitrate = Int(bitrateString)
|
|
102
|
+
}
|
|
103
|
+
if let frameRateString = details["FRAME-RATE"] {
|
|
104
|
+
frameRate = Float(frameRateString)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return VideoTrack(id: idLine, size: size, mimeType: mimeType, bitrate: bitrate, frameRate: frameRate)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// I'm not aware of any built in conversion functions. For HLS sources we only need to worry about a few formats though.
|
|
111
|
+
// https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices#:~:text=1.1.%20All%20video%20MUST%20be%20encoded%20using%20H.264/AVC%2C%20HEVC/H.265%2C%20Dolby%20Vision%2C%20or%20AV1.
|
|
112
|
+
private static func codecsToMimeType(codecs: String?) -> String? {
|
|
113
|
+
guard let codecs else {
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
if codecs.starts(with: "avc1") {
|
|
117
|
+
return "video/avc"
|
|
118
|
+
}
|
|
119
|
+
if codecs.starts(with: "hvc1") {
|
|
120
|
+
return "video/hevc"
|
|
121
|
+
}
|
|
122
|
+
if codecs.starts(with: "dvh1") {
|
|
123
|
+
return "video/dolby-vision"
|
|
124
|
+
}
|
|
125
|
+
if codecs.starts(with: "av11") {
|
|
126
|
+
return "video/av1"
|
|
127
|
+
}
|
|
128
|
+
return nil // Unknown codec
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// swiftlint:enable redundant_optional_initialization
|
|
132
|
+
|
|
133
|
+
// https://developer.apple.com/documentation/avfoundation/avpartialasyncproperty/formatdescriptions
|
|
134
|
+
private extension AVAssetTrack {
|
|
135
|
+
var mediaFormat: String {
|
|
136
|
+
get async throws {
|
|
137
|
+
var format = ""
|
|
138
|
+
let descriptions = try await load(.formatDescriptions)
|
|
139
|
+
for (index, formatDesc) in descriptions.enumerated() {
|
|
140
|
+
let subType = CMFormatDescriptionGetMediaSubType(formatDesc).toString()
|
|
141
|
+
|
|
142
|
+
// The reported subType is different for iOS and Android, ideally they should be the same
|
|
143
|
+
let correctedSubType: String
|
|
144
|
+
switch subType {
|
|
145
|
+
case "avc1": // H264 videos
|
|
146
|
+
correctedSubType = "avc"
|
|
147
|
+
case "hev1": // H265 videos
|
|
148
|
+
correctedSubType = "hevc"
|
|
149
|
+
default:
|
|
150
|
+
correctedSubType = subType
|
|
151
|
+
}
|
|
152
|
+
format += "video/\(correctedSubType)"
|
|
153
|
+
if index < descriptions.count - 1 {
|
|
154
|
+
format += ","
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return format
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private extension FourCharCode {
|
|
163
|
+
// Create a string representation of a FourCC.
|
|
164
|
+
func toString() -> String {
|
|
165
|
+
let bytes: [CChar] = [
|
|
166
|
+
CChar((self >> 24) & 0xff),
|
|
167
|
+
CChar((self >> 16) & 0xff),
|
|
168
|
+
CChar((self >> 8) & 0xff),
|
|
169
|
+
CChar(self & 0xff),
|
|
170
|
+
0
|
|
171
|
+
]
|
|
172
|
+
let result = String(cString: bytes)
|
|
173
|
+
let characterSet = CharacterSet.whitespaces
|
|
174
|
+
return result.trimmingCharacters(in: characterSet)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
|
|
4
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
5
|
+
internal struct StatusChangedEventPayload: Record {
|
|
6
|
+
@Field var status: PlayerStatus = .idle
|
|
7
|
+
@Field var oldStatus: PlayerStatus? = nil
|
|
8
|
+
@Field var error: PlaybackError? = nil
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
internal struct IsPlayingEventPayload: Record {
|
|
12
|
+
@Field var isPlaying: Bool = false
|
|
13
|
+
@Field var oldIsPlaying: Bool? = nil
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
internal struct VolumeChangedEventPayload: Record {
|
|
17
|
+
@Field var volume: Float = 1
|
|
18
|
+
@Field var oldVolume: Float? = nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
internal struct MutedChangedEventPayload: Record {
|
|
22
|
+
@Field var muted: Bool = false
|
|
23
|
+
@Field var oldMuted: Bool? = nil
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
internal struct SourceChangedEventPayload: Record {
|
|
27
|
+
@Field var source: VideoSource? = nil
|
|
28
|
+
@Field var oldSource: VideoSource? = nil
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
internal struct PlaybackRateChangedEventPayload: Record {
|
|
32
|
+
@Field var playbackRate: Float = 1
|
|
33
|
+
@Field var oldPlaybackRate: Float? = nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal struct SubtitleTracksChangedEventPayload: Record {
|
|
37
|
+
@Field var availableSubtitleTracks: [SubtitleTrack] = []
|
|
38
|
+
@Field var oldAvailableSubtitleTracks: [SubtitleTrack] = []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
internal struct SubtitleTrackChangedEventPayload: Record {
|
|
42
|
+
@Field var subtitleTrack: SubtitleTrack? = nil
|
|
43
|
+
@Field var oldSubtitleTrack: SubtitleTrack? = nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
internal struct AudioTracksChangedEventPayload: Record {
|
|
47
|
+
@Field var availableAudioTracks: [AudioTrack] = []
|
|
48
|
+
@Field var oldAvailableAudioTracks: [AudioTrack] = []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
internal struct AudioTrackChangedEventPayload: Record {
|
|
52
|
+
@Field var audioTrack: AudioTrack? = nil
|
|
53
|
+
@Field var oldAudioTrack: AudioTrack? = nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
internal struct TimeUpdate: Record {
|
|
57
|
+
@Field var currentTime: Double = 0
|
|
58
|
+
@Field var currentLiveTimestamp: Double? = nil
|
|
59
|
+
@Field var currentOffsetFromLive: Double? = nil
|
|
60
|
+
@Field var bufferedPosition: Double = -1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
internal struct VideoTrackChangedEventPayload: Record {
|
|
64
|
+
@Field var videoTrack: VideoTrack? = nil
|
|
65
|
+
@Field var oldVideoTrack: VideoTrack? = nil
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
internal struct VideoSourceLoadedEventPayload: Record {
|
|
69
|
+
@Field var videoSource: VideoSource? = nil
|
|
70
|
+
@Field var duration: Double? = nil
|
|
71
|
+
@Field var availableVideoTracks: [VideoTrack]? = nil
|
|
72
|
+
@Field var availableSubtitleTracks: [SubtitleTrack]? = nil
|
|
73
|
+
@Field var availableAudioTracks: [AudioTrack]? = nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// swiftlint:enable redundant_optional_initialization
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
6
|
+
internal struct VideoMetadata: Record {
|
|
7
|
+
@Field
|
|
8
|
+
var title: String? = nil
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var artist: String? = nil
|
|
12
|
+
|
|
13
|
+
@Field
|
|
14
|
+
var artwork: URL? = nil
|
|
15
|
+
}
|
|
16
|
+
// swiftlint:enable redundant_optional_initialization
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
4
|
+
internal struct VideoSize: Record {
|
|
5
|
+
@Field
|
|
6
|
+
var width: Int? = nil
|
|
7
|
+
|
|
8
|
+
@Field
|
|
9
|
+
var height: Int? = nil
|
|
10
|
+
|
|
11
|
+
static func from(_ size: CGSize) -> Self {
|
|
12
|
+
return VideoSize(width: Int(size.width), height: Int(size.height))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// swiftlint:enable redundant_optional_initialization
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2024-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
|
|
5
|
+
// swiftlint:disable redundant_optional_initialization - Initialization with nil is necessary
|
|
6
|
+
internal struct VideoSource: Record {
|
|
7
|
+
@Field
|
|
8
|
+
var uri: URL? = nil
|
|
9
|
+
|
|
10
|
+
@Field
|
|
11
|
+
var drm: DRMOptions? = nil
|
|
12
|
+
|
|
13
|
+
@Field
|
|
14
|
+
var metadata: VideoMetadata? = nil
|
|
15
|
+
|
|
16
|
+
@Field
|
|
17
|
+
var headers: [String: String]? = nil
|
|
18
|
+
|
|
19
|
+
@Field
|
|
20
|
+
var useCaching: Bool = false
|
|
21
|
+
|
|
22
|
+
@Field
|
|
23
|
+
var contentType: ContentType = .auto
|
|
24
|
+
}
|
|
25
|
+
// swiftlint:enable redundant_optional_initialization
|
|
@@ -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
|
+
}
|