@capgo/capacitor-video-player 8.2.4 → 8.2.7
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/com/capgo/videoplayer/VideoPlayerPlugin.java +1 -1
- package/ios/Sources/VideoPlayerPlugin/AVPlayerControlsVisibilityObserver.swift +227 -0
- package/ios/Sources/VideoPlayerPlugin/DismissObservingPlayerViewController.swift +20 -0
- package/ios/Sources/VideoPlayerPlugin/ExitEmissionGuard.swift +11 -0
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +485 -26
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerCastController.swift +200 -63
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +1 -1
- package/ios/Tests/VideoPlayerPluginTests/HLSSubtitlePlaylistTests.swift +10 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerCastControllerTests.swift +103 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerPluginTests.swift +9 -4
- package/package.json +1 -1
|
@@ -46,7 +46,7 @@ import org.json.JSONObject;
|
|
|
46
46
|
)
|
|
47
47
|
public class VideoPlayerPlugin extends Plugin {
|
|
48
48
|
|
|
49
|
-
private final String pluginVersion = "8.2.
|
|
49
|
+
private final String pluginVersion = "8.2.7";
|
|
50
50
|
|
|
51
51
|
// Permission alias constants
|
|
52
52
|
private static final String PERMISSION_DENIED_ERROR = "Unable to access media videos, user denied permission request";
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import AVKit
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Mirrors AVKit playback-control visibility for custom overlay views.
|
|
5
|
+
///
|
|
6
|
+
/// AVPlayerViewController does not expose control visibility publicly. This observer
|
|
7
|
+
/// locates the controls container using only the public view hierarchy and watches
|
|
8
|
+
/// `hidden` / `alpha` so overlay controls can follow native chrome.
|
|
9
|
+
final class AVPlayerControlsVisibilityObserver: NSObject {
|
|
10
|
+
private weak var playerViewController: AVPlayerViewController?
|
|
11
|
+
private weak var monitoredView: UIView?
|
|
12
|
+
private var onVisibilityChange: ((Bool) -> Void)?
|
|
13
|
+
private var retryWorkItem: DispatchWorkItem?
|
|
14
|
+
private var reanchorWorkItem: DispatchWorkItem?
|
|
15
|
+
private var hiddenObservation: NSKeyValueObservation?
|
|
16
|
+
private var alphaObservation: NSKeyValueObservation?
|
|
17
|
+
private var lastReportedVisible: Bool?
|
|
18
|
+
private var remainingInstallAttempts = 30
|
|
19
|
+
private var isActive = false
|
|
20
|
+
|
|
21
|
+
private static let reanchorInterval: TimeInterval = 0.25
|
|
22
|
+
|
|
23
|
+
deinit {
|
|
24
|
+
stop()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func start(
|
|
28
|
+
observing playerViewController: AVPlayerViewController,
|
|
29
|
+
onChange: @escaping (Bool) -> Void
|
|
30
|
+
) {
|
|
31
|
+
stop()
|
|
32
|
+
isActive = true
|
|
33
|
+
self.playerViewController = playerViewController
|
|
34
|
+
self.onVisibilityChange = onChange
|
|
35
|
+
remainingInstallAttempts = 30
|
|
36
|
+
installObserverIfPossible()
|
|
37
|
+
scheduleReanchorCheck()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func stop() {
|
|
41
|
+
isActive = false
|
|
42
|
+
retryWorkItem?.cancel()
|
|
43
|
+
retryWorkItem = nil
|
|
44
|
+
reanchorWorkItem?.cancel()
|
|
45
|
+
reanchorWorkItem = nil
|
|
46
|
+
removeMonitoredViewObserver()
|
|
47
|
+
playerViewController = nil
|
|
48
|
+
onVisibilityChange = nil
|
|
49
|
+
lastReportedVisible = nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static func controlsContainer(in playerView: UIView) -> UIView? {
|
|
53
|
+
guard let container = playerView.subviews.first else {
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
return container.subviews.last
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static func areControlsVisible(in controlsView: UIView?) -> Bool {
|
|
60
|
+
guard let controlsView else {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
return !controlsView.isHidden && controlsView.alpha > 0.05
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static func shouldReanchor(
|
|
67
|
+
monitoredView: UIView?,
|
|
68
|
+
currentControlsView: UIView?
|
|
69
|
+
) -> Bool {
|
|
70
|
+
guard let monitoredView else {
|
|
71
|
+
return currentControlsView != nil
|
|
72
|
+
}
|
|
73
|
+
guard let currentControlsView else {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
if monitoredView !== currentControlsView {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
return monitoredView.superview == nil && monitoredView.window == nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func installObserverIfPossible() {
|
|
83
|
+
guard isActive else {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
guard let playerView = playerViewController?.view else {
|
|
88
|
+
scheduleRetry()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
guard let controlsView = Self.controlsContainer(in: playerView) else {
|
|
93
|
+
scheduleRetry()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if monitoredView === controlsView {
|
|
98
|
+
reportVisibilityIfNeeded(for: controlsView)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeMonitoredViewObserver()
|
|
103
|
+
attachObserver(to: controlsView)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private func attachObserver(to controlsView: UIView) {
|
|
107
|
+
monitoredView = controlsView
|
|
108
|
+
hiddenObservation = controlsView.observe(\.isHidden, options: [.new]) { [weak self] view, _ in
|
|
109
|
+
self?.reportVisibilityIfNeeded(for: view)
|
|
110
|
+
}
|
|
111
|
+
alphaObservation = controlsView.observe(\.alpha, options: [.new]) { [weak self] view, _ in
|
|
112
|
+
self?.reportVisibilityIfNeeded(for: view)
|
|
113
|
+
}
|
|
114
|
+
reportVisibilityIfNeeded(for: controlsView)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func reanchorIfNeeded() {
|
|
118
|
+
guard isActive else {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
guard let playerView = playerViewController?.view else {
|
|
123
|
+
if monitoredView != nil {
|
|
124
|
+
removeMonitoredViewObserver()
|
|
125
|
+
}
|
|
126
|
+
scheduleRetry()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let currentControlsView = Self.controlsContainer(in: playerView)
|
|
131
|
+
guard Self.shouldReanchor(
|
|
132
|
+
monitoredView: monitoredView,
|
|
133
|
+
currentControlsView: currentControlsView
|
|
134
|
+
) else {
|
|
135
|
+
if let monitoredView {
|
|
136
|
+
reportVisibilityIfNeeded(for: monitoredView)
|
|
137
|
+
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
removeMonitoredViewObserver()
|
|
142
|
+
if let currentControlsView {
|
|
143
|
+
remainingInstallAttempts = 30
|
|
144
|
+
attachObserver(to: currentControlsView)
|
|
145
|
+
} else {
|
|
146
|
+
scheduleRetry()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func scheduleReanchorCheck() {
|
|
151
|
+
reanchorWorkItem?.cancel()
|
|
152
|
+
guard isActive else {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
157
|
+
guard let self, self.isActive else {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
self.reanchorIfNeeded()
|
|
161
|
+
self.scheduleReanchorCheck()
|
|
162
|
+
}
|
|
163
|
+
reanchorWorkItem = workItem
|
|
164
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.reanchorInterval, execute: workItem)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func scheduleRetry() {
|
|
168
|
+
guard isActive else {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
retryWorkItem?.cancel()
|
|
173
|
+
guard remainingInstallAttempts > 0 else {
|
|
174
|
+
lastReportedVisible = true
|
|
175
|
+
onVisibilityChange?(true)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
remainingInstallAttempts -= 1
|
|
179
|
+
|
|
180
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
181
|
+
guard let self, self.isActive else {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
self.installObserverIfPossible()
|
|
185
|
+
}
|
|
186
|
+
retryWorkItem = workItem
|
|
187
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func removeMonitoredViewObserver() {
|
|
191
|
+
hiddenObservation = nil
|
|
192
|
+
alphaObservation = nil
|
|
193
|
+
monitoredView = nil
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private func reportVisibilityIfNeeded(for controlsView: UIView) {
|
|
197
|
+
let visible = Self.areControlsVisible(in: controlsView)
|
|
198
|
+
guard lastReportedVisible != visible else {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
lastReportedVisible = visible
|
|
202
|
+
onVisibilityChange?(visible)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
enum VideoPlayerCastSeekSync {
|
|
207
|
+
static let minimumForwardDelta: Double = 0.25
|
|
208
|
+
|
|
209
|
+
static func shouldForwardLocalSeek(
|
|
210
|
+
localTime: Double,
|
|
211
|
+
lastForwardedTime: Double?
|
|
212
|
+
) -> Bool {
|
|
213
|
+
guard localTime.isFinite else {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
guard let lastForwardedTime else {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
return abs(localTime - lastForwardedTime) >= minimumForwardDelta
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
enum VideoPlayerCastOverlayLayout {
|
|
224
|
+
static func overlayView(for playerViewController: AVPlayerViewController) -> UIView? {
|
|
225
|
+
playerViewController.contentOverlayView
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import AVKit
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
final class DismissObservingPlayerViewController: AVPlayerViewController {
|
|
5
|
+
var onDismiss: (() -> Void)?
|
|
6
|
+
var shouldReportDismiss: (() -> Bool)?
|
|
7
|
+
weak var adaptivePresentationDelegate: UIAdaptivePresentationControllerDelegate?
|
|
8
|
+
|
|
9
|
+
override func viewWillAppear(_ animated: Bool) {
|
|
10
|
+
super.viewWillAppear(animated)
|
|
11
|
+
presentationController?.delegate = adaptivePresentationDelegate
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override func viewDidDisappear(_ animated: Bool) {
|
|
15
|
+
super.viewDidDisappear(animated)
|
|
16
|
+
guard isBeingDismissed else { return }
|
|
17
|
+
guard shouldReportDismiss?() ?? true else { return }
|
|
18
|
+
onDismiss?()
|
|
19
|
+
}
|
|
20
|
+
}
|