@ion299/sdk-react-native 0.1.0-beta.4 → 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 +24 -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/components/MessageInput.js +9 -13
- package/lib/commonjs/components/MessageInput.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/components/MessageInput.js +9 -13
- package/lib/module/components/MessageInput.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/components/MessageInput.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/components/MessageInput.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 +2 -4
- package/src/audio/audioController.ts +144 -0
- package/src/components/AudioMessage.tsx +198 -0
- package/src/components/MessageBubble.tsx +6 -0
- package/src/components/MessageInput.tsx +5 -13
- package/src/native/NativeChatSdkAudioPlayer.ts +54 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import AudioModule, {
|
|
2
|
+
onAudioState,
|
|
3
|
+
type AudioStateEvent,
|
|
4
|
+
} from '../native/NativeChatSdkAudioPlayer'
|
|
5
|
+
|
|
6
|
+
export interface AudioPlaybackState {
|
|
7
|
+
state: 'idle' | 'loading' | 'playing' | 'paused' | 'error'
|
|
8
|
+
positionMillis: number
|
|
9
|
+
durationMillis: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Listener = (state: AudioPlaybackState) => void
|
|
13
|
+
|
|
14
|
+
const IDLE: AudioPlaybackState = { state: 'idle', positionMillis: 0, durationMillis: 0 }
|
|
15
|
+
|
|
16
|
+
class AudioController {
|
|
17
|
+
private listeners = new Map<string, Set<Listener>>()
|
|
18
|
+
private states = new Map<string, AudioPlaybackState>()
|
|
19
|
+
private activeKey: string | null = null
|
|
20
|
+
private subscribed = false
|
|
21
|
+
|
|
22
|
+
get isAvailable(): boolean {
|
|
23
|
+
return !!AudioModule
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private ensureSubscribed() {
|
|
27
|
+
if (this.subscribed || !AudioModule) return
|
|
28
|
+
this.subscribed = true
|
|
29
|
+
onAudioState((event) => this.handleEvent(event))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private handleEvent(event: AudioStateEvent) {
|
|
33
|
+
const prev = this.getState(event.key)
|
|
34
|
+
let next: AudioPlaybackState
|
|
35
|
+
|
|
36
|
+
switch (event.state) {
|
|
37
|
+
case 'loading':
|
|
38
|
+
next = { state: 'loading', positionMillis: 0, durationMillis: event.durationMillis }
|
|
39
|
+
break
|
|
40
|
+
case 'playing':
|
|
41
|
+
next = {
|
|
42
|
+
state: 'playing',
|
|
43
|
+
positionMillis: event.positionMillis,
|
|
44
|
+
durationMillis: event.durationMillis || prev.durationMillis,
|
|
45
|
+
}
|
|
46
|
+
break
|
|
47
|
+
case 'paused':
|
|
48
|
+
next = {
|
|
49
|
+
state: 'paused',
|
|
50
|
+
positionMillis: event.positionMillis,
|
|
51
|
+
durationMillis: event.durationMillis || prev.durationMillis,
|
|
52
|
+
}
|
|
53
|
+
break
|
|
54
|
+
case 'ended':
|
|
55
|
+
case 'stopped':
|
|
56
|
+
next = { state: 'idle', positionMillis: 0, durationMillis: event.durationMillis || prev.durationMillis }
|
|
57
|
+
if (this.activeKey === event.key) this.activeKey = null
|
|
58
|
+
break
|
|
59
|
+
case 'error':
|
|
60
|
+
default:
|
|
61
|
+
next = { state: 'error', positionMillis: 0, durationMillis: 0 }
|
|
62
|
+
if (this.activeKey === event.key) this.activeKey = null
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.setState(event.key, next)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setState(key: string, state: AudioPlaybackState) {
|
|
70
|
+
this.states.set(key, state)
|
|
71
|
+
this.listeners.get(key)?.forEach((listener) => listener(state))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getState(key: string): AudioPlaybackState {
|
|
75
|
+
return this.states.get(key) ?? IDLE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
subscribe(key: string, listener: Listener): () => void {
|
|
79
|
+
this.ensureSubscribed()
|
|
80
|
+
let set = this.listeners.get(key)
|
|
81
|
+
if (!set) {
|
|
82
|
+
set = new Set()
|
|
83
|
+
this.listeners.set(key, set)
|
|
84
|
+
}
|
|
85
|
+
set.add(listener)
|
|
86
|
+
listener(this.getState(key))
|
|
87
|
+
return () => {
|
|
88
|
+
set!.delete(listener)
|
|
89
|
+
if (set!.size === 0) this.listeners.delete(key)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async play(key: string, url: string, headers: Record<string, string> = {}): Promise<void> {
|
|
94
|
+
if (!AudioModule) return
|
|
95
|
+
this.ensureSubscribed()
|
|
96
|
+
|
|
97
|
+
if (this.activeKey && this.activeKey !== key) {
|
|
98
|
+
const prevKey = this.activeKey
|
|
99
|
+
const prev = this.getState(prevKey)
|
|
100
|
+
this.setState(prevKey, { state: 'idle', positionMillis: 0, durationMillis: prev.durationMillis })
|
|
101
|
+
}
|
|
102
|
+
this.activeKey = key
|
|
103
|
+
|
|
104
|
+
const current = this.getState(key)
|
|
105
|
+
if (current.state === 'idle' || current.state === 'error') {
|
|
106
|
+
this.setState(key, { ...current, state: 'loading', positionMillis: 0 })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await AudioModule.play(key, url, headers)
|
|
111
|
+
} catch {
|
|
112
|
+
this.setState(key, { state: 'error', positionMillis: 0, durationMillis: 0 })
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async pause(key: string): Promise<void> {
|
|
117
|
+
if (!AudioModule) return
|
|
118
|
+
try {
|
|
119
|
+
await AudioModule.pause(key)
|
|
120
|
+
} catch {
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async seek(key: string, positionMillis: number): Promise<void> {
|
|
126
|
+
if (!AudioModule) return
|
|
127
|
+
try {
|
|
128
|
+
await AudioModule.seek(key, positionMillis)
|
|
129
|
+
} catch {
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async stop(key: string): Promise<void> {
|
|
135
|
+
if (!AudioModule) return
|
|
136
|
+
try {
|
|
137
|
+
await AudioModule.stop(key)
|
|
138
|
+
} catch {
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const audioController = new AudioController()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
GestureResponderEvent,
|
|
5
|
+
LayoutChangeEvent,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
TouchableOpacity,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native'
|
|
11
|
+
import { audioController, type AudioPlaybackState } from '../audio/audioController'
|
|
12
|
+
import type { ChatAttachment } from '../types'
|
|
13
|
+
import type { ChatTheme } from '../theme'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
attachment: ChatAttachment
|
|
17
|
+
isOutbound: boolean
|
|
18
|
+
theme: ChatTheme
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function attachmentKey(attachment: ChatAttachment): string {
|
|
22
|
+
return attachment.id > 0 ? `a${attachment.id}` : `t${attachment.url}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatTime(millis: number): string {
|
|
26
|
+
if (!Number.isFinite(millis) || millis <= 0) return '0:00'
|
|
27
|
+
const totalSeconds = Math.floor(millis / 1000)
|
|
28
|
+
const minutes = Math.floor(totalSeconds / 60)
|
|
29
|
+
const seconds = totalSeconds % 60
|
|
30
|
+
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function AudioMessage({ attachment, isOutbound, theme }: Props) {
|
|
34
|
+
const key = attachmentKey(attachment)
|
|
35
|
+
const [pb, setPb] = useState<AudioPlaybackState>(() => audioController.getState(key))
|
|
36
|
+
const [trackWidth, setTrackWidth] = useState(0)
|
|
37
|
+
|
|
38
|
+
useEffect(() => audioController.subscribe(key, setPb), [key])
|
|
39
|
+
|
|
40
|
+
const isPlaying = pb.state === 'playing'
|
|
41
|
+
const isLoading = pb.state === 'loading'
|
|
42
|
+
const isError = pb.state === 'error'
|
|
43
|
+
const duration = pb.durationMillis
|
|
44
|
+
const position = pb.positionMillis
|
|
45
|
+
const progress = duration > 0 ? Math.min(1, position / duration) : 0
|
|
46
|
+
|
|
47
|
+
const accent = isOutbound ? '#ffffff' : theme.primaryColor
|
|
48
|
+
const trackBg = isOutbound ? 'rgba(255,255,255,0.30)' : 'rgba(0,0,0,0.12)'
|
|
49
|
+
const timeColor = isOutbound ? 'rgba(255,255,255,0.75)' : theme.systemText
|
|
50
|
+
const iconColor = isOutbound ? theme.primaryColor : '#ffffff'
|
|
51
|
+
|
|
52
|
+
const onToggle = () => {
|
|
53
|
+
if (isPlaying) {
|
|
54
|
+
void audioController.pause(key)
|
|
55
|
+
} else {
|
|
56
|
+
void audioController.play(key, attachment.url)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const onSeek = (e: GestureResponderEvent) => {
|
|
61
|
+
if (duration <= 0 || trackWidth <= 0) return
|
|
62
|
+
const ratio = Math.max(0, Math.min(1, e.nativeEvent.locationX / trackWidth))
|
|
63
|
+
void audioController.seek(key, ratio * duration)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const onTrackLayout = (e: LayoutChangeEvent) => {
|
|
67
|
+
setTrackWidth(e.nativeEvent.layout.width)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const timeLabel = duration > 0
|
|
71
|
+
? `${formatTime(position)} / ${formatTime(duration)}`
|
|
72
|
+
: isError
|
|
73
|
+
? 'Не удалось воспроизвести'
|
|
74
|
+
: formatTime(position)
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={styles.root}>
|
|
78
|
+
<TouchableOpacity
|
|
79
|
+
style={[styles.button, { backgroundColor: accent }]}
|
|
80
|
+
onPress={onToggle}
|
|
81
|
+
activeOpacity={0.8}
|
|
82
|
+
disabled={isLoading}
|
|
83
|
+
>
|
|
84
|
+
{isLoading ? (
|
|
85
|
+
<ActivityIndicator size="small" color={iconColor} />
|
|
86
|
+
) : isPlaying ? (
|
|
87
|
+
<PauseIcon color={iconColor} />
|
|
88
|
+
) : (
|
|
89
|
+
<PlayIcon color={iconColor} />
|
|
90
|
+
)}
|
|
91
|
+
</TouchableOpacity>
|
|
92
|
+
|
|
93
|
+
<View style={styles.body}>
|
|
94
|
+
<TouchableOpacity
|
|
95
|
+
activeOpacity={1}
|
|
96
|
+
onPress={onSeek}
|
|
97
|
+
onLayout={onTrackLayout}
|
|
98
|
+
style={styles.trackWrap}
|
|
99
|
+
>
|
|
100
|
+
<View style={[styles.track, { backgroundColor: trackBg }]}>
|
|
101
|
+
<View
|
|
102
|
+
style={[styles.trackFill, { backgroundColor: accent, width: `${progress * 100}%` }]}
|
|
103
|
+
/>
|
|
104
|
+
<View
|
|
105
|
+
style={[
|
|
106
|
+
styles.thumb,
|
|
107
|
+
{ backgroundColor: accent, left: `${progress * 100}%` },
|
|
108
|
+
]}
|
|
109
|
+
/>
|
|
110
|
+
</View>
|
|
111
|
+
</TouchableOpacity>
|
|
112
|
+
<Text style={[styles.time, { color: timeColor }]}>{timeLabel}</Text>
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function PlayIcon({ color }: { color: string }) {
|
|
119
|
+
return <View style={[styles.playTriangle, { borderLeftColor: color }]} />
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function PauseIcon({ color }: { color: string }) {
|
|
123
|
+
return (
|
|
124
|
+
<View style={styles.pauseWrap}>
|
|
125
|
+
<View style={[styles.pauseBar, { backgroundColor: color }]} />
|
|
126
|
+
<View style={[styles.pauseBar, { backgroundColor: color }]} />
|
|
127
|
+
</View>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const BUTTON_SIZE = 40
|
|
132
|
+
|
|
133
|
+
const styles = StyleSheet.create({
|
|
134
|
+
root: {
|
|
135
|
+
flexDirection: 'row',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
gap: 10,
|
|
138
|
+
minWidth: 200,
|
|
139
|
+
paddingVertical: 2,
|
|
140
|
+
},
|
|
141
|
+
button: {
|
|
142
|
+
width: BUTTON_SIZE,
|
|
143
|
+
height: BUTTON_SIZE,
|
|
144
|
+
borderRadius: BUTTON_SIZE / 2,
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
justifyContent: 'center',
|
|
147
|
+
flexShrink: 0,
|
|
148
|
+
},
|
|
149
|
+
body: {
|
|
150
|
+
flex: 1,
|
|
151
|
+
gap: 4,
|
|
152
|
+
},
|
|
153
|
+
trackWrap: {
|
|
154
|
+
paddingVertical: 8,
|
|
155
|
+
justifyContent: 'center',
|
|
156
|
+
},
|
|
157
|
+
track: {
|
|
158
|
+
height: 4,
|
|
159
|
+
borderRadius: 2,
|
|
160
|
+
},
|
|
161
|
+
trackFill: {
|
|
162
|
+
height: 4,
|
|
163
|
+
borderRadius: 2,
|
|
164
|
+
},
|
|
165
|
+
thumb: {
|
|
166
|
+
position: 'absolute',
|
|
167
|
+
top: -3,
|
|
168
|
+
width: 10,
|
|
169
|
+
height: 10,
|
|
170
|
+
borderRadius: 5,
|
|
171
|
+
marginLeft: -5,
|
|
172
|
+
},
|
|
173
|
+
time: {
|
|
174
|
+
fontSize: 11,
|
|
175
|
+
lineHeight: 14,
|
|
176
|
+
},
|
|
177
|
+
playTriangle: {
|
|
178
|
+
width: 0,
|
|
179
|
+
height: 0,
|
|
180
|
+
backgroundColor: 'transparent',
|
|
181
|
+
borderStyle: 'solid',
|
|
182
|
+
borderTopWidth: 7,
|
|
183
|
+
borderBottomWidth: 7,
|
|
184
|
+
borderLeftWidth: 12,
|
|
185
|
+
borderTopColor: 'transparent',
|
|
186
|
+
borderBottomColor: 'transparent',
|
|
187
|
+
marginLeft: 3,
|
|
188
|
+
},
|
|
189
|
+
pauseWrap: {
|
|
190
|
+
flexDirection: 'row',
|
|
191
|
+
gap: 4,
|
|
192
|
+
},
|
|
193
|
+
pauseBar: {
|
|
194
|
+
width: 4,
|
|
195
|
+
height: 14,
|
|
196
|
+
borderRadius: 1,
|
|
197
|
+
},
|
|
198
|
+
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
|
|
3
3
|
import { attachmentDisplayName } from '../attachmentUtils'
|
|
4
|
+
import { audioController } from '../audio/audioController'
|
|
5
|
+
import { AudioMessage } from './AudioMessage'
|
|
4
6
|
import type { ChatAttachment, ChatButton, ChatMessage } from '../types'
|
|
5
7
|
import type { ChatTheme } from '../theme'
|
|
6
8
|
|
|
@@ -123,6 +125,10 @@ function AttachmentView({
|
|
|
123
125
|
)
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
if (attachment.type === 'audio' && audioController.isAvailable) {
|
|
129
|
+
return <AudioMessage attachment={attachment} isOutbound={isOutbound} theme={theme} />
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
const nameColor = isOutbound ? theme.outboundText : theme.inboundText
|
|
127
133
|
const sizeColor = isOutbound ? 'rgba(255,255,255,0.65)' : theme.systemText
|
|
128
134
|
const displayName = attachmentDisplayName(attachment)
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
TouchableOpacity,
|
|
12
12
|
View,
|
|
13
13
|
} from 'react-native'
|
|
14
|
-
import Svg, { Path } from 'react-native-svg'
|
|
15
14
|
import { INPUT_BOTTOM_PADDING } from '../safeArea'
|
|
16
15
|
import { pickFiles as defaultPickFiles } from '../filePicker'
|
|
17
16
|
import type { ChatTheme } from '../theme'
|
|
@@ -122,18 +121,7 @@ export function MessageInput({ theme, isSending, onSend, onPickFiles, strings }:
|
|
|
122
121
|
{picking ? (
|
|
123
122
|
<ActivityIndicator color={theme.systemText} size="small" />
|
|
124
123
|
) : (
|
|
125
|
-
<
|
|
126
|
-
width={20}
|
|
127
|
-
height={20}
|
|
128
|
-
viewBox="0 0 32 32"
|
|
129
|
-
fill="none"
|
|
130
|
-
stroke={theme.primaryColor}
|
|
131
|
-
strokeWidth={2}
|
|
132
|
-
strokeLinecap="round"
|
|
133
|
-
strokeLinejoin="round"
|
|
134
|
-
>
|
|
135
|
-
<Path d="M10 9 L10 24 C10 28 13 30 16 30 19 30 22 28 22 24 L22 6 C22 3 20 2 18 2 16 2 14 3 14 6 L14 23 C14 24 15 25 16 25 17 25 18 24 18 23 L18 9" />
|
|
136
|
-
</Svg>
|
|
124
|
+
<Text style={[styles.attachIcon, { color: theme.primaryColor }]}>+</Text>
|
|
137
125
|
)}
|
|
138
126
|
</TouchableOpacity>
|
|
139
127
|
|
|
@@ -257,6 +245,10 @@ const styles = StyleSheet.create({
|
|
|
257
245
|
justifyContent: 'center',
|
|
258
246
|
flexShrink: 0,
|
|
259
247
|
},
|
|
248
|
+
attachIcon: {
|
|
249
|
+
fontSize: 26,
|
|
250
|
+
lineHeight: 28,
|
|
251
|
+
},
|
|
260
252
|
inputWrap: {
|
|
261
253
|
flex: 1,
|
|
262
254
|
borderWidth: 1,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NativeEventEmitter, NativeModules, TurboModuleRegistry } from 'react-native'
|
|
2
|
+
import type { EventSubscription, TurboModule } from 'react-native'
|
|
3
|
+
|
|
4
|
+
export type NativeAudioState =
|
|
5
|
+
| 'loading'
|
|
6
|
+
| 'playing'
|
|
7
|
+
| 'paused'
|
|
8
|
+
| 'ended'
|
|
9
|
+
| 'stopped'
|
|
10
|
+
| 'error'
|
|
11
|
+
|
|
12
|
+
export interface AudioStateEvent {
|
|
13
|
+
key: string
|
|
14
|
+
positionMillis: number
|
|
15
|
+
durationMillis: number
|
|
16
|
+
state: NativeAudioState
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Spec extends TurboModule {
|
|
20
|
+
play(key: string, url: string, headers: Record<string, string>): Promise<void>
|
|
21
|
+
pause(key: string): Promise<void>
|
|
22
|
+
seek(key: string, positionMillis: number): Promise<void>
|
|
23
|
+
stop(key: string): Promise<void>
|
|
24
|
+
addListener(eventName: string): void
|
|
25
|
+
removeListeners(count: number): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MODULE_NAME = 'ChatSdkAudioPlayer'
|
|
29
|
+
|
|
30
|
+
function loadModule(): Spec | null {
|
|
31
|
+
try {
|
|
32
|
+
return TurboModuleRegistry.get<Spec>(MODULE_NAME)
|
|
33
|
+
} catch {
|
|
34
|
+
return (NativeModules[MODULE_NAME] as Spec | undefined) ?? null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const moduleRef = loadModule()
|
|
39
|
+
|
|
40
|
+
export default moduleRef
|
|
41
|
+
|
|
42
|
+
let emitter: NativeEventEmitter | null = null
|
|
43
|
+
|
|
44
|
+
function getEmitter(): NativeEventEmitter | null {
|
|
45
|
+
if (!moduleRef) return null
|
|
46
|
+
if (!emitter) emitter = new NativeEventEmitter(moduleRef as unknown as never)
|
|
47
|
+
return emitter
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function onAudioState(
|
|
51
|
+
handler: (event: AudioStateEvent) => void,
|
|
52
|
+
): EventSubscription | null {
|
|
53
|
+
return getEmitter()?.addListener('ChatSdkAudioState', handler) ?? null
|
|
54
|
+
}
|