react-native-audio-api 0.6.0-rc.0 → 0.6.0-rc.2
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 +35 -22
- package/android/CMakeLists.txt +6 -3
- package/android/build.gradle +1 -0
- package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +73 -0
- package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h +37 -0
- package/android/src/main/cpp/audioapi/android/core/AudioPlayer.cpp +6 -10
- package/android/src/main/cpp/audioapi/android/core/AudioPlayer.h +2 -3
- package/android/src/main/java/com/swmansion/audioapi/AudioManagerModule.kt +19 -14
- package/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +60 -0
- package/android/src/main/java/com/swmansion/audioapi/system/LockScreenManager.kt +294 -0
- package/android/src/main/java/com/swmansion/audioapi/system/MediaNotificationManager.kt +279 -0
- package/android/src/main/java/com/swmansion/audioapi/system/MediaReceiver.kt +46 -0
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionCallback.kt +39 -0
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionEventEmitter.kt +88 -0
- package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +162 -0
- package/android/src/main/java/com/swmansion/audioapi/system/VolumeChangeListener.kt +27 -0
- package/android/src/main/res/drawable/next.xml +9 -0
- package/android/src/main/res/drawable/pause.xml +9 -0
- package/android/src/main/res/drawable/play.xml +9 -0
- package/android/src/main/res/drawable/previous.xml +9 -0
- package/android/src/main/res/drawable/skip_backward_5.xml +9 -0
- package/android/src/main/res/drawable/skip_forward_5.xml +9 -0
- package/android/src/main/res/drawable/stop.xml +9 -0
- package/app.plugin.js +1 -0
- package/common/cpp/audioapi/AudioAPIModuleInstaller.h +29 -5
- package/common/cpp/audioapi/HostObjects/AnalyserNodeHostObject.h +1 -0
- package/common/cpp/audioapi/HostObjects/AudioRecorderHostObject.h +149 -0
- package/common/cpp/audioapi/core/AudioContext.cpp +4 -3
- package/common/cpp/audioapi/core/BaseAudioContext.cpp +6 -6
- package/common/cpp/audioapi/core/inputs/AudioRecorder.h +38 -0
- package/common/cpp/audioapi/core/sources/AudioBufferSourceNode.cpp +1 -6
- package/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.cpp +8 -4
- package/common/cpp/audioapi/core/sources/AudioScheduledSourceNode.h +6 -0
- package/common/cpp/audioapi/core/sources/OscillatorNode.cpp +1 -1
- package/common/cpp/audioapi/core/utils/AudioNodeDestructor.cpp +3 -3
- package/common/cpp/audioapi/core/utils/AudioNodeManager.cpp +45 -11
- package/common/cpp/audioapi/core/utils/AudioNodeManager.h +6 -2
- package/ios/audioapi/ios/AudioManagerModule.mm +16 -15
- package/ios/audioapi/ios/core/IOSAudioPlayer.h +11 -12
- package/ios/audioapi/ios/core/IOSAudioPlayer.mm +22 -16
- package/ios/audioapi/ios/core/IOSAudioRecorder.h +36 -0
- package/ios/audioapi/ios/core/IOSAudioRecorder.mm +62 -0
- package/ios/audioapi/ios/core/{AudioPlayer.h → NativeAudioPlayer.h} +1 -8
- package/ios/audioapi/ios/core/{AudioPlayer.m → NativeAudioPlayer.m} +4 -33
- package/ios/audioapi/ios/core/NativeAudioRecorder.h +25 -0
- package/ios/audioapi/ios/core/NativeAudioRecorder.m +47 -0
- package/ios/audioapi/ios/system/AudioEngine.h +7 -1
- package/ios/audioapi/ios/system/AudioEngine.mm +64 -20
- package/ios/audioapi/ios/system/AudioSessionManager.h +3 -1
- package/ios/audioapi/ios/system/AudioSessionManager.mm +37 -25
- package/ios/audioapi/ios/system/LockScreenManager.mm +4 -8
- package/ios/audioapi/ios/system/NotificationManager.h +13 -1
- package/ios/audioapi/ios/system/NotificationManager.mm +96 -44
- package/lib/commonjs/api.js +211 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/api.web.js +219 -0
- package/lib/commonjs/api.web.js.map +1 -0
- package/lib/commonjs/core/AnalyserNode.js +71 -0
- package/lib/commonjs/core/AnalyserNode.js.map +1 -0
- package/lib/commonjs/core/AudioBuffer.js +44 -0
- package/lib/commonjs/core/AudioBuffer.js.map +1 -0
- package/lib/commonjs/core/AudioBufferSourceNode.js +68 -0
- package/lib/commonjs/core/AudioBufferSourceNode.js.map +1 -0
- package/lib/commonjs/core/AudioContext.js +29 -0
- package/lib/commonjs/core/AudioContext.js.map +1 -0
- package/lib/commonjs/core/AudioDestinationNode.js +11 -0
- package/lib/commonjs/core/AudioDestinationNode.js.map +1 -0
- package/lib/commonjs/core/AudioNode.js +30 -0
- package/lib/commonjs/core/AudioNode.js.map +1 -0
- package/lib/commonjs/core/AudioParam.js +82 -0
- package/lib/commonjs/core/AudioParam.js.map +1 -0
- package/lib/commonjs/core/AudioRecorder.js +51 -0
- package/lib/commonjs/core/AudioRecorder.js.map +1 -0
- package/lib/commonjs/core/AudioScheduledSourceNode.js +38 -0
- package/lib/commonjs/core/AudioScheduledSourceNode.js.map +1 -0
- package/lib/commonjs/core/BaseAudioContext.js +80 -0
- package/lib/commonjs/core/BaseAudioContext.js.map +1 -0
- package/lib/commonjs/core/BiquadFilterNode.js +33 -0
- package/lib/commonjs/core/BiquadFilterNode.js.map +1 -0
- package/lib/commonjs/core/GainNode.js +17 -0
- package/lib/commonjs/core/GainNode.js.map +1 -0
- package/lib/commonjs/core/OfflineAudioContext.js +63 -0
- package/lib/commonjs/core/OfflineAudioContext.js.map +1 -0
- package/lib/commonjs/core/OscillatorNode.js +32 -0
- package/lib/commonjs/core/OscillatorNode.js.map +1 -0
- package/lib/commonjs/core/PeriodicWave.js +15 -0
- package/lib/commonjs/core/PeriodicWave.js.map +1 -0
- package/lib/commonjs/core/StereoPannerNode.js +17 -0
- package/lib/commonjs/core/StereoPannerNode.js.map +1 -0
- package/lib/commonjs/errors/IndexSizeError.js +14 -0
- package/lib/commonjs/errors/IndexSizeError.js.map +1 -0
- package/lib/commonjs/errors/InvalidAccessError.js +14 -0
- package/lib/commonjs/errors/InvalidAccessError.js.map +1 -0
- package/lib/commonjs/errors/InvalidStateError.js +14 -0
- package/lib/commonjs/errors/InvalidStateError.js.map +1 -0
- package/lib/commonjs/errors/NotSupportedError.js +14 -0
- package/lib/commonjs/errors/NotSupportedError.js.map +1 -0
- package/lib/commonjs/errors/RangeError.js +14 -0
- package/lib/commonjs/errors/RangeError.js.map +1 -0
- package/lib/commonjs/errors/index.js +42 -0
- package/lib/commonjs/errors/index.js.map +1 -0
- package/lib/commonjs/hooks/useSytemVolume.js +24 -0
- package/lib/commonjs/hooks/useSytemVolume.js.map +1 -0
- package/lib/commonjs/index.js +17 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/interfaces.js +6 -0
- package/lib/commonjs/interfaces.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/plugin/withAudioAPI.js +62 -0
- package/lib/commonjs/plugin/withAudioAPI.js.map +1 -0
- package/lib/commonjs/specs/NativeAudioAPIModule.js +9 -0
- package/lib/commonjs/specs/NativeAudioAPIModule.js.map +1 -0
- package/lib/commonjs/specs/NativeAudioManagerModule.js +36 -0
- package/lib/commonjs/specs/NativeAudioManagerModule.js.map +1 -0
- package/lib/commonjs/specs/index.js +27 -0
- package/lib/commonjs/specs/index.js.map +1 -0
- package/lib/commonjs/system/AudioManager.js +52 -0
- package/lib/commonjs/system/AudioManager.js.map +1 -0
- package/lib/commonjs/system/index.js +14 -0
- package/lib/commonjs/system/index.js.map +1 -0
- package/lib/commonjs/system/types.js +2 -0
- package/lib/commonjs/system/types.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/index.js +10 -0
- package/lib/commonjs/utils/index.js.map +1 -0
- package/lib/commonjs/web-core/AnalyserNode.js +38 -0
- package/lib/commonjs/web-core/AnalyserNode.js.map +1 -0
- package/lib/commonjs/web-core/AudioBuffer.js +44 -0
- package/lib/commonjs/web-core/AudioBuffer.js.map +1 -0
- package/lib/commonjs/web-core/AudioBufferSourceNode.js +214 -0
- package/lib/commonjs/web-core/AudioBufferSourceNode.js.map +1 -0
- package/lib/commonjs/web-core/AudioContext.js +93 -0
- package/lib/commonjs/web-core/AudioContext.js.map +1 -0
- package/lib/commonjs/web-core/AudioDestinationNode.js +11 -0
- package/lib/commonjs/web-core/AudioDestinationNode.js.map +1 -0
- package/lib/commonjs/web-core/AudioNode.js +33 -0
- package/lib/commonjs/web-core/AudioNode.js.map +1 -0
- package/lib/commonjs/web-core/AudioParam.js +81 -0
- package/lib/commonjs/web-core/AudioParam.js.map +1 -0
- package/lib/commonjs/web-core/AudioScheduledSourceNode.js +41 -0
- package/lib/commonjs/web-core/AudioScheduledSourceNode.js.map +1 -0
- package/lib/commonjs/web-core/BaseAudioContext.js +2 -0
- package/lib/commonjs/web-core/BaseAudioContext.js.map +1 -0
- package/lib/commonjs/web-core/BiquadFilterNode.js +33 -0
- package/lib/commonjs/web-core/BiquadFilterNode.js.map +1 -0
- package/lib/commonjs/web-core/GainNode.js +17 -0
- package/lib/commonjs/web-core/GainNode.js.map +1 -0
- package/lib/commonjs/web-core/OfflineAudioContext.js +96 -0
- package/lib/commonjs/web-core/OfflineAudioContext.js.map +1 -0
- package/lib/commonjs/web-core/OscillatorNode.js +31 -0
- package/lib/commonjs/web-core/OscillatorNode.js.map +1 -0
- package/lib/commonjs/web-core/PeriodicWave.js +15 -0
- package/lib/commonjs/web-core/PeriodicWave.js.map +1 -0
- package/lib/commonjs/web-core/StereoPannerNode.js +17 -0
- package/lib/commonjs/web-core/StereoPannerNode.js.map +1 -0
- package/lib/commonjs/web-core/custom/LoadCustomWasm.js +37 -0
- package/lib/commonjs/web-core/custom/LoadCustomWasm.js.map +1 -0
- package/lib/commonjs/web-core/custom/index.js +14 -0
- package/lib/commonjs/web-core/custom/index.js.map +1 -0
- package/lib/commonjs/web-core/custom/signalsmithStretch/LICENSE.txt +21 -0
- package/lib/commonjs/web-core/custom/signalsmithStretch/README.md +46 -0
- package/lib/commonjs/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs +826 -0
- package/lib/commonjs/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs.map +1 -0
- package/lib/module/api.js +3 -1
- package/lib/module/api.js.map +1 -1
- package/lib/module/core/AudioRecorder.js +45 -0
- package/lib/module/core/AudioRecorder.js.map +1 -0
- package/lib/module/errors/NotSupportedError.js.map +1 -1
- package/lib/module/hooks/useSytemVolume.js +19 -0
- package/lib/module/hooks/useSytemVolume.js.map +1 -0
- package/lib/module/plugin/withAudioAPI.js +58 -0
- package/lib/module/plugin/withAudioAPI.js.map +1 -0
- package/lib/module/specs/NativeAudioManagerModule.js +10 -8
- package/lib/module/specs/NativeAudioManagerModule.js.map +1 -1
- package/lib/module/system/AudioManager.js +26 -44
- package/lib/module/system/AudioManager.js.map +1 -1
- package/lib/typescript/api.d.ts +5 -1
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/core/AudioRecorder.d.ts +22 -0
- package/lib/typescript/core/AudioRecorder.d.ts.map +1 -0
- package/lib/typescript/errors/NotSupportedError.d.ts.map +1 -1
- package/lib/typescript/hooks/useSytemVolume.d.ts +2 -0
- package/lib/typescript/hooks/useSytemVolume.d.ts.map +1 -0
- package/lib/typescript/interfaces.d.ts +11 -5
- package/lib/typescript/interfaces.d.ts.map +1 -1
- package/lib/typescript/plugin/withAudioAPI.d.ts +9 -0
- package/lib/typescript/plugin/withAudioAPI.d.ts.map +1 -0
- package/lib/typescript/specs/NativeAudioManagerModule.d.ts +4 -2
- package/lib/typescript/specs/NativeAudioManagerModule.d.ts.map +1 -1
- package/lib/typescript/system/AudioManager.d.ts +5 -2
- package/lib/typescript/system/AudioManager.d.ts.map +1 -1
- package/lib/typescript/system/types.d.ts +36 -5
- package/lib/typescript/system/types.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +5 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/api.ts +13 -2
- package/src/core/AudioRecorder.ts +81 -0
- package/src/hooks/useSytemVolume.ts +19 -0
- package/src/interfaces.ts +25 -11
- package/src/plugin/withAudioAPI.ts +91 -0
- package/src/specs/NativeAudioManagerModule.ts +13 -19
- package/src/system/AudioManager.ts +37 -87
- package/src/system/types.ts +43 -17
- package/src/types.ts +13 -0
- /package/src/errors/{NotSupportedError.tsx → NotSupportedError.ts} +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.drawable.BitmapDrawable
|
|
6
|
+
import android.support.v4.media.MediaMetadataCompat
|
|
7
|
+
import android.support.v4.media.session.MediaSessionCompat
|
|
8
|
+
import android.support.v4.media.session.PlaybackStateCompat
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
import androidx.media.app.NotificationCompat.MediaStyle
|
|
12
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
13
|
+
import com.facebook.react.bridge.ReadableMap
|
|
14
|
+
import com.facebook.react.bridge.ReadableType
|
|
15
|
+
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper.Companion.instance
|
|
16
|
+
import java.io.IOException
|
|
17
|
+
import java.net.URL
|
|
18
|
+
|
|
19
|
+
class LockScreenManager(
|
|
20
|
+
private val reactContext: ReactApplicationContext,
|
|
21
|
+
private val mediaSession: MediaSessionCompat,
|
|
22
|
+
private val mediaNotificationManager: MediaNotificationManager,
|
|
23
|
+
val channelId: String,
|
|
24
|
+
) {
|
|
25
|
+
private var pb: PlaybackStateCompat.Builder = PlaybackStateCompat.Builder()
|
|
26
|
+
private var state: PlaybackStateCompat = pb.build()
|
|
27
|
+
private var controls: Long = 0
|
|
28
|
+
var isPlaying: Boolean = false
|
|
29
|
+
|
|
30
|
+
private var nb: NotificationCompat.Builder = NotificationCompat.Builder(reactContext, channelId)
|
|
31
|
+
|
|
32
|
+
private var artworkThread: Thread? = null
|
|
33
|
+
|
|
34
|
+
private var title: String? = null
|
|
35
|
+
private var artist: String? = null
|
|
36
|
+
private var album: String? = null
|
|
37
|
+
private var description: String? = null
|
|
38
|
+
private var duration: Long = 0
|
|
39
|
+
private var speed: Float = 1.0F
|
|
40
|
+
private var elapsedTime: Long = 0L
|
|
41
|
+
private var artwork: String? = null
|
|
42
|
+
private var playbackState: Int = PlaybackStateCompat.STATE_PAUSED
|
|
43
|
+
|
|
44
|
+
init {
|
|
45
|
+
this.pb.setActions(controls)
|
|
46
|
+
|
|
47
|
+
this.nb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
48
|
+
this.nb.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
49
|
+
|
|
50
|
+
updateNotificationMediaStyle()
|
|
51
|
+
|
|
52
|
+
mediaNotificationManager.updateActions(controls)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun setLockScreenInfo(info: ReadableMap?) {
|
|
56
|
+
if (artworkThread != null && artworkThread!!.isAlive) {
|
|
57
|
+
artworkThread!!.interrupt()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
artworkThread = null
|
|
61
|
+
|
|
62
|
+
if (info == null) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
val md = MediaMetadataCompat.Builder()
|
|
67
|
+
|
|
68
|
+
if (info.hasKey("title")) {
|
|
69
|
+
title = info.getString("title")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (info.hasKey("artist")) {
|
|
73
|
+
artist = info.getString("artist")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (info.hasKey("album")) {
|
|
77
|
+
album = info.getString("album")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (info.hasKey("description")) {
|
|
81
|
+
description = info.getString("description")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (info.hasKey("duration")) {
|
|
85
|
+
duration = (info.getDouble("duration") * 1000).toLong()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
md.putText(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
|
89
|
+
md.putText(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
|
90
|
+
md.putText(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
|
|
91
|
+
md.putText(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, description)
|
|
92
|
+
md.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
|
|
93
|
+
|
|
94
|
+
nb.setContentTitle(title)
|
|
95
|
+
nb.setContentText(artist)
|
|
96
|
+
nb.setContentInfo(album)
|
|
97
|
+
|
|
98
|
+
if (info.hasKey("artwork")) {
|
|
99
|
+
var localArtwork = false
|
|
100
|
+
|
|
101
|
+
if (info.getType("artwork") == ReadableType.Map) {
|
|
102
|
+
artwork = info.getMap("artwork")?.getString("uri")
|
|
103
|
+
localArtwork = true
|
|
104
|
+
} else {
|
|
105
|
+
artwork = info.getString("artwork")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
val artworkLocal = localArtwork
|
|
109
|
+
|
|
110
|
+
artworkThread =
|
|
111
|
+
Thread {
|
|
112
|
+
try {
|
|
113
|
+
val bitmap: Bitmap? = artwork?.let { loadArtwork(it, artworkLocal) }
|
|
114
|
+
|
|
115
|
+
val currentMetadata: MediaMetadataCompat = mediaSession.controller.metadata
|
|
116
|
+
val newBuilder =
|
|
117
|
+
MediaMetadataCompat.Builder(
|
|
118
|
+
currentMetadata,
|
|
119
|
+
)
|
|
120
|
+
mediaSession.setMetadata(
|
|
121
|
+
newBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap).build(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
nb.setLargeIcon(bitmap)
|
|
125
|
+
mediaNotificationManager.show(nb, isPlaying)
|
|
126
|
+
|
|
127
|
+
artworkThread = null
|
|
128
|
+
} catch (ex: Exception) {
|
|
129
|
+
ex.printStackTrace()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
artworkThread!!.start()
|
|
133
|
+
} else {
|
|
134
|
+
md.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null)
|
|
135
|
+
nb.setLargeIcon(null as Bitmap?)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
speed =
|
|
139
|
+
if (info.hasKey("speed")) {
|
|
140
|
+
info.getDouble("speed").toFloat()
|
|
141
|
+
} else {
|
|
142
|
+
state.playbackSpeed
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
elapsedTime =
|
|
146
|
+
if (info.hasKey("elapsedTime")) {
|
|
147
|
+
info.getDouble("elapsedTime").toLong()
|
|
148
|
+
} else {
|
|
149
|
+
state.position
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (info.hasKey("state")) {
|
|
153
|
+
val state = info.getString("state")
|
|
154
|
+
|
|
155
|
+
when (state) {
|
|
156
|
+
"state_playing" -> {
|
|
157
|
+
this.playbackState = PlaybackStateCompat.STATE_PLAYING
|
|
158
|
+
}
|
|
159
|
+
"state_paused" -> {
|
|
160
|
+
this.playbackState = PlaybackStateCompat.STATE_PAUSED
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
updatePlaybackState(this.playbackState)
|
|
166
|
+
|
|
167
|
+
mediaSession.setMetadata(md.build())
|
|
168
|
+
mediaSession.setActive(true)
|
|
169
|
+
mediaNotificationManager.show(nb, isPlaying)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun resetLockScreenInfo() {
|
|
173
|
+
if (artworkThread != null && artworkThread!!.isAlive) artworkThread!!.interrupt()
|
|
174
|
+
artworkThread = null
|
|
175
|
+
|
|
176
|
+
mediaNotificationManager.hide()
|
|
177
|
+
mediaSession.setActive(false)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fun enableRemoteCommand(
|
|
181
|
+
name: String,
|
|
182
|
+
enabled: Boolean,
|
|
183
|
+
) {
|
|
184
|
+
var controlValue = 0L
|
|
185
|
+
when (name) {
|
|
186
|
+
"play" -> controlValue = PlaybackStateCompat.ACTION_PLAY
|
|
187
|
+
"pause" -> controlValue = PlaybackStateCompat.ACTION_PAUSE
|
|
188
|
+
"stop" -> controlValue = PlaybackStateCompat.ACTION_STOP
|
|
189
|
+
"togglePlayPause" -> controlValue = PlaybackStateCompat.ACTION_PLAY_PAUSE
|
|
190
|
+
"nextTrack" -> controlValue = PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
|
191
|
+
"previousTrack" -> controlValue = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
|
192
|
+
"skipForward" -> controlValue = PlaybackStateCompat.ACTION_REWIND
|
|
193
|
+
"skipBackward" -> controlValue = PlaybackStateCompat.ACTION_REWIND
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
controls =
|
|
197
|
+
if (enabled) {
|
|
198
|
+
controls or controlValue
|
|
199
|
+
} else {
|
|
200
|
+
controls and controlValue.inv()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
mediaNotificationManager.updateActions(controls)
|
|
204
|
+
pb.setActions(controls)
|
|
205
|
+
|
|
206
|
+
state = pb.build()
|
|
207
|
+
mediaSession.setPlaybackState(state)
|
|
208
|
+
|
|
209
|
+
updateNotificationMediaStyle()
|
|
210
|
+
|
|
211
|
+
if (mediaSession.isActive) {
|
|
212
|
+
mediaNotificationManager.show(nb, isPlaying)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun loadArtwork(
|
|
217
|
+
url: String,
|
|
218
|
+
local: Boolean,
|
|
219
|
+
): Bitmap? {
|
|
220
|
+
var bitmap: Bitmap? = null
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// If we are running the app in debug mode, the "local" image will be served from htt://localhost:8080, so we need to check for this case and load those images from URL
|
|
224
|
+
if (local && !url.startsWith("http")) {
|
|
225
|
+
// Gets the drawable from the RN's helper for local resources
|
|
226
|
+
val helper = instance
|
|
227
|
+
val image = helper.getResourceDrawable(reactContext, url)
|
|
228
|
+
|
|
229
|
+
bitmap =
|
|
230
|
+
if (image is BitmapDrawable) {
|
|
231
|
+
image.bitmap
|
|
232
|
+
} else {
|
|
233
|
+
BitmapFactory.decodeFile(url)
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
// Open connection to the URL and decodes the image
|
|
237
|
+
val con = URL(url).openConnection()
|
|
238
|
+
con.connect()
|
|
239
|
+
val input = con.getInputStream()
|
|
240
|
+
bitmap = BitmapFactory.decodeStream(input)
|
|
241
|
+
input.close()
|
|
242
|
+
}
|
|
243
|
+
} catch (ex: IOException) {
|
|
244
|
+
Log.w("MediaSessionManager", "Could not load the artwork", ex)
|
|
245
|
+
} catch (ex: IndexOutOfBoundsException) {
|
|
246
|
+
Log.w("MediaSessionManager", "Could not load the artwork", ex)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return bitmap
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fun updatePlaybackState(playbackState: Int) {
|
|
253
|
+
isPlaying = playbackState == PlaybackStateCompat.STATE_PLAYING
|
|
254
|
+
|
|
255
|
+
pb.setState(playbackState, elapsedTime, speed)
|
|
256
|
+
pb.setActions(controls)
|
|
257
|
+
state = pb.build()
|
|
258
|
+
mediaSession.setPlaybackState(state)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun hasControl(control: Long): Boolean = (controls and control) == control
|
|
262
|
+
|
|
263
|
+
private fun updateNotificationMediaStyle() {
|
|
264
|
+
val style = MediaStyle()
|
|
265
|
+
style.setMediaSession(mediaSession.sessionToken)
|
|
266
|
+
var controlCount = 0
|
|
267
|
+
if (hasControl(PlaybackStateCompat.ACTION_PLAY) ||
|
|
268
|
+
hasControl(PlaybackStateCompat.ACTION_PAUSE) ||
|
|
269
|
+
hasControl(
|
|
270
|
+
PlaybackStateCompat.ACTION_PLAY_PAUSE,
|
|
271
|
+
)
|
|
272
|
+
) {
|
|
273
|
+
controlCount += 1
|
|
274
|
+
}
|
|
275
|
+
if (hasControl(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) {
|
|
276
|
+
controlCount += 1
|
|
277
|
+
}
|
|
278
|
+
if (hasControl(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) {
|
|
279
|
+
controlCount += 1
|
|
280
|
+
}
|
|
281
|
+
if (hasControl(PlaybackStateCompat.ACTION_FAST_FORWARD)) {
|
|
282
|
+
controlCount += 1
|
|
283
|
+
}
|
|
284
|
+
if (hasControl(PlaybackStateCompat.ACTION_REWIND)) {
|
|
285
|
+
controlCount += 1
|
|
286
|
+
}
|
|
287
|
+
val actions = IntArray(controlCount)
|
|
288
|
+
for (i in actions.indices) {
|
|
289
|
+
actions[i] = i
|
|
290
|
+
}
|
|
291
|
+
style.setShowActionsInCompactView(*actions)
|
|
292
|
+
nb.setStyle(style)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.Notification
|
|
5
|
+
import android.app.PendingIntent
|
|
6
|
+
import android.app.Service
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.content.res.Resources
|
|
9
|
+
import android.os.Binder
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.os.IBinder
|
|
12
|
+
import android.provider.ContactsContract
|
|
13
|
+
import android.support.v4.media.session.PlaybackStateCompat
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import android.view.KeyEvent
|
|
16
|
+
import androidx.core.app.NotificationCompat
|
|
17
|
+
import androidx.core.app.NotificationManagerCompat
|
|
18
|
+
import androidx.core.content.ContextCompat
|
|
19
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
20
|
+
import com.swmansion.audioapi.R
|
|
21
|
+
import java.lang.ref.WeakReference
|
|
22
|
+
|
|
23
|
+
class MediaNotificationManager(
|
|
24
|
+
val reactContext: ReactApplicationContext,
|
|
25
|
+
val notificationId: Int,
|
|
26
|
+
val channelId: String,
|
|
27
|
+
) {
|
|
28
|
+
private var smallIcon: Int = R.drawable.play
|
|
29
|
+
private var customIcon: Int = 0
|
|
30
|
+
|
|
31
|
+
private var play: NotificationCompat.Action? = null
|
|
32
|
+
private var pause: NotificationCompat.Action? = null
|
|
33
|
+
private var stop: NotificationCompat.Action? = null
|
|
34
|
+
private var next: NotificationCompat.Action? = null
|
|
35
|
+
private var previous: NotificationCompat.Action? = null
|
|
36
|
+
private var skipForward: NotificationCompat.Action? = null
|
|
37
|
+
private var skipBackward: NotificationCompat.Action? = null
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
const val REMOVE_NOTIFICATION: String = "audio_manager_remove_notification"
|
|
41
|
+
const val PACKAGE_NAME: String = "com.swmansion.audioapi.system"
|
|
42
|
+
const val MEDIA_BUTTON: String = "audio_manager_media_button"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@SuppressLint("RestrictedApi")
|
|
46
|
+
@Synchronized
|
|
47
|
+
fun prepareNotification(
|
|
48
|
+
builder: NotificationCompat.Builder,
|
|
49
|
+
isPlaying: Boolean,
|
|
50
|
+
): Notification {
|
|
51
|
+
builder.mActions.clear()
|
|
52
|
+
|
|
53
|
+
if (previous != null) {
|
|
54
|
+
builder.addAction(previous)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (skipBackward != null) {
|
|
58
|
+
builder.addAction(skipBackward)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (play != null && !isPlaying) {
|
|
62
|
+
builder.addAction(play)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (pause != null && isPlaying) {
|
|
66
|
+
builder.addAction(pause)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (stop != null) {
|
|
70
|
+
builder.addAction(stop)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (next != null) {
|
|
74
|
+
builder.addAction(next)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (skipForward != null) {
|
|
78
|
+
builder.addAction(skipForward)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
builder.setSmallIcon(if (customIcon != 0) customIcon else smallIcon)
|
|
82
|
+
|
|
83
|
+
val packageName: String = reactContext.packageName
|
|
84
|
+
val openApp: Intent? = reactContext.packageManager.getLaunchIntentForPackage(packageName)
|
|
85
|
+
try {
|
|
86
|
+
builder.setContentIntent(
|
|
87
|
+
PendingIntent.getActivity(
|
|
88
|
+
reactContext,
|
|
89
|
+
0,
|
|
90
|
+
openApp,
|
|
91
|
+
PendingIntent.FLAG_IMMUTABLE,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
Log.w("AudioManagerModule", "Error creating content intent: ${e.message}")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
val remove = Intent(REMOVE_NOTIFICATION)
|
|
99
|
+
remove.putExtra(PACKAGE_NAME, reactContext.applicationInfo.packageName)
|
|
100
|
+
builder.setDeleteIntent(
|
|
101
|
+
PendingIntent.getBroadcast(
|
|
102
|
+
reactContext,
|
|
103
|
+
0,
|
|
104
|
+
remove,
|
|
105
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return builder.build()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@SuppressLint("MissingPermission")
|
|
113
|
+
@Synchronized
|
|
114
|
+
fun show(
|
|
115
|
+
builder: NotificationCompat.Builder?,
|
|
116
|
+
isPlaying: Boolean,
|
|
117
|
+
) {
|
|
118
|
+
NotificationManagerCompat.from(reactContext).notify(
|
|
119
|
+
notificationId,
|
|
120
|
+
prepareNotification(
|
|
121
|
+
builder!!,
|
|
122
|
+
isPlaying,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fun hide() {
|
|
128
|
+
NotificationManagerCompat.from(reactContext).cancel(notificationId)
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
val myIntent =
|
|
132
|
+
Intent(
|
|
133
|
+
reactContext,
|
|
134
|
+
NotificationService::class.java,
|
|
135
|
+
)
|
|
136
|
+
reactContext.stopService(myIntent)
|
|
137
|
+
} catch (e: java.lang.Exception) {
|
|
138
|
+
Log.w("AudioManagerModule", "Error stopping service: ${e.message}")
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Synchronized
|
|
143
|
+
fun updateActions(mask: Long) {
|
|
144
|
+
play = createAction("play", "Play", mask, PlaybackStateCompat.ACTION_PLAY, play)
|
|
145
|
+
pause = createAction("pause", "Pause", mask, PlaybackStateCompat.ACTION_PAUSE, pause)
|
|
146
|
+
stop = createAction("stop", "Stop", mask, PlaybackStateCompat.ACTION_STOP, stop)
|
|
147
|
+
next = createAction("next", "Next", mask, PlaybackStateCompat.ACTION_SKIP_TO_NEXT, next)
|
|
148
|
+
previous =
|
|
149
|
+
createAction(
|
|
150
|
+
"previous",
|
|
151
|
+
"Previous",
|
|
152
|
+
mask,
|
|
153
|
+
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
|
|
154
|
+
previous,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
skipForward =
|
|
158
|
+
createAction(
|
|
159
|
+
"skip_forward_5",
|
|
160
|
+
"Skip Forward",
|
|
161
|
+
mask,
|
|
162
|
+
PlaybackStateCompat.ACTION_FAST_FORWARD,
|
|
163
|
+
skipForward,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
skipBackward =
|
|
167
|
+
createAction(
|
|
168
|
+
"skip_backward_5",
|
|
169
|
+
"Skip Backward",
|
|
170
|
+
mask,
|
|
171
|
+
PlaybackStateCompat.ACTION_REWIND,
|
|
172
|
+
skipBackward,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun createAction(
|
|
177
|
+
iconName: String,
|
|
178
|
+
title: String,
|
|
179
|
+
mask: Long,
|
|
180
|
+
action: Long,
|
|
181
|
+
oldAction: NotificationCompat.Action?,
|
|
182
|
+
): NotificationCompat.Action? {
|
|
183
|
+
if ((mask and action) == 0L) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (oldAction != null) {
|
|
188
|
+
return oldAction
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
val r: Resources = reactContext.resources
|
|
192
|
+
val packageName: String = reactContext.packageName
|
|
193
|
+
val icon = r.getIdentifier(iconName, "drawable", packageName)
|
|
194
|
+
|
|
195
|
+
val keyCode = PlaybackStateCompat.toKeyCode(action)
|
|
196
|
+
val intent = Intent(MEDIA_BUTTON)
|
|
197
|
+
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
|
|
198
|
+
intent.putExtra(ContactsContract.Directory.PACKAGE_NAME, packageName)
|
|
199
|
+
val i =
|
|
200
|
+
PendingIntent.getBroadcast(
|
|
201
|
+
reactContext,
|
|
202
|
+
keyCode,
|
|
203
|
+
intent,
|
|
204
|
+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return NotificationCompat.Action(icon, title, i)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class NotificationService : Service() {
|
|
211
|
+
private val binder = LocalBinder()
|
|
212
|
+
private var notification: Notification? = null
|
|
213
|
+
|
|
214
|
+
inner class LocalBinder : Binder() {
|
|
215
|
+
private var weakService: WeakReference<NotificationService>? = null
|
|
216
|
+
|
|
217
|
+
fun onBind(service: NotificationService) {
|
|
218
|
+
weakService = WeakReference(service)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fun getService(): NotificationService? = weakService?.get()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
override fun onBind(intent: Intent): IBinder {
|
|
225
|
+
binder.onBind(this)
|
|
226
|
+
return binder
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fun forceForeground() {
|
|
230
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
231
|
+
val intent = Intent(this, NotificationService::class.java)
|
|
232
|
+
ContextCompat.startForegroundService(this, intent)
|
|
233
|
+
notification =
|
|
234
|
+
MediaSessionManager.mediaNotificationManager
|
|
235
|
+
.prepareNotification(NotificationCompat.Builder(this, MediaSessionManager.channelId), false)
|
|
236
|
+
startForeground(MediaSessionManager.notificationId, notification)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
override fun onCreate() {
|
|
241
|
+
super.onCreate()
|
|
242
|
+
try {
|
|
243
|
+
notification =
|
|
244
|
+
MediaSessionManager.mediaNotificationManager
|
|
245
|
+
.prepareNotification(NotificationCompat.Builder(this, MediaSessionManager.channelId), false)
|
|
246
|
+
startForeground(MediaSessionManager.notificationId, notification)
|
|
247
|
+
} catch (ex: Exception) {
|
|
248
|
+
Log.w("AudioManagerModule", "Error starting service: ${ex.message}")
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override fun onStartCommand(
|
|
253
|
+
intent: Intent?,
|
|
254
|
+
flags: Int,
|
|
255
|
+
startId: Int,
|
|
256
|
+
): Int {
|
|
257
|
+
onCreate()
|
|
258
|
+
return START_NOT_STICKY
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
262
|
+
super.onTaskRemoved(rootIntent)
|
|
263
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
264
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
265
|
+
}
|
|
266
|
+
stopSelf()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
override fun onDestroy() {
|
|
270
|
+
super.onDestroy()
|
|
271
|
+
|
|
272
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
273
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
stopSelf()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.media.AudioManager
|
|
7
|
+
import android.view.KeyEvent
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
|
|
10
|
+
class MediaReceiver(
|
|
11
|
+
val reactContext: ReactApplicationContext,
|
|
12
|
+
private val mediaSessionManager: MediaSessionManager,
|
|
13
|
+
) : BroadcastReceiver() {
|
|
14
|
+
override fun onReceive(
|
|
15
|
+
context: Context?,
|
|
16
|
+
intent: Intent?,
|
|
17
|
+
) {
|
|
18
|
+
val action = intent!!.action
|
|
19
|
+
|
|
20
|
+
if (MediaNotificationManager.REMOVE_NOTIFICATION == action) {
|
|
21
|
+
if (!checkApp(intent)) return
|
|
22
|
+
|
|
23
|
+
mediaSessionManager.mediaNotificationManager.hide()
|
|
24
|
+
mediaSessionManager.mediaSession.isActive = false
|
|
25
|
+
|
|
26
|
+
mediaSessionManager.eventEmitter.sendEvent("onCloseNotification", null)
|
|
27
|
+
} else if (MediaNotificationManager.MEDIA_BUTTON == action || Intent.ACTION_MEDIA_BUTTON == action) {
|
|
28
|
+
if (!intent.hasExtra(Intent.EXTRA_KEY_EVENT)) return
|
|
29
|
+
if (!checkApp(intent)) return
|
|
30
|
+
|
|
31
|
+
val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
|
32
|
+
mediaSessionManager.mediaSession.controller.dispatchMediaButtonEvent(keyEvent)
|
|
33
|
+
} else if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == action) {
|
|
34
|
+
mediaSessionManager.mediaSession.controller.transportControls
|
|
35
|
+
.pause()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private fun checkApp(intent: Intent): Boolean {
|
|
40
|
+
if (intent.hasExtra(MediaNotificationManager.PACKAGE_NAME)) {
|
|
41
|
+
val name = intent.getStringExtra(MediaNotificationManager.PACKAGE_NAME)
|
|
42
|
+
if (!reactContext.packageName.equals(name)) return false
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package com.swmansion.audioapi.system
|
|
2
|
+
|
|
3
|
+
import android.support.v4.media.session.MediaSessionCompat
|
|
4
|
+
import android.support.v4.media.session.PlaybackStateCompat
|
|
5
|
+
|
|
6
|
+
class MediaSessionCallback(
|
|
7
|
+
val eventEmitter: MediaSessionEventEmitter,
|
|
8
|
+
private val lockScreenManager: LockScreenManager,
|
|
9
|
+
) : MediaSessionCompat.Callback() {
|
|
10
|
+
override fun onPlay() {
|
|
11
|
+
lockScreenManager.updatePlaybackState(PlaybackStateCompat.STATE_PLAYING)
|
|
12
|
+
eventEmitter.onPlay()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override fun onPause() {
|
|
16
|
+
lockScreenManager.updatePlaybackState(PlaybackStateCompat.STATE_PAUSED)
|
|
17
|
+
eventEmitter.onPause()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun onStop() {
|
|
21
|
+
eventEmitter.onStop()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun onSkipToNext() {
|
|
25
|
+
eventEmitter.onSkipToNext()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onSkipToPrevious() {
|
|
29
|
+
eventEmitter.onSkipToPrevious()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun onFastForward() {
|
|
33
|
+
eventEmitter.onFastForward()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun onRewind() {
|
|
37
|
+
eventEmitter.onRewind()
|
|
38
|
+
}
|
|
39
|
+
}
|