@scr2em/capacitor-plugin-recorder 0.0.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/CapacitorPluginRecorder.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +395 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/capacitor/recorderplayer/RecorderPlayer.java +524 -0
- package/android/src/main/java/com/capacitor/recorderplayer/RecorderPlayerPlugin.java +302 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +562 -0
- package/dist/esm/definitions.d.ts +125 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +30 -0
- package/dist/esm/web.js +279 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +293 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +296 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/RecorderPlayerPlugin/RecorderPlayer.swift +318 -0
- package/ios/Sources/RecorderPlayerPlugin/RecorderPlayerPlugin.swift +249 -0
- package/ios/Tests/RecorderPlayerPluginTests/RecorderPlayerTests.swift +15 -0
- package/package.json +80 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
|
|
4
|
+
public enum RecordingStatus: String {
|
|
5
|
+
case recording = "recording"
|
|
6
|
+
case paused = "paused"
|
|
7
|
+
case stopped = "stopped"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public enum PlaybackStatus: String {
|
|
11
|
+
case playing = "playing"
|
|
12
|
+
case paused = "paused"
|
|
13
|
+
case stopped = "stopped"
|
|
14
|
+
case ended = "ended"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public protocol RecorderPlayerDelegate: AnyObject {
|
|
18
|
+
func onRecordingStatusChange(status: RecordingStatus, duration: Int)
|
|
19
|
+
func onPlaybackStatusChange(playerId: String, status: PlaybackStatus, currentPosition: Int, duration: Int)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Internal class to hold player state
|
|
23
|
+
private class PlayerState: NSObject, AVAudioPlayerDelegate {
|
|
24
|
+
let playerId: String
|
|
25
|
+
let audioPlayer: AVAudioPlayer
|
|
26
|
+
var status: PlaybackStatus = .stopped
|
|
27
|
+
var playbackTimer: Timer?
|
|
28
|
+
weak var delegate: RecorderPlayerDelegate?
|
|
29
|
+
|
|
30
|
+
init(playerId: String, audioPlayer: AVAudioPlayer) {
|
|
31
|
+
self.playerId = playerId
|
|
32
|
+
self.audioPlayer = audioPlayer
|
|
33
|
+
super.init()
|
|
34
|
+
self.audioPlayer.delegate = self
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func startTimer() {
|
|
38
|
+
stopTimer()
|
|
39
|
+
playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
40
|
+
self?.notifyStatusChange()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func stopTimer() {
|
|
45
|
+
playbackTimer?.invalidate()
|
|
46
|
+
playbackTimer = nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func notifyStatusChange() {
|
|
50
|
+
let currentPosition = Int(audioPlayer.currentTime * 1000)
|
|
51
|
+
let duration = Int(audioPlayer.duration * 1000)
|
|
52
|
+
delegate?.onPlaybackStatusChange(playerId: playerId, status: status, currentPosition: currentPosition, duration: duration)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// AVAudioPlayerDelegate
|
|
56
|
+
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
57
|
+
status = .ended
|
|
58
|
+
stopTimer()
|
|
59
|
+
notifyStatusChange()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
|
|
63
|
+
status = .stopped
|
|
64
|
+
stopTimer()
|
|
65
|
+
notifyStatusChange()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
deinit {
|
|
69
|
+
stopTimer()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc public class RecorderPlayer: NSObject {
|
|
74
|
+
private var audioRecorder: AVAudioRecorder?
|
|
75
|
+
private var recordingSession: AVAudioSession?
|
|
76
|
+
|
|
77
|
+
private var recordingStatus: RecordingStatus = .stopped
|
|
78
|
+
|
|
79
|
+
private var recordingStartTime: Date?
|
|
80
|
+
private var pausedDuration: TimeInterval = 0
|
|
81
|
+
private var currentRecordingPath: URL?
|
|
82
|
+
|
|
83
|
+
// Multiple players support
|
|
84
|
+
private var players: [String: PlayerState] = [:]
|
|
85
|
+
private var playerIdCounter: Int = 0
|
|
86
|
+
|
|
87
|
+
public weak var delegate: RecorderPlayerDelegate?
|
|
88
|
+
|
|
89
|
+
public override init() {
|
|
90
|
+
super.init()
|
|
91
|
+
setupAudioSession()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func setupAudioSession() {
|
|
95
|
+
recordingSession = AVAudioSession.sharedInstance()
|
|
96
|
+
do {
|
|
97
|
+
try recordingSession?.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth, .mixWithOthers])
|
|
98
|
+
try recordingSession?.setActive(true)
|
|
99
|
+
} catch {
|
|
100
|
+
print("Failed to setup audio session: \(error)")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func generatePlayerId() -> String {
|
|
105
|
+
playerIdCounter += 1
|
|
106
|
+
return "player-\(playerIdCounter)-\(Int(Date().timeIntervalSince1970 * 1000))"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@objc public func startRecord(path: String?) throws {
|
|
110
|
+
guard recordingStatus == .stopped else {
|
|
111
|
+
throw NSError(domain: "RecorderPlayer", code: 1, userInfo: [NSLocalizedDescriptionKey: "Recording already in progress"])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Use Application Support directory + bundle identifier (matches Capacitor's Directory.Data)
|
|
115
|
+
let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
116
|
+
let dataDir = appSupportDir.appendingPathComponent(Bundle.main.bundleIdentifier ?? "")
|
|
117
|
+
// Create directory if it doesn't exist
|
|
118
|
+
try? FileManager.default.createDirectory(at: dataDir, withIntermediateDirectories: true)
|
|
119
|
+
|
|
120
|
+
let audioPath: URL
|
|
121
|
+
if let path = path {
|
|
122
|
+
// Use relative path within data directory
|
|
123
|
+
audioPath = dataDir.appendingPathComponent(path)
|
|
124
|
+
// Create parent directories if needed
|
|
125
|
+
let parentDir = audioPath.deletingLastPathComponent()
|
|
126
|
+
try? FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true)
|
|
127
|
+
} else {
|
|
128
|
+
let audioFilename = "recording_\(Int(Date().timeIntervalSince1970)).m4a"
|
|
129
|
+
audioPath = dataDir.appendingPathComponent(audioFilename)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
currentRecordingPath = audioPath
|
|
133
|
+
|
|
134
|
+
let settings: [String: Any] = [
|
|
135
|
+
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
|
136
|
+
AVSampleRateKey: 44100,
|
|
137
|
+
AVNumberOfChannelsKey: 1,
|
|
138
|
+
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
do {
|
|
142
|
+
audioRecorder = try AVAudioRecorder(url: audioPath, settings: settings)
|
|
143
|
+
audioRecorder?.record()
|
|
144
|
+
recordingStartTime = Date()
|
|
145
|
+
pausedDuration = 0
|
|
146
|
+
recordingStatus = .recording
|
|
147
|
+
notifyRecordingStatusChange()
|
|
148
|
+
} catch {
|
|
149
|
+
throw NSError(domain: "RecorderPlayer", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to start recording: \(error.localizedDescription)"])
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@objc public func stopRecord() throws -> (path: String, duration: Int) {
|
|
154
|
+
guard let recorder = audioRecorder, recordingStatus != .stopped else {
|
|
155
|
+
throw NSError(domain: "RecorderPlayer", code: 3, userInfo: [NSLocalizedDescriptionKey: "No recording in progress"])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let duration = Int(recorder.currentTime * 1000)
|
|
159
|
+
recorder.stop()
|
|
160
|
+
|
|
161
|
+
let path = currentRecordingPath?.path ?? ""
|
|
162
|
+
|
|
163
|
+
audioRecorder = nil
|
|
164
|
+
recordingStatus = .stopped
|
|
165
|
+
notifyRecordingStatusChange()
|
|
166
|
+
|
|
167
|
+
return (path, duration)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@objc public func pauseRecord() throws {
|
|
171
|
+
guard let recorder = audioRecorder, recordingStatus == .recording else {
|
|
172
|
+
throw NSError(domain: "RecorderPlayer", code: 4, userInfo: [NSLocalizedDescriptionKey: "No active recording to pause"])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
recorder.pause()
|
|
176
|
+
pausedDuration = recorder.currentTime
|
|
177
|
+
recordingStatus = .paused
|
|
178
|
+
notifyRecordingStatusChange()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@objc public func resumeRecord() throws {
|
|
182
|
+
guard let recorder = audioRecorder, recordingStatus == .paused else {
|
|
183
|
+
throw NSError(domain: "RecorderPlayer", code: 5, userInfo: [NSLocalizedDescriptionKey: "No paused recording to resume"])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
recorder.record()
|
|
187
|
+
recordingStatus = .recording
|
|
188
|
+
notifyRecordingStatusChange()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@objc public func preparePlay(path: String) throws -> (playerId: String, duration: Int) {
|
|
192
|
+
// Use Application Support directory + bundle identifier (matches Capacitor's Directory.Data)
|
|
193
|
+
let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
194
|
+
let dataDir = appSupportDir.appendingPathComponent(Bundle.main.bundleIdentifier ?? "")
|
|
195
|
+
let url = dataDir.appendingPathComponent(path)
|
|
196
|
+
|
|
197
|
+
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
198
|
+
throw NSError(domain: "RecorderPlayer", code: 6, userInfo: [NSLocalizedDescriptionKey: "Audio file not found at path: \(path)"])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
do {
|
|
202
|
+
let audioPlayer = try AVAudioPlayer(contentsOf: url)
|
|
203
|
+
audioPlayer.prepareToPlay()
|
|
204
|
+
|
|
205
|
+
let playerId = generatePlayerId()
|
|
206
|
+
let playerState = PlayerState(playerId: playerId, audioPlayer: audioPlayer)
|
|
207
|
+
playerState.delegate = delegate
|
|
208
|
+
players[playerId] = playerState
|
|
209
|
+
|
|
210
|
+
let duration = Int(audioPlayer.duration * 1000)
|
|
211
|
+
playerState.notifyStatusChange()
|
|
212
|
+
|
|
213
|
+
return (playerId, duration)
|
|
214
|
+
} catch {
|
|
215
|
+
throw NSError(domain: "RecorderPlayer", code: 7, userInfo: [NSLocalizedDescriptionKey: "Failed to load audio: \(error.localizedDescription)"])
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@objc public func play(playerId: String) throws {
|
|
220
|
+
guard let playerState = players[playerId] else {
|
|
221
|
+
throw NSError(domain: "RecorderPlayer", code: 8, userInfo: [NSLocalizedDescriptionKey: "No player found with ID: \(playerId)"])
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
playerState.audioPlayer.play()
|
|
225
|
+
playerState.status = .playing
|
|
226
|
+
playerState.startTimer()
|
|
227
|
+
playerState.notifyStatusChange()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@objc public func pausePlay(playerId: String) throws {
|
|
231
|
+
guard let playerState = players[playerId] else {
|
|
232
|
+
throw NSError(domain: "RecorderPlayer", code: 9, userInfo: [NSLocalizedDescriptionKey: "No player found with ID: \(playerId)"])
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
guard playerState.status == .playing else {
|
|
236
|
+
throw NSError(domain: "RecorderPlayer", code: 9, userInfo: [NSLocalizedDescriptionKey: "No audio playing to pause"])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
playerState.audioPlayer.pause()
|
|
240
|
+
playerState.status = .paused
|
|
241
|
+
playerState.stopTimer()
|
|
242
|
+
playerState.notifyStatusChange()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@objc public func resumePlay(playerId: String) throws {
|
|
246
|
+
guard let playerState = players[playerId] else {
|
|
247
|
+
throw NSError(domain: "RecorderPlayer", code: 10, userInfo: [NSLocalizedDescriptionKey: "No player found with ID: \(playerId)"])
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
guard playerState.status == .paused else {
|
|
251
|
+
throw NSError(domain: "RecorderPlayer", code: 10, userInfo: [NSLocalizedDescriptionKey: "No paused audio to resume"])
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
playerState.audioPlayer.play()
|
|
255
|
+
playerState.status = .playing
|
|
256
|
+
playerState.startTimer()
|
|
257
|
+
playerState.notifyStatusChange()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@objc public func stopPlay(playerId: String) throws {
|
|
261
|
+
guard let playerState = players[playerId] else {
|
|
262
|
+
throw NSError(domain: "RecorderPlayer", code: 11, userInfo: [NSLocalizedDescriptionKey: "No player found with ID: \(playerId)"])
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
playerState.audioPlayer.stop()
|
|
266
|
+
playerState.audioPlayer.currentTime = 0
|
|
267
|
+
playerState.status = .stopped
|
|
268
|
+
playerState.stopTimer()
|
|
269
|
+
playerState.notifyStatusChange()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@objc public func seekTo(playerId: String, position: Int) throws {
|
|
273
|
+
guard let playerState = players[playerId] else {
|
|
274
|
+
throw NSError(domain: "RecorderPlayer", code: 12, userInfo: [NSLocalizedDescriptionKey: "No player found with ID: \(playerId)"])
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let positionInSeconds = TimeInterval(position) / 1000.0
|
|
278
|
+
guard positionInSeconds >= 0 && positionInSeconds <= playerState.audioPlayer.duration else {
|
|
279
|
+
throw NSError(domain: "RecorderPlayer", code: 13, userInfo: [NSLocalizedDescriptionKey: "Invalid seek position"])
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
playerState.audioPlayer.currentTime = positionInSeconds
|
|
283
|
+
playerState.notifyStatusChange()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@objc public func getPlaybackStatus(playerId: String) -> (status: String, currentPosition: Int, duration: Int)? {
|
|
287
|
+
guard let playerState = players[playerId] else {
|
|
288
|
+
return nil
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let currentPosition = Int(playerState.audioPlayer.currentTime * 1000)
|
|
292
|
+
let duration = Int(playerState.audioPlayer.duration * 1000)
|
|
293
|
+
return (playerState.status.rawValue, currentPosition, duration)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@objc public func getRecordingStatus() -> (status: String, duration: Int) {
|
|
297
|
+
var duration = 0
|
|
298
|
+
if let recorder = audioRecorder {
|
|
299
|
+
duration = Int(recorder.currentTime * 1000)
|
|
300
|
+
}
|
|
301
|
+
return (recordingStatus.rawValue, duration)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@objc public func destroyPlayer(playerId: String) {
|
|
305
|
+
guard let playerState = players[playerId] else {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
playerState.audioPlayer.stop()
|
|
310
|
+
playerState.stopTimer()
|
|
311
|
+
players.removeValue(forKey: playerId)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private func notifyRecordingStatusChange() {
|
|
315
|
+
let status = getRecordingStatus()
|
|
316
|
+
delegate?.onRecordingStatusChange(status: recordingStatus, duration: status.duration)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
@objc(RecorderPlayerPlugin)
|
|
6
|
+
public class RecorderPlayerPlugin: CAPPlugin, CAPBridgedPlugin, RecorderPlayerDelegate {
|
|
7
|
+
public let identifier = "RecorderPlayerPlugin"
|
|
8
|
+
public let jsName = "RecorderPlayer"
|
|
9
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
10
|
+
CAPPluginMethod(name: "requestPermission", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "checkPermission", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "startRecord", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "stopRecord", returnType: CAPPluginReturnPromise),
|
|
14
|
+
CAPPluginMethod(name: "pauseRecord", returnType: CAPPluginReturnPromise),
|
|
15
|
+
CAPPluginMethod(name: "resumeRecord", returnType: CAPPluginReturnPromise),
|
|
16
|
+
CAPPluginMethod(name: "preparePlay", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "play", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "pausePlay", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "resumePlay", returnType: CAPPluginReturnPromise),
|
|
20
|
+
CAPPluginMethod(name: "stopPlay", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "seekTo", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "getPlaybackStatus", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "getRecordingStatus", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "destroyPlayer", returnType: CAPPluginReturnPromise)
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
private lazy var implementation: RecorderPlayer = {
|
|
28
|
+
let recorder = RecorderPlayer()
|
|
29
|
+
recorder.delegate = self
|
|
30
|
+
return recorder
|
|
31
|
+
}()
|
|
32
|
+
|
|
33
|
+
@objc func requestPermission(_ call: CAPPluginCall) {
|
|
34
|
+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
35
|
+
DispatchQueue.main.async {
|
|
36
|
+
call.resolve([
|
|
37
|
+
"microphone": granted ? "granted" : "denied"
|
|
38
|
+
])
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@objc func checkPermission(_ call: CAPPluginCall) {
|
|
44
|
+
let status = AVAudioSession.sharedInstance().recordPermission
|
|
45
|
+
var permissionStatus: String
|
|
46
|
+
|
|
47
|
+
switch status {
|
|
48
|
+
case .granted:
|
|
49
|
+
permissionStatus = "granted"
|
|
50
|
+
case .denied:
|
|
51
|
+
permissionStatus = "denied"
|
|
52
|
+
case .undetermined:
|
|
53
|
+
permissionStatus = "prompt"
|
|
54
|
+
@unknown default:
|
|
55
|
+
permissionStatus = "prompt"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
call.resolve([
|
|
59
|
+
"microphone": permissionStatus
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@objc func startRecord(_ call: CAPPluginCall) {
|
|
64
|
+
let path = call.getString("path")
|
|
65
|
+
do {
|
|
66
|
+
try implementation.startRecord(path: path)
|
|
67
|
+
call.resolve()
|
|
68
|
+
} catch {
|
|
69
|
+
call.reject(error.localizedDescription)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc func stopRecord(_ call: CAPPluginCall) {
|
|
74
|
+
do {
|
|
75
|
+
let result = try implementation.stopRecord()
|
|
76
|
+
call.resolve([
|
|
77
|
+
"path": result.path,
|
|
78
|
+
"duration": result.duration
|
|
79
|
+
])
|
|
80
|
+
} catch {
|
|
81
|
+
call.reject(error.localizedDescription)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@objc func pauseRecord(_ call: CAPPluginCall) {
|
|
86
|
+
do {
|
|
87
|
+
try implementation.pauseRecord()
|
|
88
|
+
call.resolve()
|
|
89
|
+
} catch {
|
|
90
|
+
call.reject(error.localizedDescription)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@objc func resumeRecord(_ call: CAPPluginCall) {
|
|
95
|
+
do {
|
|
96
|
+
try implementation.resumeRecord()
|
|
97
|
+
call.resolve()
|
|
98
|
+
} catch {
|
|
99
|
+
call.reject(error.localizedDescription)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@objc func preparePlay(_ call: CAPPluginCall) {
|
|
104
|
+
guard let path = call.getString("path") else {
|
|
105
|
+
call.reject("Path is required")
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
do {
|
|
110
|
+
let result = try implementation.preparePlay(path: path)
|
|
111
|
+
call.resolve([
|
|
112
|
+
"playerId": result.playerId,
|
|
113
|
+
"duration": result.duration
|
|
114
|
+
])
|
|
115
|
+
} catch {
|
|
116
|
+
call.reject(error.localizedDescription)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@objc func play(_ call: CAPPluginCall) {
|
|
121
|
+
guard let playerId = call.getString("playerId") else {
|
|
122
|
+
call.reject("playerId is required")
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
do {
|
|
127
|
+
try implementation.play(playerId: playerId)
|
|
128
|
+
call.resolve()
|
|
129
|
+
} catch {
|
|
130
|
+
call.reject(error.localizedDescription)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@objc func pausePlay(_ call: CAPPluginCall) {
|
|
135
|
+
guard let playerId = call.getString("playerId") else {
|
|
136
|
+
call.reject("playerId is required")
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
do {
|
|
141
|
+
try implementation.pausePlay(playerId: playerId)
|
|
142
|
+
call.resolve()
|
|
143
|
+
} catch {
|
|
144
|
+
call.reject(error.localizedDescription)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@objc func resumePlay(_ call: CAPPluginCall) {
|
|
149
|
+
guard let playerId = call.getString("playerId") else {
|
|
150
|
+
call.reject("playerId is required")
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
do {
|
|
155
|
+
try implementation.resumePlay(playerId: playerId)
|
|
156
|
+
call.resolve()
|
|
157
|
+
} catch {
|
|
158
|
+
call.reject(error.localizedDescription)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@objc func stopPlay(_ call: CAPPluginCall) {
|
|
163
|
+
guard let playerId = call.getString("playerId") else {
|
|
164
|
+
call.reject("playerId is required")
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
do {
|
|
169
|
+
try implementation.stopPlay(playerId: playerId)
|
|
170
|
+
call.resolve()
|
|
171
|
+
} catch {
|
|
172
|
+
call.reject(error.localizedDescription)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@objc func seekTo(_ call: CAPPluginCall) {
|
|
177
|
+
guard let playerId = call.getString("playerId") else {
|
|
178
|
+
call.reject("playerId is required")
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
guard let position = call.getInt("position") else {
|
|
183
|
+
call.reject("Position is required")
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
do {
|
|
188
|
+
try implementation.seekTo(playerId: playerId, position: position)
|
|
189
|
+
call.resolve()
|
|
190
|
+
} catch {
|
|
191
|
+
call.reject(error.localizedDescription)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@objc func getPlaybackStatus(_ call: CAPPluginCall) {
|
|
196
|
+
guard let playerId = call.getString("playerId") else {
|
|
197
|
+
call.reject("playerId is required")
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
guard let status = implementation.getPlaybackStatus(playerId: playerId) else {
|
|
202
|
+
call.reject("No player found with ID: \(playerId)")
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
call.resolve([
|
|
207
|
+
"playerId": playerId,
|
|
208
|
+
"status": status.status,
|
|
209
|
+
"currentPosition": status.currentPosition,
|
|
210
|
+
"duration": status.duration
|
|
211
|
+
])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@objc func getRecordingStatus(_ call: CAPPluginCall) {
|
|
215
|
+
let status = implementation.getRecordingStatus()
|
|
216
|
+
call.resolve([
|
|
217
|
+
"status": status.status,
|
|
218
|
+
"duration": status.duration
|
|
219
|
+
])
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@objc func destroyPlayer(_ call: CAPPluginCall) {
|
|
223
|
+
guard let playerId = call.getString("playerId") else {
|
|
224
|
+
call.reject("playerId is required")
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
implementation.destroyPlayer(playerId: playerId)
|
|
229
|
+
call.resolve()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// MARK: - RecorderPlayerDelegate
|
|
233
|
+
|
|
234
|
+
public func onRecordingStatusChange(status: RecordingStatus, duration: Int) {
|
|
235
|
+
notifyListeners("recordingStatusChange", data: [
|
|
236
|
+
"status": status.rawValue,
|
|
237
|
+
"duration": duration
|
|
238
|
+
])
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public func onPlaybackStatusChange(playerId: String, status: PlaybackStatus, currentPosition: Int, duration: Int) {
|
|
242
|
+
notifyListeners("playbackStatusChange", data: [
|
|
243
|
+
"playerId": playerId,
|
|
244
|
+
"status": status.rawValue,
|
|
245
|
+
"currentPosition": currentPosition,
|
|
246
|
+
"duration": duration
|
|
247
|
+
])
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import RecorderPlayerPlugin
|
|
3
|
+
|
|
4
|
+
class RecorderPlayerTests: XCTestCase {
|
|
5
|
+
func testEcho() {
|
|
6
|
+
// This is an example of a functional test case for a plugin.
|
|
7
|
+
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
+
|
|
9
|
+
let implementation = RecorderPlayer()
|
|
10
|
+
let value = "Hello, World!"
|
|
11
|
+
let result = implementation.echo(value)
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(value, result)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scr2em/capacitor-plugin-recorder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ee",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapacitorPluginRecorder.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "me",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"capacitor",
|
|
29
|
+
"plugin",
|
|
30
|
+
"native"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
34
|
+
"verify:ios": "xcodebuild -scheme CapacitorPluginRecorder -destination generic/platform=iOS",
|
|
35
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
36
|
+
"verify:web": "npm run build",
|
|
37
|
+
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
38
|
+
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
39
|
+
"eslint": "eslint . --ext ts",
|
|
40
|
+
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
41
|
+
"swiftlint": "node-swiftlint",
|
|
42
|
+
"docgen": "docgen --api RecorderPlayerPlugin --output-readme README.md --output-json dist/docs.json",
|
|
43
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
44
|
+
"clean": "rimraf ./dist",
|
|
45
|
+
"watch": "tsc --watch",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@capacitor/android": "^8.0.0",
|
|
50
|
+
"@capacitor/core": "^8.0.0",
|
|
51
|
+
"@capacitor/docgen": "^0.3.1",
|
|
52
|
+
"@capacitor/ios": "^8.0.0",
|
|
53
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
54
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
55
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
56
|
+
"eslint": "^8.57.1",
|
|
57
|
+
"prettier": "^3.6.2",
|
|
58
|
+
"prettier-plugin-java": "^2.7.7",
|
|
59
|
+
"rimraf": "^6.1.0",
|
|
60
|
+
"rollup": "^4.53.2",
|
|
61
|
+
"swiftlint": "^2.0.0",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@capacitor/core": ">=8.0.0"
|
|
66
|
+
},
|
|
67
|
+
"prettier": "@ionic/prettier-config",
|
|
68
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
69
|
+
"eslintConfig": {
|
|
70
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
71
|
+
},
|
|
72
|
+
"capacitor": {
|
|
73
|
+
"ios": {
|
|
74
|
+
"src": "ios"
|
|
75
|
+
},
|
|
76
|
+
"android": {
|
|
77
|
+
"src": "android"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|