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.
@@ -0,0 +1,524 @@
1
+ package com.capacitor.recorderplayer;
2
+
3
+ import android.content.Context;
4
+ import android.media.AudioAttributes;
5
+ import android.media.AudioManager;
6
+ import android.media.MediaPlayer;
7
+ import android.media.MediaRecorder;
8
+ import android.os.Build;
9
+ import android.os.Handler;
10
+ import android.os.Looper;
11
+ import android.util.Log;
12
+
13
+ import java.io.File;
14
+ import java.io.IOException;
15
+ import java.util.HashMap;
16
+ import java.util.Map;
17
+ import java.util.concurrent.atomic.AtomicInteger;
18
+
19
+ public class RecorderPlayer {
20
+
21
+ private static final String TAG = "RecorderPlayer";
22
+
23
+ public enum RecordingStatus {
24
+ RECORDING("recording"),
25
+ PAUSED("paused"),
26
+ STOPPED("stopped");
27
+
28
+ private final String value;
29
+
30
+ RecordingStatus(String value) {
31
+ this.value = value;
32
+ }
33
+
34
+ public String getValue() {
35
+ return value;
36
+ }
37
+ }
38
+
39
+ public enum PlaybackStatus {
40
+ PLAYING("playing"),
41
+ PAUSED("paused"),
42
+ STOPPED("stopped"),
43
+ ENDED("ended");
44
+
45
+ private final String value;
46
+
47
+ PlaybackStatus(String value) {
48
+ this.value = value;
49
+ }
50
+
51
+ public String getValue() {
52
+ return value;
53
+ }
54
+ }
55
+
56
+ public interface RecorderPlayerListener {
57
+ void onRecordingStatusChange(RecordingStatus status, int duration);
58
+ void onPlaybackStatusChange(String playerId, PlaybackStatus status, int currentPosition, int duration);
59
+ }
60
+
61
+ // Inner class to hold player state
62
+ private static class PlayerState {
63
+ final String playerId;
64
+ final MediaPlayer mediaPlayer;
65
+ PlaybackStatus status = PlaybackStatus.STOPPED;
66
+ Handler playbackHandler;
67
+ Runnable playbackRunnable;
68
+
69
+ PlayerState(String playerId, MediaPlayer mediaPlayer) {
70
+ this.playerId = playerId;
71
+ this.mediaPlayer = mediaPlayer;
72
+ this.playbackHandler = new Handler(Looper.getMainLooper());
73
+ }
74
+
75
+ void startTimer(RecorderPlayerListener listener) {
76
+ stopTimer();
77
+ playbackRunnable = new Runnable() {
78
+ @Override
79
+ public void run() {
80
+ if (status == PlaybackStatus.PLAYING && mediaPlayer != null) {
81
+ notifyStatusChange(listener);
82
+ playbackHandler.postDelayed(this, 250);
83
+ }
84
+ }
85
+ };
86
+ playbackHandler.postDelayed(playbackRunnable, 250);
87
+ }
88
+
89
+ void stopTimer() {
90
+ if (playbackRunnable != null) {
91
+ playbackHandler.removeCallbacks(playbackRunnable);
92
+ playbackRunnable = null;
93
+ }
94
+ }
95
+
96
+ void notifyStatusChange(RecorderPlayerListener listener) {
97
+ if (listener != null && mediaPlayer != null) {
98
+ try {
99
+ int currentPosition = mediaPlayer.getCurrentPosition();
100
+ int duration = mediaPlayer.getDuration();
101
+ listener.onPlaybackStatusChange(playerId, status, currentPosition, duration);
102
+ } catch (IllegalStateException e) {
103
+ // MediaPlayer may be in invalid state
104
+ }
105
+ }
106
+ }
107
+
108
+ void release() {
109
+ stopTimer();
110
+ if (mediaPlayer != null) {
111
+ try {
112
+ mediaPlayer.stop();
113
+ } catch (IllegalStateException ignored) {
114
+ }
115
+ mediaPlayer.release();
116
+ }
117
+ }
118
+ }
119
+
120
+ private final Context context;
121
+ private final AudioManager audioManager;
122
+ private MediaRecorder mediaRecorder;
123
+
124
+ private RecordingStatus recordingStatus = RecordingStatus.STOPPED;
125
+
126
+ private String currentRecordingPath;
127
+ private long recordingStartTime;
128
+ private long pausedDuration;
129
+
130
+ // Multiple players support
131
+ private final Map<String, PlayerState> players = new HashMap<>();
132
+ private final AtomicInteger playerIdCounter = new AtomicInteger(0);
133
+
134
+ private RecorderPlayerListener listener;
135
+
136
+ public RecorderPlayer(Context context) {
137
+ this.context = context;
138
+ this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
139
+ }
140
+
141
+ public void setListener(RecorderPlayerListener listener) {
142
+ this.listener = listener;
143
+ }
144
+
145
+ private String generatePlayerId() {
146
+ return "player-" + playerIdCounter.incrementAndGet() + "-" + System.currentTimeMillis();
147
+ }
148
+
149
+ public void startRecord(String path) throws Exception {
150
+ if (recordingStatus != RecordingStatus.STOPPED) {
151
+ throw new Exception("Recording already in progress");
152
+ }
153
+
154
+ // Use files directory (matches Capacitor's Directory.Data)
155
+ File dataDir = context.getFilesDir();
156
+
157
+ File audioFile;
158
+ if (path != null) {
159
+ // Use relative path within data directory
160
+ audioFile = new File(dataDir, path);
161
+ // Create parent directories if needed
162
+ File parentDir = audioFile.getParentFile();
163
+ if (parentDir != null && !parentDir.exists()) {
164
+ parentDir.mkdirs();
165
+ }
166
+ } else {
167
+ String audioFilename = "recording_" + System.currentTimeMillis() + ".m4a";
168
+ audioFile = new File(dataDir, audioFilename);
169
+ }
170
+ currentRecordingPath = audioFile.getAbsolutePath();
171
+
172
+ Log.d(TAG, "Starting recording to: " + currentRecordingPath);
173
+
174
+ try {
175
+ mediaRecorder = new MediaRecorder();
176
+ mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
177
+ mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
178
+ mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
179
+ mediaRecorder.setAudioSamplingRate(44100);
180
+ mediaRecorder.setAudioEncodingBitRate(128000);
181
+ mediaRecorder.setOutputFile(currentRecordingPath);
182
+ mediaRecorder.prepare();
183
+ mediaRecorder.start();
184
+
185
+ recordingStartTime = System.currentTimeMillis();
186
+ pausedDuration = 0;
187
+ recordingStatus = RecordingStatus.RECORDING;
188
+ notifyRecordingStatusChange();
189
+ } catch (IOException e) {
190
+ Log.e(TAG, "Failed to start recording: " + e.getMessage());
191
+ throw new Exception("Failed to start recording: " + e.getMessage());
192
+ }
193
+ }
194
+
195
+ public RecordResult stopRecord() throws Exception {
196
+ if (mediaRecorder == null || recordingStatus == RecordingStatus.STOPPED) {
197
+ throw new Exception("No recording in progress");
198
+ }
199
+
200
+ int duration = (int) (System.currentTimeMillis() - recordingStartTime - pausedDuration);
201
+
202
+ try {
203
+ mediaRecorder.stop();
204
+ mediaRecorder.release();
205
+ } catch (RuntimeException e) {
206
+ Log.e(TAG, "Error stopping recording: " + e.getMessage());
207
+ }
208
+
209
+ mediaRecorder = null;
210
+ String path = currentRecordingPath;
211
+ recordingStatus = RecordingStatus.STOPPED;
212
+ notifyRecordingStatusChange();
213
+
214
+ // Verify file exists and has content
215
+ File recordedFile = new File(path);
216
+ Log.d(TAG, "Recording stopped. File exists: " + recordedFile.exists() + ", size: " + recordedFile.length());
217
+
218
+ return new RecordResult(path, duration);
219
+ }
220
+
221
+ public void pauseRecord() throws Exception {
222
+ if (mediaRecorder == null || recordingStatus != RecordingStatus.RECORDING) {
223
+ throw new Exception("No active recording to pause");
224
+ }
225
+
226
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
227
+ mediaRecorder.pause();
228
+ pausedDuration = System.currentTimeMillis() - recordingStartTime;
229
+ recordingStatus = RecordingStatus.PAUSED;
230
+ notifyRecordingStatusChange();
231
+ } else {
232
+ throw new Exception("Pause recording is not supported on this Android version");
233
+ }
234
+ }
235
+
236
+ public void resumeRecord() throws Exception {
237
+ if (mediaRecorder == null || recordingStatus != RecordingStatus.PAUSED) {
238
+ throw new Exception("No paused recording to resume");
239
+ }
240
+
241
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
242
+ mediaRecorder.resume();
243
+ recordingStartTime = System.currentTimeMillis() - pausedDuration;
244
+ recordingStatus = RecordingStatus.RECORDING;
245
+ notifyRecordingStatusChange();
246
+ } else {
247
+ throw new Exception("Resume recording is not supported on this Android version");
248
+ }
249
+ }
250
+
251
+ public PreparePlayResult preparePlay(String path) throws Exception {
252
+ Log.d(TAG, "preparePlay called with path: " + path);
253
+
254
+ // Use files directory (matches Capacitor's Directory.Data)
255
+ File dataDir = context.getFilesDir();
256
+ File audioFile = new File(dataDir, path);
257
+ String absolutePath = audioFile.getAbsolutePath();
258
+
259
+ if (!audioFile.exists()) {
260
+ Log.e(TAG, "Audio file not found at path: " + path);
261
+ throw new Exception("Audio file not found at path: " + path);
262
+ }
263
+
264
+ Log.d(TAG, "Audio file exists, size: " + audioFile.length());
265
+
266
+ try {
267
+ MediaPlayer mediaPlayer = new MediaPlayer();
268
+
269
+ // Set audio attributes for music/media playback through speaker
270
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
271
+ AudioAttributes audioAttributes = new AudioAttributes.Builder()
272
+ .setUsage(AudioAttributes.USAGE_MEDIA)
273
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
274
+ .build();
275
+ mediaPlayer.setAudioAttributes(audioAttributes);
276
+ } else {
277
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
278
+ }
279
+
280
+ mediaPlayer.setDataSource(absolutePath);
281
+
282
+ String playerId = generatePlayerId();
283
+ PlayerState playerState = new PlayerState(playerId, mediaPlayer);
284
+ players.put(playerId, playerState);
285
+
286
+ mediaPlayer.setOnPreparedListener(mp -> {
287
+ Log.d(TAG, "MediaPlayer prepared, duration: " + mp.getDuration());
288
+ });
289
+
290
+ mediaPlayer.setOnCompletionListener(mp -> {
291
+ Log.d(TAG, "Playback completed for player: " + playerId);
292
+ PlayerState ps = players.get(playerId);
293
+ if (ps != null) {
294
+ ps.status = PlaybackStatus.ENDED;
295
+ ps.stopTimer();
296
+ ps.notifyStatusChange(listener);
297
+ }
298
+ });
299
+
300
+ mediaPlayer.setOnErrorListener((mp, what, extra) -> {
301
+ Log.e(TAG, "MediaPlayer error: what=" + what + ", extra=" + extra);
302
+ PlayerState ps = players.get(playerId);
303
+ if (ps != null) {
304
+ ps.status = PlaybackStatus.STOPPED;
305
+ ps.stopTimer();
306
+ ps.notifyStatusChange(listener);
307
+ }
308
+ return true;
309
+ });
310
+
311
+ mediaPlayer.prepare();
312
+
313
+ int duration = mediaPlayer.getDuration();
314
+ Log.d(TAG, "Audio prepared successfully, duration: " + duration + "ms, playerId: " + playerId);
315
+
316
+ playerState.notifyStatusChange(listener);
317
+ return new PreparePlayResult(playerId, duration);
318
+ } catch (IOException e) {
319
+ Log.e(TAG, "Failed to load audio: " + e.getMessage());
320
+ throw new Exception("Failed to load audio: " + e.getMessage());
321
+ }
322
+ }
323
+
324
+ public void play(String playerId) throws Exception {
325
+ Log.d(TAG, "play called for playerId: " + playerId);
326
+
327
+ PlayerState playerState = players.get(playerId);
328
+ if (playerState == null) {
329
+ throw new Exception("No player found with ID: " + playerId);
330
+ }
331
+
332
+ playerState.mediaPlayer.start();
333
+ playerState.status = PlaybackStatus.PLAYING;
334
+ playerState.startTimer(listener);
335
+ playerState.notifyStatusChange(listener);
336
+
337
+ Log.d(TAG, "Playback started, isPlaying: " + playerState.mediaPlayer.isPlaying());
338
+ }
339
+
340
+ public void pausePlay(String playerId) throws Exception {
341
+ PlayerState playerState = players.get(playerId);
342
+ if (playerState == null) {
343
+ throw new Exception("No player found with ID: " + playerId);
344
+ }
345
+
346
+ if (playerState.status != PlaybackStatus.PLAYING) {
347
+ throw new Exception("No audio playing to pause");
348
+ }
349
+
350
+ playerState.mediaPlayer.pause();
351
+ playerState.status = PlaybackStatus.PAUSED;
352
+ playerState.stopTimer();
353
+ playerState.notifyStatusChange(listener);
354
+ }
355
+
356
+ public void resumePlay(String playerId) throws Exception {
357
+ PlayerState playerState = players.get(playerId);
358
+ if (playerState == null) {
359
+ throw new Exception("No player found with ID: " + playerId);
360
+ }
361
+
362
+ if (playerState.status != PlaybackStatus.PAUSED) {
363
+ throw new Exception("No paused audio to resume");
364
+ }
365
+
366
+ playerState.mediaPlayer.start();
367
+ playerState.status = PlaybackStatus.PLAYING;
368
+ playerState.startTimer(listener);
369
+ playerState.notifyStatusChange(listener);
370
+ }
371
+
372
+ public void stopPlay(String playerId) throws Exception {
373
+ PlayerState playerState = players.get(playerId);
374
+ if (playerState == null) {
375
+ throw new Exception("No player found with ID: " + playerId);
376
+ }
377
+
378
+ playerState.mediaPlayer.stop();
379
+ playerState.mediaPlayer.seekTo(0);
380
+ try {
381
+ playerState.mediaPlayer.prepare();
382
+ } catch (IOException ignored) {
383
+ }
384
+ playerState.status = PlaybackStatus.STOPPED;
385
+ playerState.stopTimer();
386
+ playerState.notifyStatusChange(listener);
387
+ }
388
+
389
+ public void seekTo(String playerId, int position) throws Exception {
390
+ PlayerState playerState = players.get(playerId);
391
+ if (playerState == null) {
392
+ throw new Exception("No player found with ID: " + playerId);
393
+ }
394
+
395
+ int duration = playerState.mediaPlayer.getDuration();
396
+ if (position < 0 || position > duration) {
397
+ throw new Exception("Invalid seek position");
398
+ }
399
+
400
+ playerState.mediaPlayer.seekTo(position);
401
+ playerState.notifyStatusChange(listener);
402
+ }
403
+
404
+ public PlaybackStatusResult getPlaybackStatus(String playerId) {
405
+ PlayerState playerState = players.get(playerId);
406
+ if (playerState == null) {
407
+ return null;
408
+ }
409
+
410
+ int currentPosition = playerState.mediaPlayer.getCurrentPosition();
411
+ int duration = playerState.mediaPlayer.getDuration();
412
+ return new PlaybackStatusResult(playerId, playerState.status.getValue(), currentPosition, duration);
413
+ }
414
+
415
+ public RecordingStatusResult getRecordingStatus() {
416
+ int duration = 0;
417
+ if (recordingStatus == RecordingStatus.RECORDING) {
418
+ duration = (int) (System.currentTimeMillis() - recordingStartTime);
419
+ } else if (recordingStatus == RecordingStatus.PAUSED) {
420
+ duration = (int) pausedDuration;
421
+ }
422
+ return new RecordingStatusResult(recordingStatus.getValue(), duration);
423
+ }
424
+
425
+ public void destroyPlayer(String playerId) {
426
+ PlayerState playerState = players.remove(playerId);
427
+ if (playerState != null) {
428
+ playerState.release();
429
+ Log.d(TAG, "Destroyed player: " + playerId);
430
+ }
431
+ }
432
+
433
+ private void notifyRecordingStatusChange() {
434
+ if (listener != null) {
435
+ RecordingStatusResult status = getRecordingStatus();
436
+ listener.onRecordingStatusChange(recordingStatus, status.getDuration());
437
+ }
438
+ }
439
+
440
+ // Result classes
441
+ public static class RecordResult {
442
+ private final String path;
443
+ private final int duration;
444
+
445
+ public RecordResult(String path, int duration) {
446
+ this.path = path;
447
+ this.duration = duration;
448
+ }
449
+
450
+ public String getPath() {
451
+ return path;
452
+ }
453
+
454
+ public int getDuration() {
455
+ return duration;
456
+ }
457
+ }
458
+
459
+ public static class PreparePlayResult {
460
+ private final String playerId;
461
+ private final int duration;
462
+
463
+ public PreparePlayResult(String playerId, int duration) {
464
+ this.playerId = playerId;
465
+ this.duration = duration;
466
+ }
467
+
468
+ public String getPlayerId() {
469
+ return playerId;
470
+ }
471
+
472
+ public int getDuration() {
473
+ return duration;
474
+ }
475
+ }
476
+
477
+ public static class PlaybackStatusResult {
478
+ private final String playerId;
479
+ private final String status;
480
+ private final int currentPosition;
481
+ private final int duration;
482
+
483
+ public PlaybackStatusResult(String playerId, String status, int currentPosition, int duration) {
484
+ this.playerId = playerId;
485
+ this.status = status;
486
+ this.currentPosition = currentPosition;
487
+ this.duration = duration;
488
+ }
489
+
490
+ public String getPlayerId() {
491
+ return playerId;
492
+ }
493
+
494
+ public String getStatus() {
495
+ return status;
496
+ }
497
+
498
+ public int getCurrentPosition() {
499
+ return currentPosition;
500
+ }
501
+
502
+ public int getDuration() {
503
+ return duration;
504
+ }
505
+ }
506
+
507
+ public static class RecordingStatusResult {
508
+ private final String status;
509
+ private final int duration;
510
+
511
+ public RecordingStatusResult(String status, int duration) {
512
+ this.status = status;
513
+ this.duration = duration;
514
+ }
515
+
516
+ public String getStatus() {
517
+ return status;
518
+ }
519
+
520
+ public int getDuration() {
521
+ return duration;
522
+ }
523
+ }
524
+ }