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 CHANGED
@@ -1,39 +1,33 @@
1
1
  # react-native-tpstreams
2
+ [![npm version](https://img.shields.io/npm/v/react-native-tpstreams.svg?style=flat-square)](https://www.npmjs.com/package/react-native-tpstreams)
3
+ [![npm downloads](https://img.shields.io/npm/dm/react-native-tpstreams.svg?style=flat-square)](https://www.npmjs.com/package/react-native-tpstreams)
4
+ [![license](https://img.shields.io/npm/l/react-native-tpstreams.svg?style=flat-square)](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
- ```sh
11
+ ```bash
10
12
  npm install react-native-tpstreams
11
13
  ```
12
14
 
13
- ---
15
+ ## Quick Start
14
16
 
15
- ## Getting Started
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 with your organization ID
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 the Player Component
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
- - `enableDownload`: (Optional) Whether to enable download functionality for the video. When set to true, the player will show a download button. Default is false.
40
+ ### Player Props
95
41
 
96
- - `offlineLicenseExpireTime`: (Optional) The expiration time for offline licenses in seconds. If not provided, defaults to 15 days (1,296,000 seconds).
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
- - `downloadMetadata`: (Optional) Custom metadata to attach to downloads. Accepts an object with string key-value pairs. This metadata is stored with the download and can be retrieved later. Default is undefined.
53
+ ### Player Methods
99
54
 
100
- ---
101
-
102
- ## Downloads
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
- #### Progress Listener Types
59
+ const playerRef = useRef<TPStreamsPlayerRef>(null);
135
60
 
136
- ```typescript
137
- // Progress update event (uses existing DownloadItem interface)
138
- type DownloadProgressChange = DownloadItem;
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
- // Listener function type
141
- type DownloadProgressListener = (downloads: DownloadProgressChange[]) => void;
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
- ### Download Item
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
- import { useRef } from 'react';
163
- import { View, Button } from 'react-native';
164
- import { TPStreamsPlayerView } from 'react-native-tpstreams';
165
- import type { TPStreamsPlayerRef } from 'react-native-tpstreams';
166
-
167
- function TPStreamsPlayerExample() {
168
- const playerRef = useRef<TPStreamsPlayerRef>(null);
169
-
170
- const handlePlay = () => {
171
- playerRef.current?.play();
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
- ## Download Example
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
- type DownloadItem,
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 checkStatus = async (videoId: string) => {
259
- try {
260
- const status = await getDownloadStatus(videoId);
261
- console.log(`Status: ${status}`);
262
- return status;
263
- } catch (error) {
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
- // Resume a download
305
- const resumeVideoDownload = async (videoId: string) => {
306
- try {
307
- await resumeDownload(videoId);
308
- console.log('Download resumed');
309
-
310
- // Check status after resuming
311
- const status = await getDownloadStatus(videoId);
312
- console.log(`New status: ${status}`);
313
- } catch (error) {
314
- console.error('Error resuming download:', error);
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
- // Remove a download
319
- const removeVideoDownload = async (videoId: string) => {
320
- try {
321
- await removeDownload(videoId);
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
- ## Real-time Download Progress Example
139
+ ### Real-time Progress
330
140
 
331
- Here's a complete example showing how to implement real-time download progress in a React Native component:
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
- const DownloadProgressExample = () => {
355
- const [downloads, setDownloads] = useState<DownloadItem[]>([]);
356
- const [isInitializing, setIsInitializing] = useState(true);
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
- const styles = StyleSheet.create({
510
- container: {
511
- flex: 1,
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
- export default DownloadProgressExample;
158
+ // Cleanup
159
+ async function cleanup() {
160
+ subscription.remove();
161
+ await removeDownloadProgressListener();
162
+ }
594
163
  ```
595
164
 
596
- ### Key Features of the Real-time Progress System:
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
- See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tpstreams",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Video component for TPStreams",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",