react-native-tpstreams 1.0.5 → 1.0.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/README.md +94 -542
- package/ios/TPStreamsRNPlayerView.swift +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
# react-native-tpstreams
|
|
2
|
+
[](https://www.npmjs.com/package/react-native-tpstreams)
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-tpstreams)
|
|
4
|
+
[](LICENSE)
|
|
2
5
|
|
|
3
|
-
Video player component for TPStreams
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
A React Native video player component for TPStreams with offline download support.
|
|
6
8
|
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
9
|
-
```
|
|
11
|
+
```bash
|
|
10
12
|
npm install react-native-tpstreams
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## Quick Start
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
### Initialize TPStreams
|
|
18
|
-
|
|
19
|
-
First, initialize TPStreams with your organization ID. This should be done **only once** at your app's entry point (e.g., App.js or index.js):
|
|
17
|
+
### 1. Initialize TPStreams
|
|
20
18
|
|
|
21
19
|
```js
|
|
22
20
|
import { TPStreams } from "react-native-tpstreams";
|
|
23
21
|
|
|
24
|
-
// Initialize
|
|
25
|
-
// Do this only once at your app's entry point
|
|
22
|
+
// Initialize once at your app's entry point
|
|
26
23
|
TPStreams.initialize('YOUR_ORGANIZATION_ID');
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
### Add
|
|
30
|
-
|
|
31
|
-
Then add the player component to your app:
|
|
26
|
+
### 2. Add Player Component
|
|
32
27
|
|
|
33
28
|
```js
|
|
34
29
|
import { TPStreamsPlayerView } from "react-native-tpstreams";
|
|
35
30
|
|
|
36
|
-
// Use the player component where needed
|
|
37
31
|
<TPStreamsPlayerView
|
|
38
32
|
videoId="YOUR_VIDEO_ID"
|
|
39
33
|
accessToken="YOUR_ACCESS_TOKEN"
|
|
@@ -41,195 +35,64 @@ import { TPStreamsPlayerView } from "react-native-tpstreams";
|
|
|
41
35
|
/>
|
|
42
36
|
```
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## Player Methods
|
|
47
|
-
|
|
48
|
-
- `play()`: Starts video playback.
|
|
49
|
-
|
|
50
|
-
- `pause()`: Pauses video playback.
|
|
51
|
-
|
|
52
|
-
- `seekTo(positionMs: number)`: Seeks to position in milliseconds.
|
|
53
|
-
|
|
54
|
-
- `setPlaybackSpeed(speed: number)`: Sets playback speed (e.g., 0.5, 1.0, 2.0).
|
|
55
|
-
|
|
56
|
-
- `getCurrentPosition()`: Gets current position in milliseconds. Returns `Promise<number>`.
|
|
57
|
-
|
|
58
|
-
- `getDuration()`: Gets video duration in milliseconds. Returns `Promise<number>`.
|
|
59
|
-
|
|
60
|
-
- `isPlaying()`: Checks if video is currently playing. Returns `Promise<boolean>`.
|
|
61
|
-
|
|
62
|
-
- `getPlaybackSpeed()`: Gets current playback speed. Returns `Promise<number>`.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Player Events
|
|
67
|
-
|
|
68
|
-
- `onPlayerStateChanged(state: number)`: Fires when player state changes.
|
|
69
|
-
|
|
70
|
-
- `onIsPlayingChanged(isPlaying: boolean)`: Fires when playing state changes.
|
|
71
|
-
|
|
72
|
-
- `onPlaybackSpeedChanged(speed: number)`: Fires when playback speed changes.
|
|
73
|
-
|
|
74
|
-
- `onIsLoadingChanged(isLoading: boolean)`: Fires when loading state changes.
|
|
75
|
-
|
|
76
|
-
- `onError(error: {message: string, code: number, details?: string})`: Fires when an error occurs.
|
|
77
|
-
|
|
78
|
-
- `onAccessTokenExpired(videoId: string, callback: (newToken: string) => void)`: Fires when the access token expires. Call the callback with a new token to continue playback.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## Player Props
|
|
83
|
-
|
|
84
|
-
- `videoId`: (Required) The ID of the video to play.
|
|
85
|
-
|
|
86
|
-
- `accessToken`: (Required) Access token for the video.
|
|
87
|
-
|
|
88
|
-
- `startAt`: (Optional) Position in seconds where playback should start. Default is 0.
|
|
89
|
-
|
|
90
|
-
- `shouldAutoPlay`: (Optional) Whether the video should start playing automatically. Default is true.
|
|
91
|
-
|
|
92
|
-
- `showDefaultCaptions`: (Optional) Whether to show default captions if available. Default is false.
|
|
38
|
+
## API Reference
|
|
93
39
|
|
|
94
|
-
|
|
40
|
+
### Player Props
|
|
95
41
|
|
|
96
|
-
|
|
42
|
+
| Prop | Type | Required | Description |
|
|
43
|
+
|------|------|----------|-------------|
|
|
44
|
+
| `videoId` | `string` | Yes | Video ID to play |
|
|
45
|
+
| `accessToken` | `string` | Yes | Access token for the video |
|
|
46
|
+
| `startAt` | `number` | No | Start position in seconds (default: 0) |
|
|
47
|
+
| `shouldAutoPlay` | `boolean` | No | Auto-play on load (default: true) |
|
|
48
|
+
| `showDefaultCaptions` | `boolean` | No | Show captions if available (default: false) |
|
|
49
|
+
| `enableDownload` | `boolean` | No | Enable download functionality (default: false) |
|
|
50
|
+
| `offlineLicenseExpireTime` | `number` | No | License expiration in seconds (default: 15 days) |
|
|
51
|
+
| `downloadMetadata` | `object` | No | Custom metadata for downloads |
|
|
97
52
|
|
|
98
|
-
|
|
53
|
+
### Player Methods
|
|
99
54
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
### Download Methods
|
|
105
|
-
|
|
106
|
-
- `pauseDownload(videoId: string)`: Pauses an ongoing download. Returns `Promise<void>`.
|
|
107
|
-
|
|
108
|
-
- `resumeDownload(videoId: string)`: Resumes a paused download. Returns `Promise<void>`.
|
|
109
|
-
|
|
110
|
-
- `removeDownload(videoId: string)`: Removes a downloaded video. Returns `Promise<void>`.
|
|
111
|
-
|
|
112
|
-
- `isDownloaded(videoId: string)`: Checks if a video has been downloaded. Returns `Promise<boolean>`.
|
|
113
|
-
|
|
114
|
-
- `isDownloading(videoId: string)`: Checks if a video is currently downloading. Returns `Promise<boolean>`.
|
|
115
|
-
|
|
116
|
-
- `isPaused(videoId: string)`: Checks if a video download is paused. Returns `Promise<boolean>`.
|
|
117
|
-
|
|
118
|
-
- `getDownloadStatus(videoId: string)`: Gets the download status of a video as a descriptive string. Returns `Promise<string>`.
|
|
119
|
-
|
|
120
|
-
- `getAllDownloads()`: Gets all downloaded videos. Returns `Promise<DownloadItem[]>`.
|
|
121
|
-
|
|
122
|
-
### Real-time Download Progress
|
|
123
|
-
|
|
124
|
-
The library provides real-time download progress updates for optimal performance:
|
|
125
|
-
|
|
126
|
-
#### Progress Listener Methods
|
|
127
|
-
|
|
128
|
-
- `addDownloadProgressListener()`: Starts listening for download progress updates. Returns `Promise<void>`.
|
|
129
|
-
|
|
130
|
-
- `removeDownloadProgressListener()`: Stops listening for download progress updates. Returns `Promise<void>`.
|
|
131
|
-
|
|
132
|
-
- `onDownloadProgressChanged(listener: DownloadProgressListener)`: Adds a listener for progress changes. Returns `EmitterSubscription`.
|
|
55
|
+
```js
|
|
56
|
+
import { useRef } from 'react';
|
|
57
|
+
import type { TPStreamsPlayerRef } from 'react-native-tpstreams';
|
|
133
58
|
|
|
134
|
-
|
|
59
|
+
const playerRef = useRef<TPStreamsPlayerRef>(null);
|
|
135
60
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
61
|
+
// Control playback
|
|
62
|
+
playerRef.current?.play();
|
|
63
|
+
playerRef.current?.pause();
|
|
64
|
+
playerRef.current?.seekTo(30000); // 30 seconds
|
|
65
|
+
playerRef.current?.setPlaybackSpeed(2.0);
|
|
139
66
|
|
|
140
|
-
//
|
|
141
|
-
|
|
67
|
+
// Get player state (inside an async function)
|
|
68
|
+
const getPlayerState = async () => {
|
|
69
|
+
const position = await playerRef.current?.getCurrentPosition();
|
|
70
|
+
const duration = await playerRef.current?.getDuration();
|
|
71
|
+
const isPlaying = await playerRef.current?.isPlaying();
|
|
72
|
+
const speed = await playerRef.current?.getPlaybackSpeed();
|
|
73
|
+
|
|
74
|
+
return { position, duration, isPlaying, speed };
|
|
75
|
+
};
|
|
142
76
|
```
|
|
143
77
|
|
|
144
|
-
###
|
|
145
|
-
|
|
146
|
-
The download item object (`DownloadItem`) contains information about a downloaded or downloading video, including:
|
|
147
|
-
|
|
148
|
-
- `videoId`: The ID of the video.
|
|
149
|
-
- `title`: The title of the video.
|
|
150
|
-
- `thumbnailUrl`: URL to the video thumbnail (if available).
|
|
151
|
-
- `totalBytes`: Total size of the video in bytes.
|
|
152
|
-
- `downloadedBytes`: Number of bytes downloaded so far.
|
|
153
|
-
- `progressPercentage`: Download progress from 0 to 100.
|
|
154
|
-
- `state`: The current state of the download as String (Queued, Downloading, Completed, Failed, Removing, Restarting, Paused).
|
|
155
|
-
- `metadata`: Custom metadata attached to the download as a JSON string (if provided during download).
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## Example
|
|
78
|
+
### Player Events
|
|
160
79
|
|
|
161
80
|
```js
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const handlePause = () => {
|
|
175
|
-
playerRef.current?.pause();
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const handleSeek = () => {
|
|
179
|
-
playerRef.current?.seekTo(30000); // 30 seconds
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const checkPosition = async () => {
|
|
183
|
-
try {
|
|
184
|
-
const position = await playerRef.current?.getCurrentPosition();
|
|
185
|
-
console.log(`Current position: ${position}ms`);
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error('Error getting position:', error);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
return (
|
|
192
|
-
<View>
|
|
193
|
-
<TPStreamsPlayerView
|
|
194
|
-
ref={playerRef}
|
|
195
|
-
videoId="YOUR_VIDEO_ID"
|
|
196
|
-
accessToken="YOUR_ACCESS_TOKEN"
|
|
197
|
-
style={{ height: 250 }}
|
|
198
|
-
startAt={100}
|
|
199
|
-
shouldAutoPlay={false}
|
|
200
|
-
showDefaultCaptions={true}
|
|
201
|
-
enableDownload={true}
|
|
202
|
-
offlineLicenseExpireTime={2 * 24 * 60 * 60} // 2 days in seconds
|
|
203
|
-
downloadMetadata={{
|
|
204
|
-
category: 'educational',
|
|
205
|
-
subject: 'mathematics',
|
|
206
|
-
level: 'intermediate'
|
|
207
|
-
}}
|
|
208
|
-
onPlayerStateChanged={(state) => console.log(`Player state: ${state}`)}
|
|
209
|
-
onIsPlayingChanged={(isPlaying) => console.log(`Is playing: ${isPlaying}`)}
|
|
210
|
-
onPlaybackSpeedChanged={(speed) => console.log(`Speed changed: ${speed}x`)}
|
|
211
|
-
onIsLoadingChanged={(isLoading) => console.log(`Loading: ${isLoading}`)}
|
|
212
|
-
onError={(error) => console.error('Player error:', error)}
|
|
213
|
-
onAccessTokenExpired={async (videoId, callback) => {
|
|
214
|
-
// Fetch a new token from your server
|
|
215
|
-
const newToken = await getNewTokenForVideo(videoId);
|
|
216
|
-
callback(newToken);
|
|
217
|
-
}}
|
|
218
|
-
/>
|
|
219
|
-
|
|
220
|
-
<Button title="Play" onPress={handlePlay} />
|
|
221
|
-
<Button title="Pause" onPress={handlePause} />
|
|
222
|
-
<Button title="Seek to 30s" onPress={handleSeek} />
|
|
223
|
-
<Button title="2x Speed" onPress={() => playerRef.current?.setPlaybackSpeed(2.0)} />
|
|
224
|
-
<Button title="Get Position" onPress={checkPosition} />
|
|
225
|
-
</View>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
81
|
+
<TPStreamsPlayerView
|
|
82
|
+
onPlayerStateChanged={(state) => console.log('State:', state)}
|
|
83
|
+
onIsPlayingChanged={(isPlaying) => console.log('Playing:', isPlaying)}
|
|
84
|
+
onPlaybackSpeedChanged={(speed) => console.log('Speed:', speed)}
|
|
85
|
+
onIsLoadingChanged={(isLoading) => console.log('Loading:', isLoading)}
|
|
86
|
+
onError={(error) => console.error('Error:', error)}
|
|
87
|
+
onAccessTokenExpired={(videoId, callback) => {
|
|
88
|
+
// Fetch new token and call callback(newToken)
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
228
91
|
```
|
|
229
92
|
|
|
230
|
-
|
|
93
|
+
## Downloads
|
|
231
94
|
|
|
232
|
-
|
|
95
|
+
### Download Management
|
|
233
96
|
|
|
234
97
|
```js
|
|
235
98
|
import {
|
|
@@ -237,384 +100,73 @@ import {
|
|
|
237
100
|
resumeDownload,
|
|
238
101
|
removeDownload,
|
|
239
102
|
getAllDownloads,
|
|
240
|
-
getDownloadStatus,
|
|
241
103
|
isDownloaded,
|
|
242
104
|
isDownloading,
|
|
243
|
-
|
|
105
|
+
getDownloadStatus,
|
|
244
106
|
} from 'react-native-tpstreams';
|
|
245
107
|
|
|
246
|
-
// Get all downloads
|
|
247
|
-
const loadDownloads = async () => {
|
|
248
|
-
try {
|
|
249
|
-
const items: DownloadItem[] = await getAllDownloads();
|
|
250
|
-
console.log(`Found ${items.length} downloads`);
|
|
251
|
-
return items;
|
|
252
|
-
} catch (error) {
|
|
253
|
-
console.error('Failed to load downloads:', error);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
108
|
// Check download status
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
console.error('Error checking status:', error);
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// Check if video is downloaded
|
|
269
|
-
const checkIfDownloaded = async (videoId: string) => {
|
|
270
|
-
try {
|
|
271
|
-
const downloaded: boolean = await isDownloaded(videoId);
|
|
272
|
-
console.log(`Is downloaded: ${downloaded}`);
|
|
273
|
-
return downloaded;
|
|
274
|
-
} catch (error) {
|
|
275
|
-
console.error('Error checking if downloaded:', error);
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Check if video is currently downloading
|
|
280
|
-
const checkIfDownloading = async (videoId: string) => {
|
|
281
|
-
try {
|
|
282
|
-
const downloading: boolean = await isDownloading(videoId);
|
|
283
|
-
console.log(`Is downloading: ${downloading}`);
|
|
284
|
-
return downloading;
|
|
285
|
-
} catch (error) {
|
|
286
|
-
console.error('Error checking if downloading:', error);
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
// Pause a download
|
|
291
|
-
const pauseVideoDownload = async (videoId: string) => {
|
|
292
|
-
try {
|
|
293
|
-
await pauseDownload(videoId);
|
|
294
|
-
console.log('Download paused successfully');
|
|
295
|
-
|
|
296
|
-
// Check status after pausing
|
|
297
|
-
const status = await getDownloadStatus(videoId);
|
|
298
|
-
console.log(`New status: ${status}`);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
console.error('Error pausing download:', error);
|
|
301
|
-
}
|
|
109
|
+
const checkDownloadStatus = async (videoId) => {
|
|
110
|
+
const downloaded = await isDownloaded(videoId);
|
|
111
|
+
const downloading = await isDownloading(videoId);
|
|
112
|
+
const status = await getDownloadStatus(videoId);
|
|
113
|
+
|
|
114
|
+
return { downloaded, downloading, status };
|
|
302
115
|
};
|
|
303
116
|
|
|
304
|
-
//
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
117
|
+
// Manage downloads
|
|
118
|
+
const manageDownload = async (videoId, action) => {
|
|
119
|
+
switch (action) {
|
|
120
|
+
case 'pause':
|
|
121
|
+
await pauseDownload(videoId);
|
|
122
|
+
break;
|
|
123
|
+
case 'resume':
|
|
124
|
+
await resumeDownload(videoId);
|
|
125
|
+
break;
|
|
126
|
+
case 'remove':
|
|
127
|
+
await removeDownload(videoId);
|
|
128
|
+
break;
|
|
315
129
|
}
|
|
316
130
|
};
|
|
317
131
|
|
|
318
|
-
//
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
console.log('Download removed');
|
|
323
|
-
} catch (error) {
|
|
324
|
-
console.error('Error removing download:', error);
|
|
325
|
-
}
|
|
132
|
+
// Get all downloads
|
|
133
|
+
const getAllDownloadedVideos = async () => {
|
|
134
|
+
const downloads = await getAllDownloads();
|
|
135
|
+
return downloads;
|
|
326
136
|
};
|
|
327
137
|
```
|
|
328
138
|
|
|
329
|
-
|
|
139
|
+
### Real-time Progress
|
|
330
140
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
```jsx
|
|
334
|
-
import React, { useState, useEffect } from 'react';
|
|
335
|
-
import {
|
|
336
|
-
View,
|
|
337
|
-
Text,
|
|
338
|
-
StyleSheet,
|
|
339
|
-
TouchableOpacity,
|
|
340
|
-
ScrollView,
|
|
341
|
-
Alert,
|
|
342
|
-
} from 'react-native';
|
|
141
|
+
```js
|
|
343
142
|
import {
|
|
344
143
|
addDownloadProgressListener,
|
|
345
144
|
removeDownloadProgressListener,
|
|
346
145
|
onDownloadProgressChanged,
|
|
347
|
-
pauseDownload,
|
|
348
|
-
resumeDownload,
|
|
349
|
-
removeDownload,
|
|
350
|
-
type DownloadItem,
|
|
351
|
-
type DownloadProgressChange,
|
|
352
146
|
} from 'react-native-tpstreams';
|
|
353
147
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
useEffect(() => {
|
|
359
|
-
let subscription: any = null;
|
|
360
|
-
|
|
361
|
-
// Setup progress listener when component mounts
|
|
362
|
-
const setupProgressListener = async () => {
|
|
363
|
-
try {
|
|
364
|
-
// Start listening for progress updates
|
|
365
|
-
await addDownloadProgressListener();
|
|
366
|
-
|
|
367
|
-
// Add listener for progress updates
|
|
368
|
-
subscription = onDownloadProgressChanged((downloads: DownloadProgressChange[]) => {
|
|
369
|
-
console.log('Progress changes received:', downloads.length, 'downloads');
|
|
370
|
-
|
|
371
|
-
// Simply replace the state with the complete list from native
|
|
372
|
-
setDownloads(downloads);
|
|
373
|
-
});
|
|
374
|
-
} catch (error) {
|
|
375
|
-
console.error('Failed to setup progress listener:', error);
|
|
376
|
-
setIsInitializing(false);
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
setupProgressListener();
|
|
381
|
-
|
|
382
|
-
// Cleanup function - moved outside async function
|
|
383
|
-
return () => {
|
|
384
|
-
if (subscription) {
|
|
385
|
-
subscription.remove(); // Remove the listener
|
|
386
|
-
}
|
|
387
|
-
removeDownloadProgressListener(); // Stop listening
|
|
388
|
-
};
|
|
389
|
-
}, []);
|
|
390
|
-
|
|
391
|
-
const handlePauseDownload = async (videoId: string) => {
|
|
392
|
-
try {
|
|
393
|
-
await pauseDownload(videoId);
|
|
394
|
-
console.log('Download paused successfully');
|
|
395
|
-
} catch (error) {
|
|
396
|
-
console.error('Error pausing download:', error);
|
|
397
|
-
Alert.alert('Error', 'Failed to pause download');
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const handleResumeDownload = async (videoId: string) => {
|
|
402
|
-
try {
|
|
403
|
-
await resumeDownload(videoId);
|
|
404
|
-
console.log('Download resumed successfully');
|
|
405
|
-
} catch (error) {
|
|
406
|
-
console.error('Error resuming download:', error);
|
|
407
|
-
Alert.alert('Error', 'Failed to resume download');
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const handleRemoveDownload = async (videoId: string) => {
|
|
412
|
-
try {
|
|
413
|
-
await removeDownload(videoId);
|
|
414
|
-
console.log('Download removed successfully');
|
|
415
|
-
} catch (error) {
|
|
416
|
-
console.error('Error removing download:', error);
|
|
417
|
-
Alert.alert('Error', 'Failed to remove download');
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const renderDownloadItem = (item: DownloadItem) => {
|
|
422
|
-
const isCompleted = item.state === 'Completed';
|
|
423
|
-
const isDownloading = item.state === 'Downloading';
|
|
424
|
-
const isPaused = item.state === 'Paused';
|
|
425
|
-
|
|
426
|
-
return (
|
|
427
|
-
<View key={item.videoId} style={styles.downloadItem}>
|
|
428
|
-
<Text style={styles.title}>{item.title}</Text>
|
|
429
|
-
<Text style={styles.status}>Status: {item.state}</Text>
|
|
430
|
-
|
|
431
|
-
{!isCompleted && (
|
|
432
|
-
<View style={styles.progressContainer}>
|
|
433
|
-
<View style={styles.progressBar}>
|
|
434
|
-
<View
|
|
435
|
-
style={[
|
|
436
|
-
styles.progressFill,
|
|
437
|
-
{ width: `${item.progressPercentage}%` }
|
|
438
|
-
]}
|
|
439
|
-
/>
|
|
440
|
-
</View>
|
|
441
|
-
<Text style={styles.progressText}>
|
|
442
|
-
{item.progressPercentage.toFixed(1)}%
|
|
443
|
-
</Text>
|
|
444
|
-
</View>
|
|
445
|
-
)}
|
|
446
|
-
|
|
447
|
-
{item.totalBytes > 0 && (
|
|
448
|
-
<Text style={styles.bytesText}>
|
|
449
|
-
{(item.downloadedBytes / (1024 * 1024)).toFixed(1)} MB /
|
|
450
|
-
{(item.totalBytes / (1024 * 1024)).toFixed(1)} MB
|
|
451
|
-
</Text>
|
|
452
|
-
)}
|
|
453
|
-
|
|
454
|
-
<View style={styles.buttonContainer}>
|
|
455
|
-
{!isCompleted && (
|
|
456
|
-
<>
|
|
457
|
-
{isDownloading && (
|
|
458
|
-
<TouchableOpacity
|
|
459
|
-
style={styles.button}
|
|
460
|
-
onPress={() => handlePauseDownload(item.videoId)}
|
|
461
|
-
>
|
|
462
|
-
<Text style={styles.buttonText}>Pause</Text>
|
|
463
|
-
</TouchableOpacity>
|
|
464
|
-
)}
|
|
465
|
-
|
|
466
|
-
{isPaused && (
|
|
467
|
-
<TouchableOpacity
|
|
468
|
-
style={styles.button}
|
|
469
|
-
onPress={() => handleResumeDownload(item.videoId)}
|
|
470
|
-
>
|
|
471
|
-
<Text style={styles.buttonText}>Resume</Text>
|
|
472
|
-
</TouchableOpacity>
|
|
473
|
-
)}
|
|
474
|
-
</>
|
|
475
|
-
)}
|
|
476
|
-
|
|
477
|
-
<TouchableOpacity
|
|
478
|
-
style={[styles.button, styles.removeButton]}
|
|
479
|
-
onPress={() => handleRemoveDownload(item.videoId)}
|
|
480
|
-
>
|
|
481
|
-
<Text style={styles.buttonText}>Remove</Text>
|
|
482
|
-
</TouchableOpacity>
|
|
483
|
-
</View>
|
|
484
|
-
</View>
|
|
485
|
-
);
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
if (isInitializing) {
|
|
489
|
-
return (
|
|
490
|
-
<View style={styles.container}>
|
|
491
|
-
<Text>Loading downloads...</Text>
|
|
492
|
-
</View>
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return (
|
|
497
|
-
<ScrollView style={styles.container}>
|
|
498
|
-
<Text style={styles.header}>Downloads ({downloads.length})</Text>
|
|
499
|
-
|
|
500
|
-
{downloads.length > 0 ? (
|
|
501
|
-
downloads.map(renderDownloadItem)
|
|
502
|
-
) : (
|
|
503
|
-
<Text style={styles.emptyText}>No downloads available</Text>
|
|
504
|
-
)}
|
|
505
|
-
</ScrollView>
|
|
506
|
-
);
|
|
507
|
-
};
|
|
148
|
+
// Start listening
|
|
149
|
+
async function startListening() {
|
|
150
|
+
await addDownloadProgressListener();
|
|
151
|
+
}
|
|
508
152
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
padding: 16,
|
|
513
|
-
backgroundColor: '#f5f5f5',
|
|
514
|
-
},
|
|
515
|
-
header: {
|
|
516
|
-
fontSize: 20,
|
|
517
|
-
fontWeight: 'bold',
|
|
518
|
-
marginBottom: 16,
|
|
519
|
-
},
|
|
520
|
-
downloadItem: {
|
|
521
|
-
backgroundColor: '#fff',
|
|
522
|
-
padding: 16,
|
|
523
|
-
marginBottom: 12,
|
|
524
|
-
borderRadius: 8,
|
|
525
|
-
shadowColor: '#000',
|
|
526
|
-
shadowOffset: { width: 0, height: 2 },
|
|
527
|
-
shadowOpacity: 0.1,
|
|
528
|
-
shadowRadius: 4,
|
|
529
|
-
elevation: 3,
|
|
530
|
-
},
|
|
531
|
-
title: {
|
|
532
|
-
fontSize: 16,
|
|
533
|
-
fontWeight: 'bold',
|
|
534
|
-
marginBottom: 8,
|
|
535
|
-
},
|
|
536
|
-
status: {
|
|
537
|
-
fontSize: 14,
|
|
538
|
-
color: '#666',
|
|
539
|
-
marginBottom: 8,
|
|
540
|
-
},
|
|
541
|
-
progressContainer: {
|
|
542
|
-
flexDirection: 'row',
|
|
543
|
-
alignItems: 'center',
|
|
544
|
-
marginBottom: 8,
|
|
545
|
-
},
|
|
546
|
-
progressBar: {
|
|
547
|
-
flex: 1,
|
|
548
|
-
height: 8,
|
|
549
|
-
backgroundColor: '#eee',
|
|
550
|
-
borderRadius: 4,
|
|
551
|
-
marginRight: 12,
|
|
552
|
-
},
|
|
553
|
-
progressFill: {
|
|
554
|
-
height: '100%',
|
|
555
|
-
backgroundColor: '#007AFF',
|
|
556
|
-
borderRadius: 4,
|
|
557
|
-
},
|
|
558
|
-
progressText: {
|
|
559
|
-
fontSize: 12,
|
|
560
|
-
color: '#666',
|
|
561
|
-
width: 40,
|
|
562
|
-
},
|
|
563
|
-
bytesText: {
|
|
564
|
-
fontSize: 12,
|
|
565
|
-
color: '#666',
|
|
566
|
-
marginBottom: 12,
|
|
567
|
-
},
|
|
568
|
-
buttonContainer: {
|
|
569
|
-
flexDirection: 'row',
|
|
570
|
-
gap: 8,
|
|
571
|
-
},
|
|
572
|
-
button: {
|
|
573
|
-
paddingVertical: 8,
|
|
574
|
-
paddingHorizontal: 16,
|
|
575
|
-
backgroundColor: '#007AFF',
|
|
576
|
-
borderRadius: 6,
|
|
577
|
-
},
|
|
578
|
-
removeButton: {
|
|
579
|
-
backgroundColor: '#FF3B30',
|
|
580
|
-
},
|
|
581
|
-
buttonText: {
|
|
582
|
-
color: '#fff',
|
|
583
|
-
fontSize: 14,
|
|
584
|
-
fontWeight: '600',
|
|
585
|
-
},
|
|
586
|
-
emptyText: {
|
|
587
|
-
textAlign: 'center',
|
|
588
|
-
color: '#666',
|
|
589
|
-
fontSize: 16,
|
|
590
|
-
},
|
|
153
|
+
// Listen for updates
|
|
154
|
+
const subscription = onDownloadProgressChanged((downloads) => {
|
|
155
|
+
console.log('Download progress:', downloads);
|
|
591
156
|
});
|
|
592
157
|
|
|
593
|
-
|
|
158
|
+
// Cleanup
|
|
159
|
+
async function cleanup() {
|
|
160
|
+
subscription.remove();
|
|
161
|
+
await removeDownloadProgressListener();
|
|
162
|
+
}
|
|
594
163
|
```
|
|
595
164
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
1. **Real-time Updates**: Progress bars and status update in real-time
|
|
599
|
-
2. **Automatic UI Updates**: UI automatically reflects current download states
|
|
600
|
-
3. **Efficient State Management**: Uses functional state updates to avoid race conditions
|
|
601
|
-
4. **Proper Cleanup**: Removes listeners when component unmounts
|
|
602
|
-
5. **Error Handling**: Graceful error handling with user feedback
|
|
603
|
-
6. **Type Safety**: Full TypeScript support with proper types
|
|
604
|
-
|
|
605
|
-
### Best Practices:
|
|
606
|
-
|
|
607
|
-
1. **Start listening when needed**: Only start the progress listener when your screen is active
|
|
608
|
-
2. **Stop listening when not needed**: Always stop listening to save resources
|
|
609
|
-
3. **Use functional state updates**: Prevents race conditions with concurrent updates
|
|
610
|
-
4. **Debounce if needed**: Consider debouncing updates for better UI performance
|
|
611
|
-
|
|
612
|
-
---
|
|
613
|
-
|
|
614
|
-
## Contributing
|
|
165
|
+
## Resources
|
|
615
166
|
|
|
616
|
-
|
|
167
|
+
- **Sample App**: [Complete working example](https://github.com/testpress/sample_RN_App)
|
|
168
|
+
- **Documentation**: [TPStreams Developer Docs](https://developer.tpstreams.com/docs/mobile-sdk/react-native-sdk)
|
|
617
169
|
|
|
618
170
|
## License
|
|
619
171
|
|
|
620
|
-
MIT
|
|
172
|
+
MIT
|
|
@@ -7,9 +7,19 @@ import AVFoundation
|
|
|
7
7
|
@objc(TPStreamsRNPlayerView)
|
|
8
8
|
class TPStreamsRNPlayerView: UIView {
|
|
9
9
|
|
|
10
|
+
private enum PlaybackState: Int {
|
|
11
|
+
case idle = 1
|
|
12
|
+
case buffering = 2
|
|
13
|
+
case ready = 3
|
|
14
|
+
case ended = 4
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
private var player: TPAVPlayer?
|
|
11
18
|
private var playerViewController: TPStreamPlayerViewController?
|
|
12
19
|
private var playerStatusObserver: NSKeyValueObservation?
|
|
20
|
+
private var playbackSpeedObserver: NSKeyValueObservation?
|
|
21
|
+
private var timeControlStatusObserver: NSKeyValueObservation?
|
|
22
|
+
private var playerStateObserver: NSKeyValueObservation?
|
|
13
23
|
private var setupScheduled = false
|
|
14
24
|
|
|
15
25
|
@objc var videoId: NSString = ""
|
|
@@ -25,6 +35,7 @@ class TPStreamsRNPlayerView: UIView {
|
|
|
25
35
|
@objc var onDuration: RCTDirectEventBlock?
|
|
26
36
|
@objc var onIsPlaying: RCTDirectEventBlock?
|
|
27
37
|
@objc var onPlaybackSpeed: RCTDirectEventBlock?
|
|
38
|
+
|
|
28
39
|
@objc var onPlayerStateChanged: RCTDirectEventBlock?
|
|
29
40
|
@objc var onIsPlayingChanged: RCTDirectEventBlock?
|
|
30
41
|
@objc var onPlaybackSpeedChanged: RCTDirectEventBlock?
|
|
@@ -96,6 +107,12 @@ class TPStreamsRNPlayerView: UIView {
|
|
|
96
107
|
private func removeObservers() {
|
|
97
108
|
playerStatusObserver?.invalidate()
|
|
98
109
|
playerStatusObserver = nil
|
|
110
|
+
playbackSpeedObserver?.invalidate()
|
|
111
|
+
playbackSpeedObserver = nil
|
|
112
|
+
timeControlStatusObserver?.invalidate()
|
|
113
|
+
timeControlStatusObserver = nil
|
|
114
|
+
playerStateObserver?.invalidate()
|
|
115
|
+
playerStateObserver = nil
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
private func createOfflinePlayer() -> TPAVPlayer? {
|
|
@@ -167,6 +184,9 @@ class TPStreamsRNPlayerView: UIView {
|
|
|
167
184
|
|
|
168
185
|
private func observePlayerChanges() {
|
|
169
186
|
setupSeekObserver()
|
|
187
|
+
setupPlaybackSpeedObserver()
|
|
188
|
+
setupPlayingStateObserver()
|
|
189
|
+
setupPlayerStateObserver()
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
private func setupSeekObserver() {
|
|
@@ -180,6 +200,51 @@ class TPStreamsRNPlayerView: UIView {
|
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
|
|
203
|
+
private func setupPlaybackSpeedObserver() {
|
|
204
|
+
guard let player = player else { return }
|
|
205
|
+
|
|
206
|
+
playbackSpeedObserver = player.observe(\.rate, options: [.new, .initial]) { [weak self] player, _ in
|
|
207
|
+
DispatchQueue.main.async {
|
|
208
|
+
self?.onPlaybackSpeedChanged?(["speed": player.rate])
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func setupPlayingStateObserver() {
|
|
214
|
+
guard let player = player else { return }
|
|
215
|
+
|
|
216
|
+
timeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new, .initial]) { [weak self] player, _ in
|
|
217
|
+
DispatchQueue.main.async {
|
|
218
|
+
let isPlaying = player.timeControlStatus == .playing
|
|
219
|
+
self?.onIsPlayingChanged?(["isPlaying": isPlaying])
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private func setupPlayerStateObserver() {
|
|
225
|
+
guard let player = player else { return }
|
|
226
|
+
|
|
227
|
+
playerStateObserver = player.observe(\.status, options: [.new, .initial]) { [weak self] player, _ in
|
|
228
|
+
DispatchQueue.main.async {
|
|
229
|
+
let state = self?.mapPlayerStateToAndroid(player.status) ?? PlaybackState.idle.rawValue
|
|
230
|
+
self?.onPlayerStateChanged?(["playbackState": state])
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func mapPlayerStateToAndroid(_ status: AVPlayer.Status) -> Int {
|
|
236
|
+
switch status {
|
|
237
|
+
case .unknown:
|
|
238
|
+
return PlaybackState.idle.rawValue
|
|
239
|
+
case .readyToPlay:
|
|
240
|
+
return PlaybackState.ready.rawValue
|
|
241
|
+
case .failed:
|
|
242
|
+
return PlaybackState.idle.rawValue
|
|
243
|
+
@unknown default:
|
|
244
|
+
return PlaybackState.idle.rawValue
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
183
248
|
private func setupTokenDelegate() {
|
|
184
249
|
TPStreamsDownloadModule.shared?.setAccessTokenDelegate(self)
|
|
185
250
|
}
|