expo-nodemediaclient 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +55 -0
- package/README.md +203 -19
- package/android/build.gradle +6 -7
- 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 +175 -0
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePublisherViewModule.kt +111 -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 +91 -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 +174 -0
- package/ios/ExpoNodePublisherViewModule.swift +114 -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 +99 -0
- package/src/index.ts +4 -7
- package/android/src/main/java/expo/modules/nodemediaclient/ExpoNodePlayerModule.kt +0 -60
package/CHANGELOG.md
CHANGED
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
This is **expo-nodemediaclient**, an Expo native module providing live streaming capabilities using the NodeMedia SDK 4.0. It enables RTMP/RTSP/HLS/HTTP-FLV video playback and RTMP live streaming (publishing) for React Native applications using Expo's Continuous Native Generation (CNG) workflow.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
The module follows Expo's custom native module pattern with three main components:
|
|
12
|
+
|
|
13
|
+
- **NodeMediaClient** (`ExpoNodeMediaClientModule.tsx`) - SDK license management, requires separate license keys for iOS and Android
|
|
14
|
+
- **NodePlayer** (`ExpoNodePlayerView.tsx`) - Video playback component using `requireNativeViewManager`
|
|
15
|
+
- **NodePublisher** (`ExpoNodePublisherView.tsx`) - Camera capture and streaming component using `requireNativeViewManager`
|
|
16
|
+
|
|
17
|
+
The native implementations are in:
|
|
18
|
+
- `ios/ExpoNodemediaclient/` - iOS native code (Objective-C/Swift)
|
|
19
|
+
- `android/src/main/java/` - Android native code (Java)
|
|
20
|
+
|
|
21
|
+
Module configuration is in `expo-module.config.json` which maps platform-specific module names.
|
|
22
|
+
|
|
23
|
+
## Development Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run build # Build the module (TypeScript -> build/)
|
|
27
|
+
npm run clean # Clean build artifacts
|
|
28
|
+
npm run lint # Run linting
|
|
29
|
+
npm run test # Run tests
|
|
30
|
+
npm run prepare # Prepare module for development
|
|
31
|
+
npm run prepublishOnly # Prepare for publishing
|
|
32
|
+
npm run open:ios # Open example iOS project in Xcode
|
|
33
|
+
npm run open:android # Open example Android project in Android Studio
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The example app in `example/` demonstrates both Player and Publisher functionality with navigation between screens.
|
|
37
|
+
|
|
38
|
+
## Key Constants
|
|
39
|
+
|
|
40
|
+
NodePublisher exposes these important constants:
|
|
41
|
+
- `NMC_CODEC_ID_AAC`, `NMC_CODEC_ID_H264`, `NMC_CODEC_ID_H265` - Codec identifiers
|
|
42
|
+
- `NMC_PROFILE_AUTO` - Auto profile selection
|
|
43
|
+
- `VIDEO_ORIENTATION_PORTRAIT`, `VIDEO_ORIENTATION_LANDSCAPE` - Video orientation
|
|
44
|
+
- `EFFECTOR_STYLE_ID_FAIRSKIN` - Beauty filter style
|
|
45
|
+
|
|
46
|
+
## Event Callbacks
|
|
47
|
+
|
|
48
|
+
Both NodePlayer and NodePublisher support `onEventCallback` that receives `{ nativeEvent: { event: number, msg: string } }`. The `event` code indicates status changes (connection, buffering, error, etc.).
|
|
49
|
+
|
|
50
|
+
## Important Notes
|
|
51
|
+
|
|
52
|
+
- **License Required**: NodeMedia SDK requires separate license keys for iOS and Android from https://www.nodemedia.cn
|
|
53
|
+
- **CNG Required**: Projects must use Expo's Continuous Native Generation - run `npx expo prebuild` if using managed workflow
|
|
54
|
+
- **Permissions**: Publisher requires camera and microphone permissions; use `expo-camera` plugin for permission handling
|
|
55
|
+
- **SDK Version**: Currently on NodeMedia SDK 4.0 (updated in v0.2.0)
|
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
|
}
|
|
@@ -34,13 +33,13 @@ if (useManagedAndroidSdkVersions) {
|
|
|
34
33
|
android {
|
|
35
34
|
namespace "expo.modules.nodemediaclient"
|
|
36
35
|
defaultConfig {
|
|
37
|
-
versionCode
|
|
38
|
-
versionName "0.
|
|
36
|
+
versionCode 201
|
|
37
|
+
versionName "0.2.1"
|
|
39
38
|
}
|
|
40
39
|
lintOptions {
|
|
41
40
|
abortOnError false
|
|
42
41
|
}
|
|
43
42
|
dependencies {
|
|
44
|
-
implementation 'com.github.NodeMedia:NodeMediaClient-Android:
|
|
43
|
+
implementation 'com.github.NodeMedia:NodeMediaClient-Android:4.1.0'
|
|
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,175 @@
|
|
|
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
|
+
var zoomRatio: Float? = null
|
|
23
|
+
set(value) {
|
|
24
|
+
field = value
|
|
25
|
+
value?.let { np?.setZoomRatio(it) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var volume: Float? = null
|
|
29
|
+
set(value) {
|
|
30
|
+
field = value
|
|
31
|
+
value?.let { np?.setVolume(it) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var torchEnable: Boolean? = null
|
|
35
|
+
set(value) {
|
|
36
|
+
field = value
|
|
37
|
+
value?.let { np?.setTorchEnable(it) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Color and effect parameters
|
|
41
|
+
var colorStyleId: Int? = null
|
|
42
|
+
set(value) {
|
|
43
|
+
field = value
|
|
44
|
+
value?.let { np?.setEffectStyle(it) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var colorStyleIntensity: Float? = null
|
|
48
|
+
set(value) {
|
|
49
|
+
field = value
|
|
50
|
+
value?.let { np?.setEffectParameter("style", it) }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var smoothskinIntensity: Float? = null
|
|
54
|
+
set(value) {
|
|
55
|
+
field = value
|
|
56
|
+
value?.let { np?.setEffectParameter("smoothskin", it) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Audio parameters
|
|
60
|
+
var audioCodecId = NodePublisher.NMC_CODEC_ID_AAC
|
|
61
|
+
var audioProfile = NodePublisher.NMC_PROFILE_AUTO
|
|
62
|
+
var audioChannels = 2
|
|
63
|
+
var audioSamplingRate = 44100
|
|
64
|
+
var audioBitrate = 64000
|
|
65
|
+
|
|
66
|
+
// Video parameters
|
|
67
|
+
var videoCodecId = NodePublisher.NMC_CODEC_ID_H264
|
|
68
|
+
var videoProfile = NodePublisher.NMC_PROFILE_AUTO
|
|
69
|
+
var videoWidth = 720
|
|
70
|
+
var videoHeight = 1280
|
|
71
|
+
var videoFps = 30
|
|
72
|
+
var videoBitrate = 2000000
|
|
73
|
+
|
|
74
|
+
var frontCamera: Boolean? = null
|
|
75
|
+
set(value) {
|
|
76
|
+
field = value
|
|
77
|
+
value?.let {
|
|
78
|
+
np?.closeCamera()
|
|
79
|
+
np?.openCamera(if (it) NodePublisher.NMC_CAMERA_FRONT else NodePublisher.NMC_CAMERA_BACK)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Other parameters
|
|
84
|
+
var keyFrameInterval = 2
|
|
85
|
+
var videoOrientation = 1
|
|
86
|
+
|
|
87
|
+
var cameraFrontMirror: Boolean? = null
|
|
88
|
+
set(value) {
|
|
89
|
+
field = value
|
|
90
|
+
value?.let { np?.setCameraFrontMirror(it) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private val videoView = FrameLayout(context).apply {
|
|
94
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
init {
|
|
98
|
+
addView(videoView)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun onAttachedToWindow() {
|
|
102
|
+
super.onAttachedToWindow()
|
|
103
|
+
np = NodePublisher(context, LICENSE_KEY)
|
|
104
|
+
np?.setOnNodePublisherEventListener { obj, event, msg ->
|
|
105
|
+
onEventCallback(mapOf("event" to event, "msg" to msg))
|
|
106
|
+
}
|
|
107
|
+
// Apply audio and video params
|
|
108
|
+
applyAudioParams()
|
|
109
|
+
applyVideoParams()
|
|
110
|
+
|
|
111
|
+
// Apply crypto key and HWAccelEnable
|
|
112
|
+
np?.setCryptoKey(this.cryptoKey)
|
|
113
|
+
np?.setHWAccelEnable(this.HWAccelEnable)
|
|
114
|
+
np?.setDenoiseEnable(this.denoiseEnable)
|
|
115
|
+
|
|
116
|
+
// Apply color and effect params if set
|
|
117
|
+
colorStyleId?.let { np?.setEffectStyle(it) }
|
|
118
|
+
colorStyleIntensity?.let { np?.setEffectParameter("style", it) }
|
|
119
|
+
smoothskinIntensity?.let { np?.setEffectParameter("smoothskin", it) }
|
|
120
|
+
|
|
121
|
+
// Apply volume if set
|
|
122
|
+
volume?.let { np?.setVolume(it) }
|
|
123
|
+
|
|
124
|
+
// opencamera and attachview
|
|
125
|
+
np?.openCamera(if (frontCamera == true) 0 else 1)
|
|
126
|
+
np?.attachView(videoView)
|
|
127
|
+
|
|
128
|
+
np?.setZoomRatio(zoomRatio ?: 1f)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
override fun onDetachedFromWindow() {
|
|
132
|
+
np?.stop()
|
|
133
|
+
np?.closeCamera()
|
|
134
|
+
np?.detachView()
|
|
135
|
+
np = null
|
|
136
|
+
super.onDetachedFromWindow()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun applyAudioParams() {
|
|
140
|
+
np?.setAudioCodecParam(
|
|
141
|
+
audioCodecId, audioProfile,
|
|
142
|
+
audioSamplingRate, audioChannels, audioBitrate
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun applyVideoParams() {
|
|
147
|
+
np?.setVideoCodecParam(
|
|
148
|
+
videoCodecId,
|
|
149
|
+
videoProfile, videoWidth, videoHeight, videoFps, videoBitrate
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fun start(url: String?) {
|
|
154
|
+
if (!url.isNullOrEmpty()) {
|
|
155
|
+
this.url = url
|
|
156
|
+
}
|
|
157
|
+
np?.start(this.url)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fun stop() {
|
|
161
|
+
np?.stop()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fun setEffectParameter(key: String, value: Float) {
|
|
165
|
+
np?.setEffectParameter(key, value)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fun setEffectStyle(style: Int) {
|
|
169
|
+
np?.setEffectStyle(style)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun startFocusAndMeteringCenter() {
|
|
173
|
+
np?.startFocusAndMeteringCenter()
|
|
174
|
+
}
|
|
175
|
+
}
|