@ion299/sdk-react-native 0.1.0-beta.5 → 0.1.0-beta.6
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/CHANGELOG.md +15 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkAudioPlayerModule.kt +201 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +1 -0
- package/ios/ChatSdkAudioPlayer.m +25 -0
- package/ios/ChatSdkAudioPlayer.swift +193 -0
- package/lib/commonjs/audio/audioController.js +145 -0
- package/lib/commonjs/audio/audioController.js.map +1 -0
- package/lib/commonjs/components/AudioMessage.js +198 -0
- package/lib/commonjs/components/AudioMessage.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +9 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -1
- package/lib/commonjs/native/NativeChatSdkAudioPlayer.js +28 -0
- package/lib/commonjs/native/NativeChatSdkAudioPlayer.js.map +1 -0
- package/lib/module/audio/audioController.js +140 -0
- package/lib/module/audio/audioController.js.map +1 -0
- package/lib/module/components/AudioMessage.js +193 -0
- package/lib/module/components/AudioMessage.js.map +1 -0
- package/lib/module/components/MessageBubble.js +9 -0
- package/lib/module/components/MessageBubble.js.map +1 -1
- package/lib/module/native/NativeChatSdkAudioPlayer.js +23 -0
- package/lib/module/native/NativeChatSdkAudioPlayer.js.map +1 -0
- package/lib/typescript/commonjs/audio/audioController.d.ts +25 -0
- package/lib/typescript/commonjs/audio/audioController.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AudioMessage.d.ts +11 -0
- package/lib/typescript/commonjs/components/AudioMessage.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts +20 -0
- package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
- package/lib/typescript/module/audio/audioController.d.ts +25 -0
- package/lib/typescript/module/audio/audioController.d.ts.map +1 -0
- package/lib/typescript/module/components/AudioMessage.d.ts +11 -0
- package/lib/typescript/module/components/AudioMessage.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts +20 -0
- package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/audio/audioController.ts +144 -0
- package/src/components/AudioMessage.tsx +198 -0
- package/src/components/MessageBubble.tsx +6 -0
- package/src/native/NativeChatSdkAudioPlayer.ts +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.0-beta.6]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Аудио-вложения (голосовые сообщения) теперь проигрываются прямо в чате:
|
|
13
|
+
встроенный плеер с кнопкой play/pause, прогресс-баром и перемоткой по тапу.
|
|
14
|
+
Раньше аудио показывалось как обычный файл (`.webm`) только для скачивания.
|
|
15
|
+
- Собственный нативный модуль воспроизведения `ChatSdkAudioPlayer`
|
|
16
|
+
(Android `MediaPlayer`, iOS `AVPlayer`) — без сторонних JS-зависимостей.
|
|
17
|
+
Если модуль не собран, аудио откатывается на обычный файловый блок.
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
- iOS (`AVPlayer`) не декодирует контейнер WebM/Opus: голосовые, записанные
|
|
21
|
+
в браузере как `audio/webm`, на iOS воспроизвести нельзя — их нужно
|
|
22
|
+
транскодировать на бэкенде (например, в `m4a`/`aac`). Android их играет.
|
|
23
|
+
|
|
9
24
|
## [0.1.0-beta.5]
|
|
10
25
|
|
|
11
26
|
### Changed
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.chatplatform.sdk
|
|
2
|
+
|
|
3
|
+
import android.media.AudioAttributes
|
|
4
|
+
import android.media.MediaPlayer
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.HandlerThread
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
12
|
+
import com.facebook.react.bridge.ReactMethod
|
|
13
|
+
import com.facebook.react.bridge.ReadableMap
|
|
14
|
+
import com.facebook.react.bridge.WritableMap
|
|
15
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Проигрывает аудио-вложения (голосовые сообщения) прямо в чате.
|
|
19
|
+
* В каждый момент времени активен ровно один плеер — запуск нового
|
|
20
|
+
* освобождает предыдущий. Состояние отдаётся в JS событием
|
|
21
|
+
* "ChatSdkAudioState" { key, positionMillis, durationMillis, state }.
|
|
22
|
+
*/
|
|
23
|
+
class ChatSdkAudioPlayerModule(reactContext: ReactApplicationContext) :
|
|
24
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
25
|
+
|
|
26
|
+
override fun getName(): String = NAME
|
|
27
|
+
|
|
28
|
+
private val thread = HandlerThread("ChatSdkAudioPlayer").apply { start() }
|
|
29
|
+
private val handler = Handler(thread.looper)
|
|
30
|
+
|
|
31
|
+
private var player: MediaPlayer? = null
|
|
32
|
+
private var currentKey: String? = null
|
|
33
|
+
private var prepared = false
|
|
34
|
+
|
|
35
|
+
private val tick = object : Runnable {
|
|
36
|
+
override fun run() {
|
|
37
|
+
val p = player ?: return
|
|
38
|
+
try {
|
|
39
|
+
if (p.isPlaying) {
|
|
40
|
+
emit(currentKey, p.currentPosition.toLong(), safeDuration(p), "playing")
|
|
41
|
+
handler.postDelayed(this, 250)
|
|
42
|
+
}
|
|
43
|
+
} catch (_: Throwable) {
|
|
44
|
+
// плеер уже освобождён — просто перестаём тикать
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@ReactMethod
|
|
50
|
+
fun addListener(eventName: String) {
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ReactMethod
|
|
55
|
+
fun removeListeners(count: Int) {
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@ReactMethod
|
|
60
|
+
fun play(key: String, url: String, headers: ReadableMap?, promise: Promise) {
|
|
61
|
+
val hdr = HashMap<String, String>()
|
|
62
|
+
headers?.toHashMap()?.forEach { (k, v) -> if (v is String) hdr[k] = v }
|
|
63
|
+
|
|
64
|
+
handler.post {
|
|
65
|
+
try {
|
|
66
|
+
val existing = player
|
|
67
|
+
if (currentKey == key && existing != null && prepared) {
|
|
68
|
+
existing.start()
|
|
69
|
+
emit(key, existing.currentPosition.toLong(), safeDuration(existing), "playing")
|
|
70
|
+
handler.removeCallbacks(tick)
|
|
71
|
+
handler.post(tick)
|
|
72
|
+
promise.resolve(null)
|
|
73
|
+
return@post
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
releaseInternal()
|
|
77
|
+
currentKey = key
|
|
78
|
+
prepared = false
|
|
79
|
+
emit(key, 0, 0, "loading")
|
|
80
|
+
|
|
81
|
+
val mp = MediaPlayer()
|
|
82
|
+
mp.setAudioAttributes(
|
|
83
|
+
AudioAttributes.Builder()
|
|
84
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
85
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
86
|
+
.build(),
|
|
87
|
+
)
|
|
88
|
+
if (hdr.isEmpty()) {
|
|
89
|
+
mp.setDataSource(reactApplicationContext, Uri.parse(url))
|
|
90
|
+
} else {
|
|
91
|
+
mp.setDataSource(reactApplicationContext, Uri.parse(url), hdr)
|
|
92
|
+
}
|
|
93
|
+
mp.setOnPreparedListener { p ->
|
|
94
|
+
if (currentKey != key) return@setOnPreparedListener
|
|
95
|
+
prepared = true
|
|
96
|
+
p.start()
|
|
97
|
+
emit(key, 0, safeDuration(p), "playing")
|
|
98
|
+
handler.removeCallbacks(tick)
|
|
99
|
+
handler.post(tick)
|
|
100
|
+
}
|
|
101
|
+
mp.setOnCompletionListener { p ->
|
|
102
|
+
handler.removeCallbacks(tick)
|
|
103
|
+
emit(key, safeDuration(p), safeDuration(p), "ended")
|
|
104
|
+
}
|
|
105
|
+
mp.setOnErrorListener { _, _, _ ->
|
|
106
|
+
handler.removeCallbacks(tick)
|
|
107
|
+
emit(key, 0, 0, "error")
|
|
108
|
+
releaseInternal()
|
|
109
|
+
true
|
|
110
|
+
}
|
|
111
|
+
player = mp
|
|
112
|
+
mp.prepareAsync()
|
|
113
|
+
promise.resolve(null)
|
|
114
|
+
} catch (e: Throwable) {
|
|
115
|
+
emit(key, 0, 0, "error")
|
|
116
|
+
releaseInternal()
|
|
117
|
+
promise.reject("AUDIO_PLAY_FAILED", e.message ?: "Не удалось воспроизвести аудио", e)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@ReactMethod
|
|
123
|
+
fun pause(key: String, promise: Promise) {
|
|
124
|
+
handler.post {
|
|
125
|
+
try {
|
|
126
|
+
val p = player
|
|
127
|
+
if (currentKey == key && p != null && prepared && p.isPlaying) {
|
|
128
|
+
p.pause()
|
|
129
|
+
handler.removeCallbacks(tick)
|
|
130
|
+
emit(key, p.currentPosition.toLong(), safeDuration(p), "paused")
|
|
131
|
+
}
|
|
132
|
+
promise.resolve(null)
|
|
133
|
+
} catch (e: Throwable) {
|
|
134
|
+
promise.reject("AUDIO_PAUSE_FAILED", e.message ?: "Ошибка паузы", e)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@ReactMethod
|
|
140
|
+
fun seek(key: String, positionMillis: Double, promise: Promise) {
|
|
141
|
+
handler.post {
|
|
142
|
+
try {
|
|
143
|
+
val p = player
|
|
144
|
+
if (currentKey == key && p != null && prepared) {
|
|
145
|
+
p.seekTo(positionMillis.toInt())
|
|
146
|
+
val state = if (p.isPlaying) "playing" else "paused"
|
|
147
|
+
emit(key, positionMillis.toLong(), safeDuration(p), state)
|
|
148
|
+
}
|
|
149
|
+
promise.resolve(null)
|
|
150
|
+
} catch (e: Throwable) {
|
|
151
|
+
promise.reject("AUDIO_SEEK_FAILED", e.message ?: "Ошибка перемотки", e)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@ReactMethod
|
|
157
|
+
fun stop(key: String, promise: Promise) {
|
|
158
|
+
handler.post {
|
|
159
|
+
try {
|
|
160
|
+
if (currentKey == key) {
|
|
161
|
+
handler.removeCallbacks(tick)
|
|
162
|
+
releaseInternal()
|
|
163
|
+
emit(key, 0, 0, "stopped")
|
|
164
|
+
}
|
|
165
|
+
promise.resolve(null)
|
|
166
|
+
} catch (e: Throwable) {
|
|
167
|
+
promise.reject("AUDIO_STOP_FAILED", e.message ?: "Ошибка остановки", e)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun releaseInternal() {
|
|
173
|
+
try { player?.reset() } catch (_: Throwable) {}
|
|
174
|
+
try { player?.release() } catch (_: Throwable) {}
|
|
175
|
+
player = null
|
|
176
|
+
prepared = false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private fun safeDuration(p: MediaPlayer): Long =
|
|
180
|
+
try {
|
|
181
|
+
val d = p.duration
|
|
182
|
+
if (d > 0) d.toLong() else 0L
|
|
183
|
+
} catch (_: Throwable) {
|
|
184
|
+
0L
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun emit(key: String?, position: Long, duration: Long, state: String) {
|
|
188
|
+
val map: WritableMap = Arguments.createMap()
|
|
189
|
+
map.putString("key", key ?: "")
|
|
190
|
+
map.putDouble("positionMillis", position.toDouble())
|
|
191
|
+
map.putDouble("durationMillis", duration.toDouble())
|
|
192
|
+
map.putString("state", state)
|
|
193
|
+
reactApplicationContext
|
|
194
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
195
|
+
.emit("ChatSdkAudioState", map)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
companion object {
|
|
199
|
+
const val NAME = "ChatSdkAudioPlayer"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -9,6 +9,7 @@ class ChatSdkPackage : ReactPackage {
|
|
|
9
9
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(
|
|
10
10
|
ChatSdkFilePickerModule(reactContext),
|
|
11
11
|
ChatSdkDownloaderModule(reactContext),
|
|
12
|
+
ChatSdkAudioPlayerModule(reactContext),
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(ChatSdkAudioPlayer, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(play:(NSString *)key
|
|
7
|
+
url:(NSString *)url
|
|
8
|
+
headers:(NSDictionary *)headers
|
|
9
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
10
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
|
|
12
|
+
RCT_EXTERN_METHOD(pause:(NSString *)key
|
|
13
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
|
|
16
|
+
RCT_EXTERN_METHOD(seek:(NSString *)key
|
|
17
|
+
positionMillis:(nonnull NSNumber *)positionMillis
|
|
18
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
19
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
20
|
+
|
|
21
|
+
RCT_EXTERN_METHOD(stop:(NSString *)key
|
|
22
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
23
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
24
|
+
|
|
25
|
+
@end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import React
|
|
4
|
+
@objc(ChatSdkAudioPlayer)
|
|
5
|
+
class ChatSdkAudioPlayer: RCTEventEmitter {
|
|
6
|
+
|
|
7
|
+
private var player: AVPlayer?
|
|
8
|
+
private var observedItem: AVPlayerItem?
|
|
9
|
+
private var timeObserver: Any?
|
|
10
|
+
private var endObserver: NSObjectProtocol?
|
|
11
|
+
private var currentKey: String?
|
|
12
|
+
private var durationMillis: Double = 0
|
|
13
|
+
private var listenerCount = 0
|
|
14
|
+
|
|
15
|
+
@objc override static func requiresMainQueueSetup() -> Bool { return false }
|
|
16
|
+
override func supportedEvents() -> [String]! { return ["ChatSdkAudioState"] }
|
|
17
|
+
override func startObserving() { listenerCount += 1 }
|
|
18
|
+
override func stopObserving() { listenerCount = max(0, listenerCount - 1) }
|
|
19
|
+
|
|
20
|
+
@objc(play:url:headers:resolver:rejecter:)
|
|
21
|
+
func play(_ key: String,
|
|
22
|
+
url urlString: String,
|
|
23
|
+
headers: NSDictionary?,
|
|
24
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
25
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
26
|
+
DispatchQueue.main.async {
|
|
27
|
+
if self.currentKey == key, let p = self.player {
|
|
28
|
+
self.activateSession()
|
|
29
|
+
p.play()
|
|
30
|
+
self.emit(key: key, position: self.currentPositionMillis(), duration: self.durationMillis, state: "playing")
|
|
31
|
+
resolve(nil)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
guard let url = URL(string: urlString) else {
|
|
36
|
+
reject("INVALID_URL", "URL не задан", nil)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
self.teardown()
|
|
41
|
+
self.currentKey = key
|
|
42
|
+
self.emit(key: key, position: 0, duration: 0, state: "loading")
|
|
43
|
+
|
|
44
|
+
var options: [String: Any] = [:]
|
|
45
|
+
if let h = headers as? [String: String], !h.isEmpty {
|
|
46
|
+
options["AVURLAssetHTTPHeaderFieldsKey"] = h
|
|
47
|
+
}
|
|
48
|
+
let asset = AVURLAsset(url: url, options: options)
|
|
49
|
+
let item = AVPlayerItem(asset: asset)
|
|
50
|
+
let p = AVPlayer(playerItem: item)
|
|
51
|
+
self.player = p
|
|
52
|
+
self.observedItem = item
|
|
53
|
+
|
|
54
|
+
self.activateSession()
|
|
55
|
+
|
|
56
|
+
item.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
|
|
57
|
+
|
|
58
|
+
let interval = CMTime(seconds: 0.25, preferredTimescale: 600)
|
|
59
|
+
self.timeObserver = p.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
|
60
|
+
guard let self = self, self.currentKey == key else { return }
|
|
61
|
+
if p.timeControlStatus == .playing {
|
|
62
|
+
let pos = CMTimeGetSeconds(time) * 1000
|
|
63
|
+
self.emit(key: key, position: pos.isFinite ? pos : 0, duration: self.durationMillis, state: "playing")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
self.endObserver = NotificationCenter.default.addObserver(
|
|
68
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
|
69
|
+
object: item,
|
|
70
|
+
queue: .main,
|
|
71
|
+
) { [weak self] _ in
|
|
72
|
+
guard let self = self, self.currentKey == key else { return }
|
|
73
|
+
self.emit(key: key, position: self.durationMillis, duration: self.durationMillis, state: "ended")
|
|
74
|
+
p.seek(to: .zero)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
p.play()
|
|
78
|
+
resolve(nil)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@objc(pause:resolver:rejecter:)
|
|
83
|
+
func pause(_ key: String,
|
|
84
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
85
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
86
|
+
DispatchQueue.main.async {
|
|
87
|
+
if self.currentKey == key, let p = self.player {
|
|
88
|
+
p.pause()
|
|
89
|
+
self.emit(key: key, position: self.currentPositionMillis(), duration: self.durationMillis, state: "paused")
|
|
90
|
+
}
|
|
91
|
+
resolve(nil)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@objc(seek:positionMillis:resolver:rejecter:)
|
|
96
|
+
func seek(_ key: String,
|
|
97
|
+
positionMillis: NSNumber,
|
|
98
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
99
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
100
|
+
DispatchQueue.main.async {
|
|
101
|
+
guard self.currentKey == key, let p = self.player else {
|
|
102
|
+
resolve(nil)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
let seconds = positionMillis.doubleValue / 1000
|
|
106
|
+
let target = CMTime(seconds: seconds, preferredTimescale: 600)
|
|
107
|
+
p.seek(to: target) { [weak self] _ in
|
|
108
|
+
guard let self = self, self.currentKey == key else { return }
|
|
109
|
+
let state = p.timeControlStatus == .playing ? "playing" : "paused"
|
|
110
|
+
self.emit(key: key, position: positionMillis.doubleValue, duration: self.durationMillis, state: state)
|
|
111
|
+
}
|
|
112
|
+
resolve(nil)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@objc(stop:resolver:rejecter:)
|
|
117
|
+
func stop(_ key: String,
|
|
118
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
119
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
120
|
+
DispatchQueue.main.async {
|
|
121
|
+
if self.currentKey == key {
|
|
122
|
+
self.teardown()
|
|
123
|
+
self.deactivateSession()
|
|
124
|
+
self.emit(key: key, position: 0, duration: 0, state: "stopped")
|
|
125
|
+
}
|
|
126
|
+
resolve(nil)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override func observeValue(forKeyPath keyPath: String?,
|
|
131
|
+
of object: Any?,
|
|
132
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
133
|
+
context: UnsafeMutableRawPointer?) {
|
|
134
|
+
guard keyPath == "status", let item = object as? AVPlayerItem, item === observedItem else { return }
|
|
135
|
+
switch item.status {
|
|
136
|
+
case .readyToPlay:
|
|
137
|
+
let dur = CMTimeGetSeconds(item.duration) * 1000
|
|
138
|
+
durationMillis = (dur.isFinite && dur > 0) ? dur : 0
|
|
139
|
+
emit(key: currentKey, position: currentPositionMillis(), duration: durationMillis, state: "playing")
|
|
140
|
+
case .failed:
|
|
141
|
+
emit(key: currentKey, position: 0, duration: 0, state: "error")
|
|
142
|
+
teardown()
|
|
143
|
+
default:
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private func currentPositionMillis() -> Double {
|
|
149
|
+
guard let p = player else { return 0 }
|
|
150
|
+
let s = CMTimeGetSeconds(p.currentTime())
|
|
151
|
+
return s.isFinite ? s * 1000 : 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private func teardown() {
|
|
155
|
+
if let obs = timeObserver { player?.removeTimeObserver(obs); timeObserver = nil }
|
|
156
|
+
if let end = endObserver { NotificationCenter.default.removeObserver(end); endObserver = nil }
|
|
157
|
+
if let item = observedItem { item.removeObserver(self, forKeyPath: "status"); observedItem = nil }
|
|
158
|
+
player?.pause()
|
|
159
|
+
player = nil
|
|
160
|
+
durationMillis = 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func activateSession() {
|
|
164
|
+
do {
|
|
165
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [])
|
|
166
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
167
|
+
} catch {
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func deactivateSession() {
|
|
173
|
+
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private func emit(key: String?, position: Double, duration: Double, state: String) {
|
|
177
|
+
guard listenerCount > 0 else { return }
|
|
178
|
+
sendEvent(withName: "ChatSdkAudioState", body: [
|
|
179
|
+
"key": key ?? "",
|
|
180
|
+
"positionMillis": NSNumber(value: position),
|
|
181
|
+
"durationMillis": NSNumber(value: duration),
|
|
182
|
+
"state": state,
|
|
183
|
+
])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override func invalidate() {
|
|
187
|
+
DispatchQueue.main.async {
|
|
188
|
+
self.teardown()
|
|
189
|
+
self.deactivateSession()
|
|
190
|
+
}
|
|
191
|
+
super.invalidate()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.audioController = void 0;
|
|
7
|
+
var _NativeChatSdkAudioPlayer = _interopRequireWildcard(require("../native/NativeChatSdkAudioPlayer.js"));
|
|
8
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
9
|
+
const IDLE = {
|
|
10
|
+
state: 'idle',
|
|
11
|
+
positionMillis: 0,
|
|
12
|
+
durationMillis: 0
|
|
13
|
+
};
|
|
14
|
+
class AudioController {
|
|
15
|
+
listeners = new Map();
|
|
16
|
+
states = new Map();
|
|
17
|
+
activeKey = null;
|
|
18
|
+
subscribed = false;
|
|
19
|
+
get isAvailable() {
|
|
20
|
+
return !!_NativeChatSdkAudioPlayer.default;
|
|
21
|
+
}
|
|
22
|
+
ensureSubscribed() {
|
|
23
|
+
if (this.subscribed || !_NativeChatSdkAudioPlayer.default) return;
|
|
24
|
+
this.subscribed = true;
|
|
25
|
+
(0, _NativeChatSdkAudioPlayer.onAudioState)(event => this.handleEvent(event));
|
|
26
|
+
}
|
|
27
|
+
handleEvent(event) {
|
|
28
|
+
const prev = this.getState(event.key);
|
|
29
|
+
let next;
|
|
30
|
+
switch (event.state) {
|
|
31
|
+
case 'loading':
|
|
32
|
+
next = {
|
|
33
|
+
state: 'loading',
|
|
34
|
+
positionMillis: 0,
|
|
35
|
+
durationMillis: event.durationMillis
|
|
36
|
+
};
|
|
37
|
+
break;
|
|
38
|
+
case 'playing':
|
|
39
|
+
next = {
|
|
40
|
+
state: 'playing',
|
|
41
|
+
positionMillis: event.positionMillis,
|
|
42
|
+
durationMillis: event.durationMillis || prev.durationMillis
|
|
43
|
+
};
|
|
44
|
+
break;
|
|
45
|
+
case 'paused':
|
|
46
|
+
next = {
|
|
47
|
+
state: 'paused',
|
|
48
|
+
positionMillis: event.positionMillis,
|
|
49
|
+
durationMillis: event.durationMillis || prev.durationMillis
|
|
50
|
+
};
|
|
51
|
+
break;
|
|
52
|
+
case 'ended':
|
|
53
|
+
case 'stopped':
|
|
54
|
+
next = {
|
|
55
|
+
state: 'idle',
|
|
56
|
+
positionMillis: 0,
|
|
57
|
+
durationMillis: event.durationMillis || prev.durationMillis
|
|
58
|
+
};
|
|
59
|
+
if (this.activeKey === event.key) this.activeKey = null;
|
|
60
|
+
break;
|
|
61
|
+
case 'error':
|
|
62
|
+
default:
|
|
63
|
+
next = {
|
|
64
|
+
state: 'error',
|
|
65
|
+
positionMillis: 0,
|
|
66
|
+
durationMillis: 0
|
|
67
|
+
};
|
|
68
|
+
if (this.activeKey === event.key) this.activeKey = null;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
this.setState(event.key, next);
|
|
72
|
+
}
|
|
73
|
+
setState(key, state) {
|
|
74
|
+
this.states.set(key, state);
|
|
75
|
+
this.listeners.get(key)?.forEach(listener => listener(state));
|
|
76
|
+
}
|
|
77
|
+
getState(key) {
|
|
78
|
+
return this.states.get(key) ?? IDLE;
|
|
79
|
+
}
|
|
80
|
+
subscribe(key, listener) {
|
|
81
|
+
this.ensureSubscribed();
|
|
82
|
+
let set = this.listeners.get(key);
|
|
83
|
+
if (!set) {
|
|
84
|
+
set = new Set();
|
|
85
|
+
this.listeners.set(key, set);
|
|
86
|
+
}
|
|
87
|
+
set.add(listener);
|
|
88
|
+
listener(this.getState(key));
|
|
89
|
+
return () => {
|
|
90
|
+
set.delete(listener);
|
|
91
|
+
if (set.size === 0) this.listeners.delete(key);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async play(key, url, headers = {}) {
|
|
95
|
+
if (!_NativeChatSdkAudioPlayer.default) return;
|
|
96
|
+
this.ensureSubscribed();
|
|
97
|
+
if (this.activeKey && this.activeKey !== key) {
|
|
98
|
+
const prevKey = this.activeKey;
|
|
99
|
+
const prev = this.getState(prevKey);
|
|
100
|
+
this.setState(prevKey, {
|
|
101
|
+
state: 'idle',
|
|
102
|
+
positionMillis: 0,
|
|
103
|
+
durationMillis: prev.durationMillis
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
this.activeKey = key;
|
|
107
|
+
const current = this.getState(key);
|
|
108
|
+
if (current.state === 'idle' || current.state === 'error') {
|
|
109
|
+
this.setState(key, {
|
|
110
|
+
...current,
|
|
111
|
+
state: 'loading',
|
|
112
|
+
positionMillis: 0
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await _NativeChatSdkAudioPlayer.default.play(key, url, headers);
|
|
117
|
+
} catch {
|
|
118
|
+
this.setState(key, {
|
|
119
|
+
state: 'error',
|
|
120
|
+
positionMillis: 0,
|
|
121
|
+
durationMillis: 0
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async pause(key) {
|
|
126
|
+
if (!_NativeChatSdkAudioPlayer.default) return;
|
|
127
|
+
try {
|
|
128
|
+
await _NativeChatSdkAudioPlayer.default.pause(key);
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
async seek(key, positionMillis) {
|
|
132
|
+
if (!_NativeChatSdkAudioPlayer.default) return;
|
|
133
|
+
try {
|
|
134
|
+
await _NativeChatSdkAudioPlayer.default.seek(key, positionMillis);
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
async stop(key) {
|
|
138
|
+
if (!_NativeChatSdkAudioPlayer.default) return;
|
|
139
|
+
try {
|
|
140
|
+
await _NativeChatSdkAudioPlayer.default.stop(key);
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const audioController = exports.audioController = new AudioController();
|
|
145
|
+
//# sourceMappingURL=audioController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_NativeChatSdkAudioPlayer","_interopRequireWildcard","require","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","IDLE","state","positionMillis","durationMillis","AudioController","listeners","Map","states","activeKey","subscribed","isAvailable","AudioModule","ensureSubscribed","onAudioState","event","handleEvent","prev","getState","key","next","setState","forEach","listener","subscribe","Set","add","delete","size","play","url","headers","prevKey","current","pause","seek","stop","audioController","exports"],"sourceRoot":"..\\..\\..\\src","sources":["audio/audioController.ts"],"mappings":"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","ignoreList":[]}
|