expo-nodemediaclient 0.1.2 → 0.2.0
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 +6 -0
- package/README.md +203 -19
- package/android/build.gradle +4 -5
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodeMediaClientModule.kt +1 -1
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePlayerView.kt +15 -20
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePlayerViewModule.kt +58 -0
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePublisherView.kt +142 -0
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePublisherViewModule.kt +95 -0
- package/build/ExpoNodeMediaClientModule.js.map +1 -1
- package/build/ExpoNodePlayerView.d.ts +9 -3
- package/build/ExpoNodePlayerView.d.ts.map +1 -1
- package/build/ExpoNodePlayerView.js +3 -18
- package/build/ExpoNodePlayerView.js.map +1 -1
- package/build/ExpoNodePublisherView.d.ts +87 -0
- package/build/ExpoNodePublisherView.d.ts.map +1 -0
- package/build/ExpoNodePublisherView.js +44 -0
- package/build/ExpoNodePublisherView.js.map +1 -0
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -5
- package/build/index.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/ExpoNodeMediaClientModule.swift +1 -1
- package/ios/ExpoNodePlayerView.swift +76 -70
- package/ios/{ExpoNodePlayerModule.swift → ExpoNodePlayerViewModule.swift} +8 -6
- package/ios/ExpoNodePublisherView.swift +166 -0
- package/ios/ExpoNodePublisherViewModule.swift +107 -0
- package/ios/ExpoNodemediaclient.podspec +2 -3
- package/package.json +1 -1
- package/src/ExpoNodeMediaClientModule.tsx +1 -1
- package/src/ExpoNodePlayerView.tsx +12 -25
- package/src/ExpoNodePublisherView.tsx +95 -0
- package/src/index.ts +4 -7
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePlayerModule.kt +0 -60
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ if (Platform.OS === 'ios') {
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
### 播放
|
|
42
|
+
|
|
42
43
|
```js
|
|
43
44
|
import { NodePlayer, NodePlayerRef } from 'expo-nodemediaclient';
|
|
44
45
|
import { useRef, useState } from 'react';
|
|
@@ -71,36 +72,219 @@ export default function App() {
|
|
|
71
72
|
</SafeAreaView>
|
|
72
73
|
);
|
|
73
74
|
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 组件属性
|
|
78
|
+
|
|
79
|
+
| 属性 | 类型 | 说明 |
|
|
80
|
+
|------|------|------|
|
|
81
|
+
| `url` | `string` | 播放地址 (支持 RTMP、RTSP、HLS、HTTP-FLV) |
|
|
82
|
+
| `bufferTime` | `number` | 缓冲时间 (毫秒),默认 1000 |
|
|
83
|
+
| `scaleMode` | `number` | 缩放模式,`0`=填充,`1`=适应,`2`=拉伸 |
|
|
84
|
+
| `volume` | `number` | 音量 (0.0 - 1.0),默认 1.0 |
|
|
85
|
+
| `onEventCallback` | `(event) => void` | 事件回调 |
|
|
86
|
+
|
|
87
|
+
### 常用方法
|
|
74
88
|
|
|
89
|
+
```js
|
|
90
|
+
// 开始播放
|
|
91
|
+
playerRef.current?.start(url);
|
|
92
|
+
|
|
93
|
+
// 停止播放
|
|
94
|
+
playerRef.current?.stop();
|
|
75
95
|
```
|
|
76
96
|
|
|
77
|
-
###
|
|
97
|
+
### 事件回调
|
|
98
|
+
|
|
78
99
|
```js
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
const handleEvent = (event: { nativeEvent: NodePlayerEventCallback }) => {
|
|
101
|
+
console.log('事件码:', event.nativeEvent.event);
|
|
102
|
+
console.log('消息:', event.nativeEvent.msg);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
<NodePlayer
|
|
106
|
+
onEventCallback={handleEvent}
|
|
107
|
+
// ...其他属性
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## 推流
|
|
113
|
+
|
|
114
|
+
### 基本用法
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { NodePublisher, NodePublisherRef } from 'expo-nodemediaclient';
|
|
118
|
+
import { useRef, useState } from 'react';
|
|
119
|
+
|
|
120
|
+
export default function PublisherScreen() {
|
|
121
|
+
const [url, setUrl] = useState('rtmp://192.168.0.2/live/stream');
|
|
122
|
+
const [isPublishing, setIsPublishing] = useState(false);
|
|
123
|
+
const publisherRef = useRef<NodePublisherRef>(null);
|
|
124
|
+
|
|
125
|
+
const handleTogglePublish = () => {
|
|
126
|
+
if (!isPublishing) {
|
|
127
|
+
setIsPublishing(true);
|
|
128
|
+
publisherRef.current?.start(url);
|
|
129
|
+
} else {
|
|
130
|
+
setIsPublishing(false);
|
|
131
|
+
publisherRef.current?.stop();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
91
134
|
|
|
92
135
|
return (
|
|
93
136
|
<SafeAreaView style={{ flex: 1 }}>
|
|
94
|
-
<
|
|
95
|
-
ref={
|
|
137
|
+
<NodePublisher
|
|
138
|
+
ref={publisherRef}
|
|
139
|
+
style={{ flex: 1, backgroundColor: '#000' }}
|
|
96
140
|
url={url}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
audioParam={{
|
|
142
|
+
codecid: NodePublisher.NMC_CODEC_ID_AAC,
|
|
143
|
+
profile: NodePublisher.NMC_PROFILE_AUTO,
|
|
144
|
+
channels: 2,
|
|
145
|
+
samplingRate: 44100,
|
|
146
|
+
bitrate: 64_000,
|
|
147
|
+
}}
|
|
148
|
+
videoParam={{
|
|
149
|
+
codecid: NodePublisher.NMC_CODEC_ID_H264,
|
|
150
|
+
profile: NodePublisher.NMC_PROFILE_AUTO,
|
|
151
|
+
width: 720,
|
|
152
|
+
height: 1280,
|
|
153
|
+
fps: 30,
|
|
154
|
+
bitrate: 2000_000,
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
<Button
|
|
158
|
+
title={isPublishing ? "停止" : "推流"}
|
|
159
|
+
onPress={handleTogglePublish}
|
|
100
160
|
/>
|
|
101
161
|
</SafeAreaView>
|
|
102
162
|
);
|
|
103
163
|
}
|
|
104
164
|
```
|
|
105
165
|
|
|
106
|
-
|
|
166
|
+
### 组件属性
|
|
167
|
+
|
|
168
|
+
| 属性 | 类型 | 说明 |
|
|
169
|
+
|------|------|------|
|
|
170
|
+
| `url` | `string` | 推流地址 (RTMP) |
|
|
171
|
+
| `audioParam` | `AudioParam` | 音频编码参数 |
|
|
172
|
+
| `videoParam` | `VideoParam` | 视频编码参数 |
|
|
173
|
+
| `videoOrientation` | `number` | 视频方向,`NodePublisher.VIDEO_ORIENTATION_PORTRAIT` 或 `NodePublisher.VIDEO_ORIENTATION_LANDSCAPE` |
|
|
174
|
+
| `keyFrameInterval` | `number` | 关键帧间隔 (秒),默认 2 |
|
|
175
|
+
| `frontCamera` | `boolean` | 是否使用前置摄像头,默认 false |
|
|
176
|
+
| `cameraFrontMirror` | `boolean` | 前置摄像头是否镜像,默认 true |
|
|
177
|
+
| `HWAccelEnable` | `boolean` | 是否启用硬件加速,默认 true |
|
|
178
|
+
| `denoiseEnable` | `boolean` | 是否启用降噪,默认 true |
|
|
179
|
+
| `colorStyleId` | `number` | 色彩风格ID,如 `NodePublisher.EFFECTOR_STYLE_ID_FAIRSKIN` |
|
|
180
|
+
| `colorStyleIntensity` | `number` | 色彩强度 (0.0 - 1.0) |
|
|
181
|
+
| `smoothskinIntensity` | `number` | 磨皮强度 (0.0 - 1.0) |
|
|
182
|
+
| `onEventCallback` | `(event) => void` | 事件回调 |
|
|
183
|
+
|
|
184
|
+
### AudioParam
|
|
185
|
+
|
|
186
|
+
| 属性 | 类型 | 说明 |
|
|
187
|
+
|------|------|------|
|
|
188
|
+
| `codecid` | `number` | 编码ID,`NodePublisher.NMC_CODEC_ID_AAC` |
|
|
189
|
+
| `profile` | `number` | 编码profile,`NodePublisher.NMC_PROFILE_AUTO` |
|
|
190
|
+
| `channels` | `number` | 声道数,1 或 2 |
|
|
191
|
+
| `samplingRate` | `number` | 采样率,如 44100 |
|
|
192
|
+
| `bitrate` | `number` | 比特率,如 64000 |
|
|
193
|
+
|
|
194
|
+
### VideoParam
|
|
195
|
+
|
|
196
|
+
| 属性 | 类型 | 说明 |
|
|
197
|
+
|------|------|------|
|
|
198
|
+
| `codecid` | `number` | 编码ID,`NodePublisher.NMC_CODEC_ID_H264` 或 `NodePublisher.NMC_CODEC_ID_H265` |
|
|
199
|
+
| `profile` | `number` | 编码profile,`NodePublisher.NMC_PROFILE_AUTO` |
|
|
200
|
+
| `width` | `number` | 视频宽度 |
|
|
201
|
+
| `height` | `number` | 视频高度 |
|
|
202
|
+
| `fps` | `number` | 帧率 |
|
|
203
|
+
| `bitrate` | `number` | 比特率,如 2000000 |
|
|
204
|
+
|
|
205
|
+
### 常用方法
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
// 开始推流
|
|
209
|
+
publisherRef.current?.start(url);
|
|
210
|
+
|
|
211
|
+
// 停止推流
|
|
212
|
+
publisherRef.current?.stop();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 事件回调
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
const handleEvent = (event: { nativeEvent: NodePlayerEventCallback }) => {
|
|
219
|
+
console.log('事件码:', event.nativeEvent.event);
|
|
220
|
+
console.log('消息:', event.nativeEvent.msg);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
<NodePublisher
|
|
224
|
+
onEventCallback={handleEvent}
|
|
225
|
+
// ...其他属性
|
|
226
|
+
/>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 权限说明
|
|
230
|
+
|
|
231
|
+
使用推流功能需要摄像头和麦克风权限,建议使用 `expo-camera` 来申请权限。
|
|
232
|
+
|
|
233
|
+
首先安装依赖:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm install expo-camera
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
在 `app.json` 中配置 `expo-camera` 插件:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"expo": {
|
|
244
|
+
"plugins": [
|
|
245
|
+
[
|
|
246
|
+
"expo-camera",
|
|
247
|
+
{
|
|
248
|
+
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
|
|
249
|
+
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
|
|
250
|
+
"recordAudioAndroid": true
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
],
|
|
254
|
+
"ios": {
|
|
255
|
+
"infoPlist": {
|
|
256
|
+
"NSCameraUsageDescription": "需要访问摄像头以进行直播推流",
|
|
257
|
+
"NSMicrophoneUsageDescription": "需要访问麦克风以进行直播推流"
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"android": {
|
|
261
|
+
"permissions": [
|
|
262
|
+
"android.permission.CAMERA",
|
|
263
|
+
"android.permission.RECORD_AUDIO",
|
|
264
|
+
"android.permission.INTERNET"
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
在代码中申请权限:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
import { useCameraPermissions, useMicrophonePermissions } from 'expo-camera';
|
|
275
|
+
import { useEffect } from 'react';
|
|
276
|
+
|
|
277
|
+
export default function App() {
|
|
278
|
+
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
|
|
279
|
+
const [microphonePermission, requestMicrophonePermission] = useMicrophonePermissions();
|
|
280
|
+
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
requestCameraPermission();
|
|
283
|
+
requestMicrophonePermission();
|
|
284
|
+
}, []);
|
|
285
|
+
|
|
286
|
+
const hasPermission = cameraPermission?.granted && microphonePermission?.granted;
|
|
287
|
+
|
|
288
|
+
// ...
|
|
289
|
+
}
|
|
290
|
+
```
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'expo.modules.nodemediaclient'
|
|
4
|
-
version = '0.1.1'
|
|
5
4
|
|
|
6
5
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
6
|
apply from: expoModulesCorePlugin
|
|
@@ -23,10 +22,10 @@ if (useManagedAndroidSdkVersions) {
|
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
project.android {
|
|
26
|
-
compileSdkVersion safeExtGet("compileSdkVersion",
|
|
25
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
27
26
|
defaultConfig {
|
|
28
|
-
minSdkVersion safeExtGet("minSdkVersion",
|
|
29
|
-
targetSdkVersion safeExtGet("targetSdkVersion",
|
|
27
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
28
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
}
|
|
@@ -41,6 +40,6 @@ android {
|
|
|
41
40
|
abortOnError false
|
|
42
41
|
}
|
|
43
42
|
dependencies {
|
|
44
|
-
implementation 'com.github.NodeMedia:NodeMediaClient-Android:
|
|
43
|
+
implementation 'com.github.NodeMedia:NodeMediaClient-Android:4.0.11'
|
|
45
44
|
}
|
|
46
45
|
}
|
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright (c) 2025 NodeMedia Technology Co., Ltd.
|
|
3
|
-
// Created by Chen Mingliang on 2025-07-22.
|
|
4
|
-
// All rights reserved.
|
|
5
|
-
//
|
|
6
|
-
|
|
7
1
|
package expo.modules.nodemediaclient
|
|
8
2
|
|
|
9
3
|
import android.content.Context
|
|
10
|
-
import android.util.Log
|
|
11
|
-
import android.webkit.WebView
|
|
12
|
-
import android.webkit.WebViewClient
|
|
13
|
-
import android.widget.FrameLayout
|
|
14
4
|
import expo.modules.kotlin.AppContext
|
|
15
5
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
16
6
|
import expo.modules.kotlin.views.ExpoView
|
|
17
|
-
|
|
7
|
+
|
|
8
|
+
import android.util.Log
|
|
9
|
+
import android.widget.FrameLayout
|
|
10
|
+
import cn.nodemedia.NodePlayer
|
|
18
11
|
|
|
19
12
|
class ExpoNodePlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
20
|
-
// Creates and initializes an event dispatcher for the `onLoad` event.
|
|
21
|
-
// The name of the event is inferred from the value and needs to match the event name defined in the module.
|
|
22
|
-
private val onLoad by EventDispatcher()
|
|
23
13
|
private val TAG = "ExpoNodePlayerView"
|
|
24
14
|
private var np: NodePlayer? = null
|
|
15
|
+
private val onEventCallback by EventDispatcher()
|
|
16
|
+
|
|
25
17
|
var url = ""
|
|
26
18
|
var cryptoKey = "" // Add this property
|
|
27
|
-
set(value)
|
|
19
|
+
set(value) {
|
|
28
20
|
field = value
|
|
29
21
|
np?.setCryptoKey(value)
|
|
30
22
|
}
|
|
@@ -60,7 +52,7 @@ class ExpoNodePlayerView(context: Context, appContext: AppContext) : ExpoView(co
|
|
|
60
52
|
np?.setRTSPTransport(value)
|
|
61
53
|
}
|
|
62
54
|
|
|
63
|
-
var HWAccelEnable= true
|
|
55
|
+
var HWAccelEnable = true
|
|
64
56
|
set(value) {
|
|
65
57
|
field = value
|
|
66
58
|
np?.setHWAccelEnable(value)
|
|
@@ -76,6 +68,9 @@ class ExpoNodePlayerView(context: Context, appContext: AppContext) : ExpoView(co
|
|
|
76
68
|
override fun onAttachedToWindow() {
|
|
77
69
|
super.onAttachedToWindow()
|
|
78
70
|
np = NodePlayer(context, LICENSE_KEY)
|
|
71
|
+
np?.setOnNodePlayerEventListener { obj, event, msg ->
|
|
72
|
+
onEventCallback(mapOf("event" to event, "msg" to msg))
|
|
73
|
+
}
|
|
79
74
|
np?.attachView(videoView)
|
|
80
75
|
}
|
|
81
76
|
|
|
@@ -86,8 +81,8 @@ class ExpoNodePlayerView(context: Context, appContext: AppContext) : ExpoView(co
|
|
|
86
81
|
super.onDetachedFromWindow()
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
fun start(url: String?){
|
|
90
|
-
if(!url.isNullOrEmpty()) {
|
|
84
|
+
fun start(url: String?) {
|
|
85
|
+
if (!url.isNullOrEmpty()) {
|
|
91
86
|
this.url = url
|
|
92
87
|
}
|
|
93
88
|
np?.setVolume(this.volume)
|
|
@@ -101,7 +96,7 @@ class ExpoNodePlayerView(context: Context, appContext: AppContext) : ExpoView(co
|
|
|
101
96
|
np?.start(this.url)
|
|
102
97
|
}
|
|
103
98
|
|
|
104
|
-
fun stop(){
|
|
99
|
+
fun stop() {
|
|
105
100
|
np?.stop()
|
|
106
101
|
}
|
|
107
|
-
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package expo.modules.nodemediaclient
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
|
|
6
|
+
class ExpoNodePlayerViewModule : Module() {
|
|
7
|
+
override fun definition() = ModuleDefinition {
|
|
8
|
+
Name("ExpoNodePlayerView")
|
|
9
|
+
|
|
10
|
+
View(ExpoNodePlayerView::class) {
|
|
11
|
+
Events("onEventCallback")
|
|
12
|
+
|
|
13
|
+
Prop("url") { view: ExpoNodePlayerView, url: String ->
|
|
14
|
+
view.url = url
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Prop("volume") { view: ExpoNodePlayerView, volume: Float ->
|
|
18
|
+
view.volume = volume
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Prop("cryptoKey") { view: ExpoNodePlayerView, cryptoKey: String ->
|
|
22
|
+
view.cryptoKey = cryptoKey
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Prop("scaleMode") { view: ExpoNodePlayerView, scaleMode: Int ->
|
|
26
|
+
view.scaleMode = scaleMode
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Prop("bufferTime") { view: ExpoNodePlayerView, bufferTime: Int ->
|
|
30
|
+
view.bufferTime = bufferTime
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Prop("HTTPReferer") { view: ExpoNodePlayerView, HTTPReferer: String ->
|
|
34
|
+
view.HTTPReferer = HTTPReferer
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Prop("HTTPUserAgent") { view: ExpoNodePlayerView, HTTPUserAgent: String ->
|
|
38
|
+
view.HTTPUserAgent = HTTPUserAgent
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Prop("RTSPTransport") { view: ExpoNodePlayerView, RTSPTransport: String ->
|
|
42
|
+
view.RTSPTransport = RTSPTransport
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Prop("HWAccelEnable") { view: ExpoNodePlayerView, HWAccelEnable: Boolean ->
|
|
46
|
+
view.HWAccelEnable = HWAccelEnable
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
AsyncFunction("start") { view: ExpoNodePlayerView, url: String? ->
|
|
50
|
+
view.start(url)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
AsyncFunction("stop") { view: ExpoNodePlayerView ->
|
|
54
|
+
view.stop()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
package expo.modules.nodemediaclient
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import expo.modules.kotlin.AppContext
|
|
5
|
+
import expo.modules.kotlin.views.ExpoView
|
|
6
|
+
|
|
7
|
+
import android.widget.FrameLayout
|
|
8
|
+
import cn.nodemedia.NodePublisher
|
|
9
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
10
|
+
|
|
11
|
+
class ExpoNodePublisherView(context: Context, appContext: AppContext) :
|
|
12
|
+
ExpoView(context, appContext) {
|
|
13
|
+
private val TAG = "ExpoNodePublisherView"
|
|
14
|
+
private var np: NodePublisher? = null
|
|
15
|
+
private val onEventCallback by EventDispatcher()
|
|
16
|
+
|
|
17
|
+
var url = ""
|
|
18
|
+
var cryptoKey = ""
|
|
19
|
+
var HWAccelEnable = true
|
|
20
|
+
var denoiseEnable = true
|
|
21
|
+
|
|
22
|
+
// Color and effect parameters
|
|
23
|
+
var colorStyleId: Int? = null
|
|
24
|
+
set(value) {
|
|
25
|
+
field = value
|
|
26
|
+
value?.let { np?.setEffectStyle(it) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var colorStyleIntensity: Float? = null
|
|
30
|
+
set(value) {
|
|
31
|
+
field = value
|
|
32
|
+
value?.let { np?.setEffectParameter("style", it) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var smoothskinIntensity: Float? = null
|
|
36
|
+
set(value) {
|
|
37
|
+
field = value
|
|
38
|
+
value?.let { np?.setEffectParameter("smoothskin", it) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Audio parameters
|
|
42
|
+
var audioCodecId = NodePublisher.NMC_CODEC_ID_AAC
|
|
43
|
+
var audioProfile = NodePublisher.NMC_PROFILE_AUTO
|
|
44
|
+
var audioChannels = 2
|
|
45
|
+
var audioSamplingRate = 44100
|
|
46
|
+
var audioBitrate = 64000
|
|
47
|
+
|
|
48
|
+
// Video parameters
|
|
49
|
+
var videoCodecId = NodePublisher.NMC_CODEC_ID_H264
|
|
50
|
+
var videoProfile = NodePublisher.NMC_PROFILE_AUTO
|
|
51
|
+
var videoWidth = 720
|
|
52
|
+
var videoHeight = 1280
|
|
53
|
+
var videoFps = 30
|
|
54
|
+
var videoBitrate = 2000000
|
|
55
|
+
|
|
56
|
+
var frontCamera: Boolean? = null
|
|
57
|
+
set(value) {
|
|
58
|
+
field = value
|
|
59
|
+
value?.let {
|
|
60
|
+
np?.closeCamera()
|
|
61
|
+
np?.openCamera(if (it) 0 else 1)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Other parameters
|
|
66
|
+
var keyFrameInterval = 2
|
|
67
|
+
var videoOrientation = 1
|
|
68
|
+
|
|
69
|
+
var cameraFrontMirror: Boolean? = null
|
|
70
|
+
set(value) {
|
|
71
|
+
field = value
|
|
72
|
+
value?.let { np?.setCameraFrontMirror(it) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private val videoView = FrameLayout(context).apply {
|
|
76
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
init {
|
|
80
|
+
addView(videoView)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun onAttachedToWindow() {
|
|
84
|
+
super.onAttachedToWindow()
|
|
85
|
+
np = NodePublisher(context, LICENSE_KEY)
|
|
86
|
+
np?.setOnNodePublisherEventListener { obj, event, msg ->
|
|
87
|
+
onEventCallback(mapOf("event" to event, "msg" to msg))
|
|
88
|
+
}
|
|
89
|
+
applyAudioParams()
|
|
90
|
+
applyVideoParams()
|
|
91
|
+
np?.setCryptoKey(this.cryptoKey)
|
|
92
|
+
np?.setHWAccelEnable(this.HWAccelEnable)
|
|
93
|
+
np?.openCamera(if (frontCamera == true) 0 else 1)
|
|
94
|
+
np?.attachView(videoView)
|
|
95
|
+
|
|
96
|
+
// Apply color and effect params if set
|
|
97
|
+
colorStyleId?.let { np?.setEffectStyle(it) }
|
|
98
|
+
colorStyleIntensity?.let { np?.setEffectParameter("style", it) }
|
|
99
|
+
smoothskinIntensity?.let { np?.setEffectParameter("smoothskin", it) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun onDetachedFromWindow() {
|
|
103
|
+
np?.stop()
|
|
104
|
+
np?.closeCamera()
|
|
105
|
+
np?.detachView()
|
|
106
|
+
np = null
|
|
107
|
+
super.onDetachedFromWindow()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun applyAudioParams() {
|
|
111
|
+
np?.setAudioCodecParam(
|
|
112
|
+
audioCodecId, audioProfile,
|
|
113
|
+
audioSamplingRate, audioChannels, audioBitrate
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private fun applyVideoParams() {
|
|
118
|
+
np?.setVideoCodecParam(
|
|
119
|
+
videoCodecId,
|
|
120
|
+
videoProfile, videoWidth, videoHeight, videoFps, videoBitrate
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun start(url: String?) {
|
|
125
|
+
if (!url.isNullOrEmpty()) {
|
|
126
|
+
this.url = url
|
|
127
|
+
}
|
|
128
|
+
np?.start(this.url)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fun stop() {
|
|
132
|
+
np?.stop()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fun setEffectParameter(key: String, value: Float) {
|
|
136
|
+
np?.setEffectParameter(key, value)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fun setEffectStyle(style: Int) {
|
|
140
|
+
np?.setEffectStyle(style)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package expo.modules.nodemediaclient
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
|
|
6
|
+
class ExpoNodePublisherViewModule : Module() {
|
|
7
|
+
override fun definition() = ModuleDefinition {
|
|
8
|
+
Name("ExpoNodePublisherView")
|
|
9
|
+
|
|
10
|
+
View(ExpoNodePublisherView::class) {
|
|
11
|
+
Events("onEventCallback")
|
|
12
|
+
|
|
13
|
+
Prop("url") { view: ExpoNodePublisherView, url: String ->
|
|
14
|
+
view.url = url
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Prop("cryptoKey") { view: ExpoNodePublisherView, cryptoKey: String ->
|
|
18
|
+
view.cryptoKey = cryptoKey
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Prop("HWAccelEnable") { view: ExpoNodePublisherView, HWAccelEnable: Boolean ->
|
|
22
|
+
view.HWAccelEnable = HWAccelEnable
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Audio parameters
|
|
26
|
+
Prop("audioParam") { view: ExpoNodePublisherView, audioParam: Map<String, Int> ->
|
|
27
|
+
view.audioCodecId = audioParam["codecid"] as Int
|
|
28
|
+
view.audioProfile = audioParam["profile"] as Int
|
|
29
|
+
view.audioChannels = audioParam["channels"] as Int
|
|
30
|
+
view.audioSamplingRate = audioParam["samplingRate"] as Int
|
|
31
|
+
view.audioBitrate = audioParam["bitrate"] as Int
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Video parameters
|
|
35
|
+
Prop("videoParam") { view: ExpoNodePublisherView, videoParam: Map<String, Int> ->
|
|
36
|
+
view.videoCodecId = videoParam["codecid"] as Int
|
|
37
|
+
view.videoProfile = videoParam["profile"] as Int
|
|
38
|
+
view.videoWidth = videoParam["width"] as Int
|
|
39
|
+
view.videoHeight = videoParam["height"] as Int
|
|
40
|
+
view.videoFps = videoParam["fps"] as Int
|
|
41
|
+
view.videoBitrate = videoParam["bitrate"] as Int
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Prop("keyFrameInterval") { view: ExpoNodePublisherView, keyFrameInterval: Int ->
|
|
45
|
+
view.keyFrameInterval = keyFrameInterval
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Prop("videoOrientation") { view: ExpoNodePublisherView, videoOrientation: Int ->
|
|
49
|
+
view.videoOrientation = videoOrientation
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Prop("frontCamera") { view: ExpoNodePublisherView, frontCamera: Boolean ->
|
|
53
|
+
view.frontCamera = frontCamera
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Prop("cameraFrontMirror") { view: ExpoNodePublisherView, cameraFrontMirror: Boolean ->
|
|
57
|
+
view.cameraFrontMirror = cameraFrontMirror
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Prop("denoiseEnable") { view: ExpoNodePublisherView, denoiseEnable: Boolean ->
|
|
61
|
+
view.denoiseEnable = denoiseEnable
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Color and effect parameters
|
|
65
|
+
Prop("colorStyleId") { view: ExpoNodePublisherView, colorStyleId: Int ->
|
|
66
|
+
view.colorStyleId = colorStyleId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Prop("colorStyleIntensity") { view: ExpoNodePublisherView, colorStyleIntensity: Double ->
|
|
70
|
+
view.colorStyleIntensity = colorStyleIntensity.toFloat()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Prop("smoothskinIntensity") { view: ExpoNodePublisherView, smoothskinIntensity: Double ->
|
|
74
|
+
view.smoothskinIntensity = smoothskinIntensity.toFloat()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Methods
|
|
78
|
+
AsyncFunction("start") { view: ExpoNodePublisherView, url: String? ->
|
|
79
|
+
view.start(url)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
AsyncFunction("stop") { view: ExpoNodePublisherView ->
|
|
83
|
+
view.stop()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
AsyncFunction("setEffectParameter") { view: ExpoNodePublisherView, key: String, value: Float ->
|
|
87
|
+
view.setEffectParameter(key, value)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
AsyncFunction("setEffectStyle") { view: ExpoNodePublisherView, style: Int ->
|
|
91
|
+
view.setEffectStyle(style)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoNodeMediaClientModule.js","sourceRoot":"","sources":["../src/ExpoNodeMediaClientModule.tsx"],"names":[],"mappings":"AAAA,EAAE;AACF,qDAAqD;AACrD,4CAA4C;AAC5C,wBAAwB;AACxB,EAAE;AAGF,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAMzD,yDAAyD;AACzD,eAAe,mBAAmB,CAA4B,2BAA2B,CAAC,CAAC","sourcesContent":["//\n// Copyright (c) 2025 NodeMedia Technology Co., Ltd.\n// Created by Chen Mingliang on 2025-07-22.\n// All rights reserved.\n//\n\n\nimport { NativeModule, requireNativeModule } from 'expo';\n\ndeclare class ExpoNodeMediaClientModule extends NativeModule{\n setLicense(license: string): void;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<ExpoNodeMediaClientModule>('ExpoNodeMediaClientModule')
|
|
1
|
+
{"version":3,"file":"ExpoNodeMediaClientModule.js","sourceRoot":"","sources":["../src/ExpoNodeMediaClientModule.tsx"],"names":[],"mappings":"AAAA,EAAE;AACF,qDAAqD;AACrD,4CAA4C;AAC5C,wBAAwB;AACxB,EAAE;AAGF,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAMzD,yDAAyD;AACzD,eAAe,mBAAmB,CAA4B,2BAA2B,CAAC,CAAC","sourcesContent":["//\n// Copyright (c) 2025 NodeMedia Technology Co., Ltd.\n// Created by Chen Mingliang on 2025-07-22.\n// All rights reserved.\n//\n\n\nimport { NativeModule, requireNativeModule } from 'expo';\n\ndeclare class ExpoNodeMediaClientModule extends NativeModule{\n setLicense(license: string): void;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<ExpoNodeMediaClientModule>('ExpoNodeMediaClientModule');"]}
|