@neoskola/auto-play 0.2.18 → 0.3.1
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/ios/hybrid/HybridNowPlayingTemplate.swift +90 -0
- package/ios/templates/NowPlayingTemplate.swift +251 -11
- package/lib/specs/NowPlayingTemplate.nitro.d.ts +6 -0
- package/lib/templates/NowPlayingTemplate.d.ts +8 -0
- package/lib/templates/NowPlayingTemplate.js +18 -0
- package/nitrogen/generated/android/ReactNativeAutoPlayOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNowPlayingTemplateSpec.cpp +92 -0
- package/nitrogen/generated/android/c++/JHybridNowPlayingTemplateSpec.hpp +6 -0
- package/nitrogen/generated/android/c++/JNowPlayingTemplateConfig.hpp +27 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/Func_void_double_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridNowPlayingTemplateSpec.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NowPlayingTemplateConfig.kt +11 -5
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +58 -0
- package/nitrogen/generated/ios/c++/HybridNowPlayingTemplateSpecSwift.hpp +48 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_double_double.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridNowPlayingTemplateSpec.swift +6 -0
- package/nitrogen/generated/ios/swift/HybridNowPlayingTemplateSpec_cxx.swift +114 -0
- package/nitrogen/generated/ios/swift/NowPlayingTemplateConfig.swift +83 -1
- package/nitrogen/generated/shared/c++/HybridNowPlayingTemplateSpec.cpp +6 -0
- package/nitrogen/generated/shared/c++/HybridNowPlayingTemplateSpec.hpp +6 -0
- package/nitrogen/generated/shared/c++/NowPlayingTemplateConfig.hpp +10 -2
- package/package.json +1 -1
- package/src/specs/NowPlayingTemplate.nitro.ts +8 -0
- package/src/templates/NowPlayingTemplate.ts +26 -0
|
@@ -58,4 +58,94 @@ class HybridNowPlayingTemplate: HybridNowPlayingTemplateSpec {
|
|
|
58
58
|
await template.updateElapsedTime(elapsedTime: elapsedTime, duration: duration)
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
// MARK: - Native Audio Playback
|
|
63
|
+
|
|
64
|
+
func playAudio(
|
|
65
|
+
templateId: String,
|
|
66
|
+
url: String,
|
|
67
|
+
startFrom: Double
|
|
68
|
+
) throws -> Promise<Bool> {
|
|
69
|
+
return Promise.async {
|
|
70
|
+
guard
|
|
71
|
+
let template = TemplateStore.getTemplate(templateId: templateId)
|
|
72
|
+
as? NowPlayingTemplate
|
|
73
|
+
else {
|
|
74
|
+
throw AutoPlayError.templateNotFound(templateId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return await template.playAudio(url: url, startFrom: startFrom)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func pauseAudio(templateId: String) throws -> Promise<Void> {
|
|
82
|
+
return Promise.async {
|
|
83
|
+
guard
|
|
84
|
+
let template = TemplateStore.getTemplate(templateId: templateId)
|
|
85
|
+
as? NowPlayingTemplate
|
|
86
|
+
else {
|
|
87
|
+
throw AutoPlayError.templateNotFound(templateId)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await template.pauseAudio()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func resumeAudio(templateId: String) throws -> Promise<Void> {
|
|
95
|
+
return Promise.async {
|
|
96
|
+
guard
|
|
97
|
+
let template = TemplateStore.getTemplate(templateId: templateId)
|
|
98
|
+
as? NowPlayingTemplate
|
|
99
|
+
else {
|
|
100
|
+
throw AutoPlayError.templateNotFound(templateId)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await template.resumeAudio()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func seekForward(
|
|
108
|
+
templateId: String,
|
|
109
|
+
seconds: Double
|
|
110
|
+
) throws -> Promise<Void> {
|
|
111
|
+
return Promise.async {
|
|
112
|
+
guard
|
|
113
|
+
let template = TemplateStore.getTemplate(templateId: templateId)
|
|
114
|
+
as? NowPlayingTemplate
|
|
115
|
+
else {
|
|
116
|
+
throw AutoPlayError.templateNotFound(templateId)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await template.seekForward(seconds: seconds)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func seekBackward(
|
|
124
|
+
templateId: String,
|
|
125
|
+
seconds: Double
|
|
126
|
+
) throws -> Promise<Void> {
|
|
127
|
+
return Promise.async {
|
|
128
|
+
guard
|
|
129
|
+
let template = TemplateStore.getTemplate(templateId: templateId)
|
|
130
|
+
as? NowPlayingTemplate
|
|
131
|
+
else {
|
|
132
|
+
throw AutoPlayError.templateNotFound(templateId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await template.seekBackward(seconds: seconds)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func stopAudio(templateId: String) throws -> Promise<Void> {
|
|
140
|
+
return Promise.async {
|
|
141
|
+
guard
|
|
142
|
+
let template = TemplateStore.getTemplate(templateId: templateId)
|
|
143
|
+
as? NowPlayingTemplate
|
|
144
|
+
else {
|
|
145
|
+
throw AutoPlayError.templateNotFound(templateId)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await template.stopAudio()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
61
151
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import CarPlay
|
|
2
2
|
import MediaPlayer
|
|
3
|
+
import AVFoundation
|
|
3
4
|
|
|
4
5
|
class NowPlayingTemplate: AutoPlayTemplate {
|
|
5
6
|
var template: CPNowPlayingTemplate
|
|
@@ -9,6 +10,16 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
9
10
|
private var currentElapsedTime: Double = 0
|
|
10
11
|
private var currentDuration: Double = 0
|
|
11
12
|
|
|
13
|
+
// Native audio player
|
|
14
|
+
private var player: AVPlayer?
|
|
15
|
+
private var playerItem: AVPlayerItem?
|
|
16
|
+
private var timeObserver: Any?
|
|
17
|
+
private var progressReportTimer: Timer?
|
|
18
|
+
private var lastReportedSecond: Int = 0
|
|
19
|
+
private var didFinishObserver: NSObjectProtocol?
|
|
20
|
+
private var statusObservation: NSKeyValueObservation?
|
|
21
|
+
private var completionFired = false
|
|
22
|
+
|
|
12
23
|
var autoDismissMs: Double? {
|
|
13
24
|
return config.autoDismissMs
|
|
14
25
|
}
|
|
@@ -42,12 +53,213 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
|
|
56
|
+
// MARK: - Native Audio Playback
|
|
57
|
+
|
|
58
|
+
@MainActor
|
|
59
|
+
func playAudio(url: String, startFrom: Double) -> Bool {
|
|
60
|
+
// Clean up any existing player
|
|
61
|
+
cleanupPlayer()
|
|
62
|
+
completionFired = false
|
|
63
|
+
lastReportedSecond = Int(startFrom)
|
|
64
|
+
|
|
65
|
+
guard let audioURL = URL(string: url) else {
|
|
66
|
+
print("[NowPlayingTemplate] Invalid audio URL: \(url)")
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ensure AVAudioSession is active
|
|
71
|
+
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
72
|
+
|
|
73
|
+
let asset = AVURLAsset(url: audioURL)
|
|
74
|
+
playerItem = AVPlayerItem(asset: asset)
|
|
75
|
+
|
|
76
|
+
// KVO: detect duration as soon as asset loads (critical for progress bar)
|
|
77
|
+
statusObservation = playerItem?.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
78
|
+
guard item.status == .readyToPlay else {
|
|
79
|
+
if item.status == .failed {
|
|
80
|
+
print("[NowPlayingTemplate] AVPlayerItem failed: \(item.error?.localizedDescription ?? "unknown")")
|
|
81
|
+
}
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
DispatchQueue.main.async { [weak self] in
|
|
85
|
+
guard let self = self else { return }
|
|
86
|
+
let duration = CMTimeGetSeconds(item.duration)
|
|
87
|
+
if !duration.isNaN && !duration.isInfinite && duration > 0 {
|
|
88
|
+
self.currentDuration = duration
|
|
89
|
+
self.updateNowPlayingInfo()
|
|
90
|
+
print("[NowPlayingTemplate] Duration resolved via KVO: \(duration)s")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
player = AVPlayer(playerItem: playerItem)
|
|
96
|
+
|
|
97
|
+
// Seek to start position
|
|
98
|
+
if startFrom > 0 {
|
|
99
|
+
let time = CMTime(seconds: startFrom, preferredTimescale: 600)
|
|
100
|
+
player?.seek(to: time)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Periodic time observer (every 1 second) for MPNowPlayingInfoCenter updates
|
|
104
|
+
let interval = CMTime(seconds: 1.0, preferredTimescale: 600)
|
|
105
|
+
timeObserver = player?.addPeriodicTimeObserver(
|
|
106
|
+
forInterval: interval,
|
|
107
|
+
queue: .main
|
|
108
|
+
) { [weak self] time in
|
|
109
|
+
self?.handleTimeUpdate(time: time)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Playback finished notification
|
|
113
|
+
didFinishObserver = NotificationCenter.default.addObserver(
|
|
114
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
|
115
|
+
object: playerItem,
|
|
116
|
+
queue: .main
|
|
117
|
+
) { [weak self] _ in
|
|
118
|
+
self?.handlePlaybackFinished()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Progress report timer (every 30 seconds) — calls JS callback for backend reporting
|
|
122
|
+
progressReportTimer = Timer.scheduledTimer(
|
|
123
|
+
withTimeInterval: 30.0,
|
|
124
|
+
repeats: true
|
|
125
|
+
) { [weak self] _ in
|
|
126
|
+
self?.reportProgress()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Play
|
|
130
|
+
player?.play()
|
|
131
|
+
|
|
132
|
+
// Update NowPlaying UI
|
|
133
|
+
config.isPlaying = true
|
|
134
|
+
updateNowPlayingInfo()
|
|
135
|
+
MPNowPlayingInfoCenter.default().playbackState = .playing
|
|
136
|
+
|
|
137
|
+
print("[NowPlayingTemplate] Native audio playback started: \(url)")
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func handleTimeUpdate(time: CMTime) {
|
|
142
|
+
let currentTime = CMTimeGetSeconds(time)
|
|
143
|
+
let duration = CMTimeGetSeconds(playerItem?.duration ?? .zero)
|
|
144
|
+
|
|
145
|
+
guard !currentTime.isNaN else { return }
|
|
146
|
+
|
|
147
|
+
currentElapsedTime = currentTime
|
|
148
|
+
if !duration.isNaN && !duration.isInfinite && duration > 0 {
|
|
149
|
+
currentDuration = duration
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Use existing info or create fresh if nil (race condition safety)
|
|
153
|
+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
|
|
154
|
+
|
|
155
|
+
// Ensure title/artist are present if this is a fresh dictionary
|
|
156
|
+
if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
|
|
157
|
+
let titleText = Parser.parseText(text: config.title) ?? ""
|
|
158
|
+
nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
|
|
159
|
+
if let subtitle = config.subtitle {
|
|
160
|
+
nowPlayingInfo[MPMediaItemPropertyArtist] = Parser.parseText(text: subtitle)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if currentDuration > 0 {
|
|
165
|
+
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentDuration
|
|
166
|
+
}
|
|
167
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
|
|
168
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
169
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
170
|
+
|
|
171
|
+
// 95% completion check
|
|
172
|
+
if !completionFired && currentDuration > 0 && currentTime / currentDuration >= 0.95 {
|
|
173
|
+
completionFired = true
|
|
174
|
+
config.onComplete?()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func reportProgress() {
|
|
179
|
+
let currentSecond = Int(currentElapsedTime)
|
|
180
|
+
guard currentSecond != lastReportedSecond && currentSecond > 0 else { return }
|
|
181
|
+
lastReportedSecond = currentSecond
|
|
182
|
+
config.onProgressUpdate?(currentElapsedTime, currentDuration)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private func handlePlaybackFinished() {
|
|
186
|
+
config.isPlaying = false
|
|
187
|
+
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
188
|
+
// Fire completion if not already fired
|
|
189
|
+
if !completionFired {
|
|
190
|
+
completionFired = true
|
|
191
|
+
config.onComplete?()
|
|
192
|
+
}
|
|
193
|
+
config.onPlaybackFinished?()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@MainActor
|
|
197
|
+
func pauseAudio() {
|
|
198
|
+
player?.pause()
|
|
199
|
+
config.isPlaying = false
|
|
200
|
+
updatePlaybackState(isPlaying: false)
|
|
201
|
+
reportProgress()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@MainActor
|
|
205
|
+
func resumeAudio() {
|
|
206
|
+
NowPlayingSessionManager.shared.ensureSessionActive()
|
|
207
|
+
player?.play()
|
|
208
|
+
config.isPlaying = true
|
|
209
|
+
updatePlaybackState(isPlaying: true)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@MainActor
|
|
213
|
+
func seekForward(seconds: Double) {
|
|
214
|
+
guard let player = player else { return }
|
|
215
|
+
let newTime = CMTimeGetSeconds(player.currentTime()) + seconds
|
|
216
|
+
let clampedTime = min(newTime, currentDuration)
|
|
217
|
+
player.seek(to: CMTime(seconds: clampedTime, preferredTimescale: 600))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@MainActor
|
|
221
|
+
func seekBackward(seconds: Double) {
|
|
222
|
+
guard let player = player else { return }
|
|
223
|
+
let newTime = CMTimeGetSeconds(player.currentTime()) - seconds
|
|
224
|
+
let clampedTime = max(newTime, 0)
|
|
225
|
+
player.seek(to: CMTime(seconds: clampedTime, preferredTimescale: 600))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@MainActor
|
|
229
|
+
func stopAudio() {
|
|
230
|
+
reportProgress()
|
|
231
|
+
cleanupPlayer()
|
|
232
|
+
config.isPlaying = false
|
|
233
|
+
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private func cleanupPlayer() {
|
|
237
|
+
statusObservation?.invalidate()
|
|
238
|
+
statusObservation = nil
|
|
239
|
+
if let timeObserver = timeObserver {
|
|
240
|
+
player?.removeTimeObserver(timeObserver)
|
|
241
|
+
self.timeObserver = nil
|
|
242
|
+
}
|
|
243
|
+
if let didFinishObserver = didFinishObserver {
|
|
244
|
+
NotificationCenter.default.removeObserver(didFinishObserver)
|
|
245
|
+
self.didFinishObserver = nil
|
|
246
|
+
}
|
|
247
|
+
progressReportTimer?.invalidate()
|
|
248
|
+
progressReportTimer = nil
|
|
249
|
+
player?.pause()
|
|
250
|
+
player = nil
|
|
251
|
+
playerItem = nil
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// MARK: - CarPlay UI
|
|
255
|
+
|
|
45
256
|
private func setupNowPlayingButtons() {
|
|
46
257
|
var buttons: [CPNowPlayingButton] = []
|
|
47
258
|
|
|
48
259
|
let skipBackButton = CPNowPlayingImageButton(
|
|
49
260
|
image: UIImage(systemName: "gobackward.30")!
|
|
50
261
|
) { [weak self] _ in
|
|
262
|
+
self?.seekBackward(seconds: 30)
|
|
51
263
|
self?.config.onSkipBackward?()
|
|
52
264
|
}
|
|
53
265
|
buttons.append(skipBackButton)
|
|
@@ -55,6 +267,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
55
267
|
let skipForwardButton = CPNowPlayingImageButton(
|
|
56
268
|
image: UIImage(systemName: "goforward.30")!
|
|
57
269
|
) { [weak self] _ in
|
|
270
|
+
self?.seekForward(seconds: 30)
|
|
58
271
|
self?.config.onSkipForward?()
|
|
59
272
|
}
|
|
60
273
|
buttons.append(skipForwardButton)
|
|
@@ -97,6 +310,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
97
310
|
commandCenter.playCommand.isEnabled = true
|
|
98
311
|
commandCenter.playCommand.removeTarget(nil)
|
|
99
312
|
commandCenter.playCommand.addTarget { [weak self] _ in
|
|
313
|
+
self?.resumeAudio()
|
|
100
314
|
self?.config.onPlay?()
|
|
101
315
|
return .success
|
|
102
316
|
}
|
|
@@ -104,6 +318,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
104
318
|
commandCenter.pauseCommand.isEnabled = true
|
|
105
319
|
commandCenter.pauseCommand.removeTarget(nil)
|
|
106
320
|
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
|
321
|
+
self?.pauseAudio()
|
|
107
322
|
self?.config.onPause?()
|
|
108
323
|
return .success
|
|
109
324
|
}
|
|
@@ -112,6 +327,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
112
327
|
commandCenter.skipForwardCommand.preferredIntervals = [30]
|
|
113
328
|
commandCenter.skipForwardCommand.removeTarget(nil)
|
|
114
329
|
commandCenter.skipForwardCommand.addTarget { [weak self] _ in
|
|
330
|
+
self?.seekForward(seconds: 30)
|
|
115
331
|
self?.config.onSkipForward?()
|
|
116
332
|
return .success
|
|
117
333
|
}
|
|
@@ -120,6 +336,7 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
120
336
|
commandCenter.skipBackwardCommand.preferredIntervals = [30]
|
|
121
337
|
commandCenter.skipBackwardCommand.removeTarget(nil)
|
|
122
338
|
commandCenter.skipBackwardCommand.addTarget { [weak self] _ in
|
|
339
|
+
self?.seekBackward(seconds: 30)
|
|
123
340
|
self?.config.onSkipBackward?()
|
|
124
341
|
return .success
|
|
125
342
|
}
|
|
@@ -131,6 +348,8 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
131
348
|
let positionEvent = event as? MPChangePlaybackPositionCommandEvent else {
|
|
132
349
|
return .commandFailed
|
|
133
350
|
}
|
|
351
|
+
let time = CMTime(seconds: positionEvent.positionTime, preferredTimescale: 600)
|
|
352
|
+
self.player?.seek(to: time)
|
|
134
353
|
self.currentElapsedTime = positionEvent.positionTime
|
|
135
354
|
self.updateNowPlayingInfo()
|
|
136
355
|
return .success
|
|
@@ -153,6 +372,9 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
153
372
|
}
|
|
154
373
|
}
|
|
155
374
|
|
|
375
|
+
// MARK: - AutoPlayTemplate Protocol
|
|
376
|
+
|
|
377
|
+
@MainActor
|
|
156
378
|
func invalidate() {
|
|
157
379
|
setupNowPlayingButtons()
|
|
158
380
|
updateNowPlayingInfo()
|
|
@@ -180,6 +402,8 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
180
402
|
|
|
181
403
|
func onPopped() {
|
|
182
404
|
config.onPopped?()
|
|
405
|
+
cleanupPlayer()
|
|
406
|
+
|
|
183
407
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
184
408
|
commandCenter.playCommand.removeTarget(nil)
|
|
185
409
|
commandCenter.pauseCommand.removeTarget(nil)
|
|
@@ -207,13 +431,21 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
207
431
|
isSetupComplete = true
|
|
208
432
|
}
|
|
209
433
|
|
|
210
|
-
// Update playback rate
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
434
|
+
// Update playback rate — use existing info or create fresh if nil
|
|
435
|
+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
|
|
436
|
+
|
|
437
|
+
if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
|
|
438
|
+
let titleText = Parser.parseText(text: config.title) ?? ""
|
|
439
|
+
nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
|
|
440
|
+
if let subtitle = config.subtitle {
|
|
441
|
+
nowPlayingInfo[MPMediaItemPropertyArtist] = Parser.parseText(text: subtitle)
|
|
442
|
+
}
|
|
215
443
|
}
|
|
216
444
|
|
|
445
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0
|
|
446
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentElapsedTime
|
|
447
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
448
|
+
|
|
217
449
|
MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused
|
|
218
450
|
}
|
|
219
451
|
|
|
@@ -229,12 +461,20 @@ class NowPlayingTemplate: AutoPlayTemplate {
|
|
|
229
461
|
self.currentElapsedTime = elapsedTime
|
|
230
462
|
self.currentDuration = duration
|
|
231
463
|
|
|
232
|
-
// Update
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
464
|
+
// Update time-related fields — use existing info or create fresh if nil
|
|
465
|
+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
|
|
466
|
+
|
|
467
|
+
if nowPlayingInfo[MPMediaItemPropertyTitle] == nil {
|
|
468
|
+
let titleText = Parser.parseText(text: config.title) ?? ""
|
|
469
|
+
nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
|
|
470
|
+
if let subtitle = config.subtitle {
|
|
471
|
+
nowPlayingInfo[MPMediaItemPropertyArtist] = Parser.parseText(text: subtitle)
|
|
472
|
+
}
|
|
238
473
|
}
|
|
474
|
+
|
|
475
|
+
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
|
|
476
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsedTime
|
|
477
|
+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = config.isPlaying ? 1.0 : 0.0
|
|
478
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
239
479
|
}
|
|
240
480
|
}
|
|
@@ -11,5 +11,11 @@ export interface NowPlayingTemplate extends HybridObject<{
|
|
|
11
11
|
updateNowPlayingTemplatePlaybackState(templateId: string, isPlaying: boolean): Promise<void>;
|
|
12
12
|
updateNowPlayingTemplateInfo(templateId: string, title: string, subtitle: string): Promise<void>;
|
|
13
13
|
updateNowPlayingTemplateElapsedTime(templateId: string, elapsedTime: number, duration: number): Promise<void>;
|
|
14
|
+
playAudio(templateId: string, url: string, startFrom: number): Promise<boolean>;
|
|
15
|
+
pauseAudio(templateId: string): Promise<void>;
|
|
16
|
+
resumeAudio(templateId: string): Promise<void>;
|
|
17
|
+
seekForward(templateId: string, seconds: number): Promise<void>;
|
|
18
|
+
seekBackward(templateId: string, seconds: number): Promise<void>;
|
|
19
|
+
stopAudio(templateId: string): Promise<void>;
|
|
14
20
|
}
|
|
15
21
|
export {};
|
|
@@ -14,6 +14,8 @@ export interface NitroNowPlayingTemplateConfig extends TemplateConfig {
|
|
|
14
14
|
onSkipForward?: () => void;
|
|
15
15
|
onSkipBackward?: () => void;
|
|
16
16
|
onComplete?: () => void;
|
|
17
|
+
onProgressUpdate?: (currentTime: number, duration: number) => void;
|
|
18
|
+
onPlaybackFinished?: () => void;
|
|
17
19
|
}
|
|
18
20
|
export type NowPlayingTemplateConfig = Omit<NitroNowPlayingTemplateConfig, 'image'> & {
|
|
19
21
|
image?: AutoImage;
|
|
@@ -23,4 +25,10 @@ export declare class NowPlayingTemplate extends Template<NowPlayingTemplateConfi
|
|
|
23
25
|
updatePlaybackState(isPlaying: boolean): Promise<void>;
|
|
24
26
|
updateNowPlayingInfo(title: string, subtitle: string): Promise<void>;
|
|
25
27
|
updateElapsedTime(elapsedTime: number, duration: number): Promise<void>;
|
|
28
|
+
playAudio(url: string, startFrom?: number): Promise<boolean>;
|
|
29
|
+
pauseAudio(): Promise<void>;
|
|
30
|
+
resumeAudio(): Promise<void>;
|
|
31
|
+
seekForward(seconds?: number): Promise<void>;
|
|
32
|
+
seekBackward(seconds?: number): Promise<void>;
|
|
33
|
+
stopAudio(): Promise<void>;
|
|
26
34
|
}
|
|
@@ -22,4 +22,22 @@ export class NowPlayingTemplate extends Template {
|
|
|
22
22
|
updateElapsedTime(elapsedTime, duration) {
|
|
23
23
|
return HybridNowPlayingTemplate.updateNowPlayingTemplateElapsedTime(this.id, elapsedTime, duration);
|
|
24
24
|
}
|
|
25
|
+
playAudio(url, startFrom = 0) {
|
|
26
|
+
return HybridNowPlayingTemplate.playAudio(this.id, url, startFrom);
|
|
27
|
+
}
|
|
28
|
+
pauseAudio() {
|
|
29
|
+
return HybridNowPlayingTemplate.pauseAudio(this.id);
|
|
30
|
+
}
|
|
31
|
+
resumeAudio() {
|
|
32
|
+
return HybridNowPlayingTemplate.resumeAudio(this.id);
|
|
33
|
+
}
|
|
34
|
+
seekForward(seconds = 30) {
|
|
35
|
+
return HybridNowPlayingTemplate.seekForward(this.id, seconds);
|
|
36
|
+
}
|
|
37
|
+
seekBackward(seconds = 30) {
|
|
38
|
+
return HybridNowPlayingTemplate.seekBackward(this.id, seconds);
|
|
39
|
+
}
|
|
40
|
+
stopAudio() {
|
|
41
|
+
return HybridNowPlayingTemplate.stopAudio(this.id);
|
|
42
|
+
}
|
|
25
43
|
}
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
#include "JHybridMessageTemplateSpec.hpp"
|
|
46
46
|
#include "JHybridNeoSkolaTemplateSpec.hpp"
|
|
47
47
|
#include "JHybridNowPlayingTemplateSpec.hpp"
|
|
48
|
+
#include "JFunc_void_double_double.hpp"
|
|
48
49
|
#include "JHybridSearchTemplateSpec.hpp"
|
|
49
50
|
#include "JHybridSectionListTemplateSpec.hpp"
|
|
50
51
|
#include "JHybridSubscriptionGateTemplateSpec.hpp"
|
|
@@ -89,6 +90,7 @@ int initialize(JavaVM* vm) {
|
|
|
89
90
|
margelo::nitro::swe::iternio::reactnativeautoplay::JHybridMessageTemplateSpec::registerNatives();
|
|
90
91
|
margelo::nitro::swe::iternio::reactnativeautoplay::JHybridNeoSkolaTemplateSpec::registerNatives();
|
|
91
92
|
margelo::nitro::swe::iternio::reactnativeautoplay::JHybridNowPlayingTemplateSpec::registerNatives();
|
|
93
|
+
margelo::nitro::swe::iternio::reactnativeautoplay::JFunc_void_double_double_cxx::registerNatives();
|
|
92
94
|
margelo::nitro::swe::iternio::reactnativeautoplay::JHybridSearchTemplateSpec::registerNatives();
|
|
93
95
|
margelo::nitro::swe::iternio::reactnativeautoplay::JHybridSectionListTemplateSpec::registerNatives();
|
|
94
96
|
margelo::nitro::swe::iternio::reactnativeautoplay::JHybridSubscriptionGateTemplateSpec::registerNatives();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JFunc_void_double_double.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © 2026 Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <functional>
|
|
12
|
+
|
|
13
|
+
#include <functional>
|
|
14
|
+
#include <NitroModules/JNICallable.hpp>
|
|
15
|
+
|
|
16
|
+
namespace margelo::nitro::swe::iternio::reactnativeautoplay {
|
|
17
|
+
|
|
18
|
+
using namespace facebook;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents the Java/Kotlin callback `(currentTime: Double, duration: Double) -> Unit`.
|
|
22
|
+
* This can be passed around between C++ and Java/Kotlin.
|
|
23
|
+
*/
|
|
24
|
+
struct JFunc_void_double_double: public jni::JavaClass<JFunc_void_double_double> {
|
|
25
|
+
public:
|
|
26
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/swe/iternio/reactnativeautoplay/Func_void_double_double;";
|
|
27
|
+
|
|
28
|
+
public:
|
|
29
|
+
/**
|
|
30
|
+
* Invokes the function this `JFunc_void_double_double` instance holds through JNI.
|
|
31
|
+
*/
|
|
32
|
+
void invoke(double currentTime, double duration) const {
|
|
33
|
+
static const auto method = javaClassStatic()->getMethod<void(double /* currentTime */, double /* duration */)>("invoke");
|
|
34
|
+
method(self(), currentTime, duration);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An implementation of Func_void_double_double that is backed by a C++ implementation (using `std::function<...>`)
|
|
40
|
+
*/
|
|
41
|
+
class JFunc_void_double_double_cxx final: public jni::HybridClass<JFunc_void_double_double_cxx, JFunc_void_double_double> {
|
|
42
|
+
public:
|
|
43
|
+
static jni::local_ref<JFunc_void_double_double::javaobject> fromCpp(const std::function<void(double /* currentTime */, double /* duration */)>& func) {
|
|
44
|
+
return JFunc_void_double_double_cxx::newObjectCxxArgs(func);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public:
|
|
48
|
+
/**
|
|
49
|
+
* Invokes the C++ `std::function<...>` this `JFunc_void_double_double_cxx` instance holds.
|
|
50
|
+
*/
|
|
51
|
+
void invoke_cxx(double currentTime, double duration) {
|
|
52
|
+
_func(currentTime, duration);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public:
|
|
56
|
+
[[nodiscard]]
|
|
57
|
+
inline const std::function<void(double /* currentTime */, double /* duration */)>& getFunction() const {
|
|
58
|
+
return _func;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public:
|
|
62
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/swe/iternio/reactnativeautoplay/Func_void_double_double_cxx;";
|
|
63
|
+
static void registerNatives() {
|
|
64
|
+
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_double_double_cxx::invoke_cxx)});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private:
|
|
68
|
+
explicit JFunc_void_double_double_cxx(const std::function<void(double /* currentTime */, double /* duration */)>& func): _func(func) { }
|
|
69
|
+
|
|
70
|
+
private:
|
|
71
|
+
friend HybridBase;
|
|
72
|
+
std::function<void(double /* currentTime */, double /* duration */)> _func;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
} // namespace margelo::nitro::swe::iternio::reactnativeautoplay
|