react-native-media3-player 1.0.0 → 2.1.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/README.md +286 -47
- package/android/build.gradle +16 -3
- package/android/src/main/java/com/rnmedia3playerdemo/Media3PlayerView.kt +170 -7
- package/android/src/main/java/com/rnmedia3playerdemo/Media3PlayerViewManager.kt +25 -3
- package/package.json +1 -1
- package/src/Media3PlayerNativeComponent.ts +19 -2
package/README.md
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# 📽️ Media3Player
|
|
2
2
|
|
|
3
|
-
> A lightweight, high-performance React Native video player powered by Android Media3 (ExoPlayer). Fully customizable
|
|
3
|
+
> A lightweight, high-performance React Native video player powered by Android Media3 (ExoPlayer). Fully customizable with support for autoplay, mute, event handling, and automatic stream type detection (DASH, HLS, MP4). This is an **Android** only package.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
|
-
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Usage](#usage)
|
|
11
|
+
- [Props](#props)
|
|
12
|
+
- [Stream Type Detection](#stream-type-detection)
|
|
13
|
+
- [DRM Support (Widevine)](#drm-support-widevine)
|
|
14
|
+
- [Events](#events)
|
|
15
|
+
- [TypeScript Support](#typescript-support)
|
|
16
|
+
- [Examples](#examples)
|
|
17
|
+
- [Screenshots](#screenshots)
|
|
18
|
+
- [Contributing](#contributing)
|
|
16
19
|
- [License](#license)
|
|
17
20
|
|
|
18
21
|
---
|
|
@@ -20,38 +23,86 @@
|
|
|
20
23
|
## Installation
|
|
21
24
|
|
|
22
25
|
**Install via npm:**
|
|
26
|
+
|
|
23
27
|
```bash
|
|
24
28
|
npm install react-native-media3-player
|
|
25
29
|
```
|
|
26
30
|
|
|
27
31
|
**Or yarn:**
|
|
32
|
+
|
|
28
33
|
```bash
|
|
29
34
|
yarn add react-native-media3-player
|
|
30
35
|
```
|
|
31
36
|
|
|
32
|
-
> Requires **React Native >= 0.70**
|
|
37
|
+
> Requires **React Native >= 0.70**
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
**Note:** To use a specific Media3 version, add or update the following in your **android/build.gradle** (Project-level) file:
|
|
42
|
+
|
|
43
|
+
```groovy
|
|
44
|
+
buildscript {
|
|
45
|
+
ext {
|
|
46
|
+
media3Version = "1.4.1" // Set your desired Media3 version
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Be sure to sync your project after making this change.
|
|
33
52
|
|
|
34
53
|
---
|
|
35
54
|
|
|
36
55
|
## Usage
|
|
37
56
|
|
|
57
|
+
**Basic (non-DRM):**
|
|
58
|
+
|
|
38
59
|
```jsx
|
|
39
60
|
import React from 'react';
|
|
40
|
-
import {
|
|
61
|
+
import {View} from 'react-native';
|
|
41
62
|
import Media3Player from 'react-native-media3-player';
|
|
42
63
|
|
|
43
64
|
export default function App() {
|
|
44
65
|
return (
|
|
45
|
-
<View style={{
|
|
66
|
+
<View style={{flex: 1}}>
|
|
46
67
|
<Media3Player
|
|
47
|
-
style={{
|
|
48
|
-
source={{
|
|
68
|
+
style={{width: '100%', height: 250}}
|
|
69
|
+
source={{uri: 'https://example.com/video.mp4'}}
|
|
49
70
|
autoplay={true}
|
|
50
71
|
play={true}
|
|
51
72
|
mute={false}
|
|
52
73
|
onReady={() => console.log('Player is ready')}
|
|
53
74
|
onEnd={() => console.log('Video ended')}
|
|
54
|
-
onError={
|
|
75
|
+
onError={error => console.log('Player error:', error.message)}
|
|
76
|
+
/>
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**DRM-protected content (Widevine):**
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
import React from 'react';
|
|
86
|
+
import {View} from 'react-native';
|
|
87
|
+
import Media3Player from 'react-native-media3-player';
|
|
88
|
+
|
|
89
|
+
export default function App() {
|
|
90
|
+
return (
|
|
91
|
+
<View style={{flex: 1}}>
|
|
92
|
+
<Media3Player
|
|
93
|
+
style={{width: '100%', height: 250}}
|
|
94
|
+
source={{
|
|
95
|
+
uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd',
|
|
96
|
+
type: 'dash',
|
|
97
|
+
drm: {
|
|
98
|
+
licenseUrl: 'https://cwip-shaka-proxy.appspot.com/no_auth',
|
|
99
|
+
},
|
|
100
|
+
}}
|
|
101
|
+
autoplay={true}
|
|
102
|
+
play={true}
|
|
103
|
+
onReady={() => console.log('Player is ready')}
|
|
104
|
+
onEnd={() => console.log('Video ended')}
|
|
105
|
+
onError={error => console.log('Player error:', error.message)}
|
|
55
106
|
/>
|
|
56
107
|
</View>
|
|
57
108
|
);
|
|
@@ -62,23 +113,133 @@ export default function App() {
|
|
|
62
113
|
|
|
63
114
|
## Props
|
|
64
115
|
|
|
65
|
-
| Prop | Type
|
|
66
|
-
|
|
67
|
-
| `source` | `
|
|
68
|
-
| `autoplay` | `boolean`
|
|
69
|
-
| `play` | `boolean`
|
|
70
|
-
| `mute` | `boolean`
|
|
71
|
-
| `style` | `ViewStyle` or `ViewStyle[]` | `{ width: '100%', height: 250 }` | Styling for the player container.
|
|
116
|
+
| Prop | Type | Default | Description |
|
|
117
|
+
| ---------- | ---------------------------- | -------------------------------- | ------------------------------------------------------------------------ |
|
|
118
|
+
| `source` | `Source` | required | Video source object. Must include a valid `uri`. Supports optional `drm` config. |
|
|
119
|
+
| `autoplay` | `boolean` | `false` | Automatically start playback when the video is ready. |
|
|
120
|
+
| `play` | `boolean` | `false` | Controls whether the player is playing. Overrides autoplay. |
|
|
121
|
+
| `mute` | `boolean` | `false` | Mutes or unmutes the video. |
|
|
122
|
+
| `style` | `ViewStyle` or `ViewStyle[]` | `{ width: '100%', height: 250 }` | Styling for the player container. |
|
|
123
|
+
|
|
124
|
+
### `Source` Object
|
|
125
|
+
|
|
126
|
+
| Property | Type | Required | Description |
|
|
127
|
+
| -------- | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
128
|
+
| `uri` | `string` | Yes | The URI of the media to play (MP4, DASH, HLS, etc.). |
|
|
129
|
+
| `type` | `'dash' \| 'hls' \| 'mp4'` | No | Explicitly set the stream type. Defaults to `'mp4'`. If omitted, the player auto-detects the type from the URI. |
|
|
130
|
+
| `drm` | `DRMConfig` | No | DRM configuration for protected content. |
|
|
131
|
+
|
|
132
|
+
### `DRMConfig` Object
|
|
133
|
+
|
|
134
|
+
| Property | Type | Required | Description |
|
|
135
|
+
| ------------ | --------------------------------- | -------- | ----------------------------------------------------- |
|
|
136
|
+
| `licenseUrl` | `string` | Yes | The Widevine license server URL. |
|
|
137
|
+
| `headers` | `Array<{ key: string, value: string }>` | No | Custom headers to include in the DRM license request. |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Stream Type Detection
|
|
142
|
+
|
|
143
|
+
The player supports **automatic stream type detection** based on the media URI. It inspects the file extension and known manifest patterns (`.mpd` for DASH, `.m3u8` for HLS) to choose the correct media source.
|
|
144
|
+
|
|
145
|
+
You can also **explicitly set the stream type** using the `type` property in the `source` prop. This is useful when the URI does not contain a recognizable extension (e.g., signed URLs or token-based endpoints).
|
|
146
|
+
|
|
147
|
+
### Supported Stream Types
|
|
148
|
+
|
|
149
|
+
| `type` Value | Format | Description |
|
|
150
|
+
| ------------ | ------------------- | ------------------------------------- |
|
|
151
|
+
| `'dash'` | DASH (`.mpd`) | Dynamic Adaptive Streaming over HTTP |
|
|
152
|
+
| `'hls'` | HLS (`.m3u8`) | HTTP Live Streaming |
|
|
153
|
+
| `'mp4'` | Progressive (`.mp4`)| Standard progressive HTTP download |
|
|
154
|
+
|
|
155
|
+
### Auto-Detection (Recommended)
|
|
156
|
+
|
|
157
|
+
If the URI contains a known extension, you can omit `type` entirely:
|
|
158
|
+
|
|
159
|
+
```jsx
|
|
160
|
+
<Media3Player
|
|
161
|
+
source={{uri: 'https://example.com/stream/manifest.mpd'}}
|
|
162
|
+
autoplay
|
|
163
|
+
play
|
|
164
|
+
/>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The player will automatically detect this as a DASH stream.
|
|
168
|
+
|
|
169
|
+
### Explicit Type Override
|
|
170
|
+
|
|
171
|
+
Use `type` when the URI does not have a recognizable extension:
|
|
172
|
+
|
|
173
|
+
```jsx
|
|
174
|
+
<Media3Player
|
|
175
|
+
source={{
|
|
176
|
+
uri: 'https://cdn.example.com/stream?token=abc123',
|
|
177
|
+
type: 'hls',
|
|
178
|
+
}}
|
|
179
|
+
autoplay
|
|
180
|
+
play
|
|
181
|
+
/>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## DRM Support (Widevine)
|
|
187
|
+
|
|
188
|
+
This library supports **Widevine DRM** for playing protected DASH streams on Android. To enable DRM, pass a `drm` object inside the `source` prop.
|
|
189
|
+
|
|
190
|
+
### Basic DRM Playback
|
|
191
|
+
|
|
192
|
+
```jsx
|
|
193
|
+
<Media3Player
|
|
194
|
+
source={{
|
|
195
|
+
uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd',
|
|
196
|
+
drm: {
|
|
197
|
+
licenseUrl: 'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
|
|
198
|
+
},
|
|
199
|
+
}}
|
|
200
|
+
autoplay
|
|
201
|
+
play
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### DRM with Custom License Headers
|
|
206
|
+
|
|
207
|
+
Some license servers require authentication tokens or custom headers. Pass them as an array of `{ key, value }` objects:
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
<Media3Player
|
|
211
|
+
source={{
|
|
212
|
+
uri: 'https://example.com/protected-stream/manifest.mpd',
|
|
213
|
+
drm: {
|
|
214
|
+
licenseUrl: 'https://license.example.com/widevine',
|
|
215
|
+
headers: [
|
|
216
|
+
{key: 'Authorization', value: 'Bearer <your-token>'},
|
|
217
|
+
{key: 'X-Custom-Header', value: 'custom-value'},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
}}
|
|
221
|
+
autoplay
|
|
222
|
+
play
|
|
223
|
+
onError={e => console.error('DRM error:', e.message)}
|
|
224
|
+
/>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Notes
|
|
228
|
+
|
|
229
|
+
- DRM is currently supported on **Android only** (Widevine L1/L3 depending on device).
|
|
230
|
+
- The `source.uri` should point to a DASH (`.mpd`) stream encrypted with Widevine.
|
|
231
|
+
- Multi-session DRM is enabled by default for streams that require it.
|
|
232
|
+
- If the license request fails, the `onError` callback will fire with the error details.
|
|
72
233
|
|
|
73
234
|
---
|
|
74
235
|
|
|
75
236
|
## Events
|
|
76
237
|
|
|
77
|
-
| Event
|
|
78
|
-
|
|
79
|
-
| `onReady`
|
|
80
|
-
| `onEnd`
|
|
81
|
-
| `onError`
|
|
238
|
+
| Event | Callback Signature | Description |
|
|
239
|
+
| --------- | -------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
240
|
+
| `onReady` | `() => void` | Fired when the player is ready to play. |
|
|
241
|
+
| `onEnd` | `() => void` | Fired when the video reaches the end. |
|
|
242
|
+
| `onError` | `(error: { message: string }) => void` | Fired when the player encounters an error. Error object includes a `message` property. |
|
|
82
243
|
|
|
83
244
|
---
|
|
84
245
|
|
|
@@ -88,18 +249,39 @@ This library includes TypeScript definitions. Example:
|
|
|
88
249
|
|
|
89
250
|
```ts
|
|
90
251
|
import React from 'react';
|
|
91
|
-
import {
|
|
92
|
-
import Media3Player, {
|
|
252
|
+
import {ViewStyle} from 'react-native';
|
|
253
|
+
import Media3Player, {Media3PlayerProps} from 'react-native-media3-player';
|
|
93
254
|
|
|
94
255
|
const props: Media3PlayerProps = {
|
|
95
|
-
source: {
|
|
256
|
+
source: {uri: 'https://example.com/video.mp4'},
|
|
96
257
|
autoplay: true,
|
|
97
258
|
play: true,
|
|
98
259
|
mute: false,
|
|
99
|
-
style: {
|
|
260
|
+
style: {width: '100%', height: 250},
|
|
100
261
|
onReady: () => console.log('Ready'),
|
|
101
262
|
onEnd: () => console.log('End'),
|
|
102
|
-
onError:
|
|
263
|
+
onError: err => console.log(err.message),
|
|
264
|
+
};
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**With DRM and explicit stream type:**
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const drmProps: Media3PlayerProps = {
|
|
271
|
+
source: {
|
|
272
|
+
uri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd',
|
|
273
|
+
type: 'dash',
|
|
274
|
+
drm: {
|
|
275
|
+
licenseUrl: 'https://cwip-shaka-proxy.appspot.com/no_auth',
|
|
276
|
+
headers: [
|
|
277
|
+
{key: 'Authorization', value: 'Bearer my-token'},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
autoplay: true,
|
|
282
|
+
play: true,
|
|
283
|
+
style: {width: '100%', height: 250},
|
|
284
|
+
onError: err => console.log('DRM Error:', err.message),
|
|
103
285
|
};
|
|
104
286
|
```
|
|
105
287
|
|
|
@@ -108,26 +290,84 @@ const props: Media3PlayerProps = {
|
|
|
108
290
|
## Examples
|
|
109
291
|
|
|
110
292
|
**Basic Player:**
|
|
293
|
+
|
|
111
294
|
```jsx
|
|
112
|
-
<Media3Player source={{
|
|
295
|
+
<Media3Player source={{uri: 'https://example.com/video.mp4'}} />
|
|
113
296
|
```
|
|
114
297
|
|
|
115
298
|
**Autoplay & Mute:**
|
|
299
|
+
|
|
116
300
|
```jsx
|
|
117
|
-
<Media3Player
|
|
118
|
-
source={{ uri: 'https://example.com/video.mp4' }}
|
|
119
|
-
autoplay
|
|
120
|
-
mute
|
|
121
|
-
/>
|
|
301
|
+
<Media3Player source={{uri: 'https://example.com/video.mp4'}} autoplay mute />
|
|
122
302
|
```
|
|
123
303
|
|
|
124
304
|
**Event Handling:**
|
|
305
|
+
|
|
125
306
|
```jsx
|
|
126
307
|
<Media3Player
|
|
127
|
-
source={{
|
|
308
|
+
source={{uri: 'https://example.com/video.mp4'}}
|
|
128
309
|
onReady={() => console.log('Ready')}
|
|
129
310
|
onEnd={() => console.log('End')}
|
|
130
|
-
onError={
|
|
311
|
+
onError={err => console.error(err.message)}
|
|
312
|
+
/>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**HLS Stream:**
|
|
316
|
+
|
|
317
|
+
```jsx
|
|
318
|
+
<Media3Player
|
|
319
|
+
source={{
|
|
320
|
+
uri: 'https://example.com/live/stream.m3u8',
|
|
321
|
+
type: 'hls',
|
|
322
|
+
}}
|
|
323
|
+
autoplay
|
|
324
|
+
play
|
|
325
|
+
/>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**DASH Stream (auto-detected):**
|
|
329
|
+
|
|
330
|
+
```jsx
|
|
331
|
+
<Media3Player
|
|
332
|
+
source={{uri: 'https://example.com/video/manifest.mpd'}}
|
|
333
|
+
autoplay
|
|
334
|
+
play
|
|
335
|
+
/>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Widevine DRM Stream:**
|
|
339
|
+
|
|
340
|
+
```jsx
|
|
341
|
+
<Media3Player
|
|
342
|
+
source={{
|
|
343
|
+
uri: 'https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd',
|
|
344
|
+
type: 'dash',
|
|
345
|
+
drm: {
|
|
346
|
+
licenseUrl: 'https://proxy.uat.widevine.com/proxy?provider=widevine_test',
|
|
347
|
+
},
|
|
348
|
+
}}
|
|
349
|
+
autoplay
|
|
350
|
+
play
|
|
351
|
+
/>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**DRM with License Headers:**
|
|
355
|
+
|
|
356
|
+
```jsx
|
|
357
|
+
<Media3Player
|
|
358
|
+
source={{
|
|
359
|
+
uri: 'https://example.com/protected/manifest.mpd',
|
|
360
|
+
type: 'dash',
|
|
361
|
+
drm: {
|
|
362
|
+
licenseUrl: 'https://license.example.com/widevine',
|
|
363
|
+
headers: [
|
|
364
|
+
{key: 'Authorization', value: 'Bearer my-token'},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
}}
|
|
368
|
+
autoplay
|
|
369
|
+
play
|
|
370
|
+
onError={err => console.error('DRM Error:', err.message)}
|
|
131
371
|
/>
|
|
132
372
|
```
|
|
133
373
|
|
|
@@ -135,13 +375,13 @@ const props: Media3PlayerProps = {
|
|
|
135
375
|
|
|
136
376
|
## Contributing
|
|
137
377
|
|
|
138
|
-
We welcome contributions!
|
|
378
|
+
We welcome contributions!
|
|
139
379
|
|
|
140
|
-
1. Fork the repo
|
|
141
|
-
2. Create a branch (`git checkout -b feature/new-feature`)
|
|
142
|
-
3. Commit your changes (`git commit -m 'Add feature'`)
|
|
143
|
-
4. Push to the branch (`git push origin feature/new-feature`)
|
|
144
|
-
5. Open a Pull Request
|
|
380
|
+
1. Fork the repo
|
|
381
|
+
2. Create a branch (`git checkout -b feature/new-feature`)
|
|
382
|
+
3. Commit your changes (`git commit -m 'Add feature'`)
|
|
383
|
+
4. Push to the branch (`git push origin feature/new-feature`)
|
|
384
|
+
5. Open a Pull Request
|
|
145
385
|
|
|
146
386
|
> Please follow [Semantic Versioning](https://semver.org/) for commits.
|
|
147
387
|
|
|
@@ -150,4 +390,3 @@ We welcome contributions!
|
|
|
150
390
|
## License
|
|
151
391
|
|
|
152
392
|
MIT © [Mohammad Rehan](https://github.com/mdRehan991)
|
|
153
|
-
|
package/android/build.gradle
CHANGED
|
@@ -5,6 +5,11 @@ apply plugin: "org.jetbrains.kotlin.android"
|
|
|
5
5
|
// Apply the React Native plugin to enable React Native integration
|
|
6
6
|
apply plugin: "com.facebook.react"
|
|
7
7
|
|
|
8
|
+
// Use app-defined version if available, otherwise fallback
|
|
9
|
+
def media3Version = rootProject.ext.has("media3Version")
|
|
10
|
+
? rootProject.ext.media3Version
|
|
11
|
+
: "1.4.1"
|
|
12
|
+
|
|
8
13
|
android {
|
|
9
14
|
// Set the namespace for the generated R class and manifest
|
|
10
15
|
namespace "com.rnmedia3playerdemo"
|
|
@@ -46,9 +51,17 @@ dependencies {
|
|
|
46
51
|
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
|
47
52
|
|
|
48
53
|
// AndroidX Media3 ExoPlayer, UI components, and Common classes
|
|
49
|
-
implementation "androidx.media3:media3-exoplayer
|
|
50
|
-
implementation "androidx.media3:media3-ui
|
|
51
|
-
implementation "androidx.media3:media3-common
|
|
54
|
+
implementation "androidx.media3:media3-exoplayer:$media3Version"
|
|
55
|
+
implementation "androidx.media3:media3-ui:$media3Version"
|
|
56
|
+
implementation "androidx.media3:media3-common:$media3Version"
|
|
57
|
+
|
|
58
|
+
// streaming formats
|
|
59
|
+
implementation "androidx.media3:media3-exoplayer-dash:$media3Version"
|
|
60
|
+
implementation "androidx.media3:media3-exoplayer-hls:$media3Version"
|
|
61
|
+
implementation "androidx.media3:media3-exoplayer-smoothstreaming:$media3Version"
|
|
62
|
+
|
|
63
|
+
// OkHttp data source for Media3 (DRM/auth/advanced networking)
|
|
64
|
+
implementation "androidx.media3:media3-datasource-okhttp:$media3Version"
|
|
52
65
|
|
|
53
66
|
// Android core utilities and extensions (KTX)
|
|
54
67
|
implementation "androidx.core:core-ktx:1.13.1"
|
|
@@ -14,6 +14,14 @@ import com.facebook.react.bridge.ReactContext
|
|
|
14
14
|
import com.facebook.react.bridge.WritableMap
|
|
15
15
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
16
16
|
import com.facebook.react.uimanager.events.Event
|
|
17
|
+
import androidx.media3.common.C
|
|
18
|
+
import androidx.media3.common.util.Util
|
|
19
|
+
import androidx.media3.datasource.DefaultHttpDataSource
|
|
20
|
+
import androidx.media3.exoplayer.source.MediaSource
|
|
21
|
+
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
|
22
|
+
import androidx.media3.exoplayer.dash.DashMediaSource
|
|
23
|
+
import androidx.media3.exoplayer.hls.HlsMediaSource
|
|
24
|
+
import androidx.media3.exoplayer.smoothstreaming.SsMediaSource
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Custom view that wraps ExoPlayer and PlayerView, integrating with React Native.
|
|
@@ -83,19 +91,174 @@ class Media3PlayerView(context: Context) : FrameLayout(context) {
|
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
94
|
+
* Maps a media type string from JavaScript to one of Media3's content type constants.
|
|
95
|
+
*
|
|
96
|
+
* This function interprets the "type" property provided from React Native JS props ("dash", "hls", "mp4")
|
|
97
|
+
* and translates it to the corresponding Media3 content type constant used by ExoPlayer.
|
|
98
|
+
* Returns null if the string is null, empty, or does not match a known type.
|
|
99
|
+
*
|
|
100
|
+
* @param type Optional string type from JS ("dash", "hls", "mp4")
|
|
101
|
+
* @return Media3 content type constant (C.CONTENT_TYPE_DASH, etc.), or null if unrecognized
|
|
88
102
|
*/
|
|
89
|
-
fun
|
|
103
|
+
private fun mapTypeFromJS(type: String?): Int? {
|
|
104
|
+
return when (type?.lowercase()) {
|
|
105
|
+
"dash" -> C.CONTENT_TYPE_DASH
|
|
106
|
+
"hls" -> C.CONTENT_TYPE_HLS
|
|
107
|
+
"mp4" -> C.CONTENT_TYPE_OTHER
|
|
108
|
+
else -> null
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Infers the content type of a given URI for media playback.
|
|
114
|
+
*
|
|
115
|
+
* This method first uses Media3's built-in Util.inferContentType to try
|
|
116
|
+
* to detect the type (DASH, HLS, SmoothStreaming, or Other) based on file extension or URI.
|
|
117
|
+
* If this detection fails (returns CONTENT_TYPE_OTHER), it falls back to checking
|
|
118
|
+
* for well-known streaming manifest extensions (.mpd for DASH, .m3u8 for HLS)
|
|
119
|
+
* in the URI string. This fallback is important for cases where extensions are
|
|
120
|
+
* not present (e.g. signed URLs or tokens).
|
|
121
|
+
*
|
|
122
|
+
* @param uri The Uri of the media source.
|
|
123
|
+
* @return The detected content type as one of C.CONTENT_TYPE_* constants.
|
|
124
|
+
*/
|
|
125
|
+
private fun inferContentTypeSafe(uri: Uri): Int {
|
|
126
|
+
|
|
127
|
+
// Use Media3's built-in Util.inferContentType to try to detect the type.
|
|
128
|
+
val detectedType = Util.inferContentType(uri)
|
|
129
|
+
|
|
130
|
+
// If Media3 was able to determine the type, return it early.
|
|
131
|
+
if (detectedType != C.CONTENT_TYPE_OTHER) {
|
|
132
|
+
return detectedType
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Convert the Uri to a string
|
|
136
|
+
val url = uri.toString()
|
|
137
|
+
|
|
138
|
+
// Fallback: manually check for known manifest extensions in the URL
|
|
139
|
+
// This helps handle sources where the content type can't be inferred automatically.
|
|
140
|
+
return when {
|
|
141
|
+
url.contains(".mpd", ignoreCase = true) -> C.CONTENT_TYPE_DASH
|
|
142
|
+
url.contains(".m3u8", ignoreCase = true) -> C.CONTENT_TYPE_HLS
|
|
143
|
+
else -> C.CONTENT_TYPE_OTHER
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Builds a MediaSource instance required by ExoPlayer based on the content type of the given URI.
|
|
149
|
+
* Handles DASH, HLS, SmoothStreaming, and generic progressive streams.
|
|
150
|
+
*
|
|
151
|
+
* @param uri The Uri of the media to play.
|
|
152
|
+
* @param mediaItem The MediaItem (with possible DRM/config) for playback.
|
|
153
|
+
* @return The constructed MediaSource for the ExoPlayer.
|
|
154
|
+
*/
|
|
155
|
+
private fun buildMediaSource(
|
|
156
|
+
uri: Uri,
|
|
157
|
+
mediaItem: MediaItem,
|
|
158
|
+
type: String?
|
|
159
|
+
): MediaSource {
|
|
160
|
+
|
|
161
|
+
// Create a DefaultHttpDataSourceFactory for the MediaSource.
|
|
162
|
+
// This is used to fetch the media content from the network.
|
|
163
|
+
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
|
164
|
+
|
|
165
|
+
// Check if a type was explicitly passed from JS (e.g., "hls", "dash", "mp4") and map to Media3 type constant.
|
|
166
|
+
val overrideType = mapTypeFromJS(type)
|
|
167
|
+
// Otherwise, infer the content type from the URI (file extension or stream manifest).
|
|
168
|
+
val detectedType = inferContentTypeSafe(uri)
|
|
169
|
+
// Use the JS override type if available, else fall back to detected type.
|
|
170
|
+
val finalType = overrideType ?: detectedType
|
|
171
|
+
|
|
172
|
+
// Log the content type detection process for debugging purposes.
|
|
173
|
+
Log.d(
|
|
174
|
+
"Media3Player",
|
|
175
|
+
"Type → override: $overrideType detected: $detectedType final: $finalType url: $uri"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// Determine content type and return the appropriate MediaSource.
|
|
179
|
+
// This is used to create the appropriate MediaSource for the ExoPlayer.
|
|
180
|
+
return when (finalType) {
|
|
181
|
+
// DASH (MPD) stream
|
|
182
|
+
C.CONTENT_TYPE_DASH -> {
|
|
183
|
+
DashMediaSource.Factory(dataSourceFactory)
|
|
184
|
+
.createMediaSource(mediaItem)
|
|
185
|
+
}
|
|
186
|
+
// HLS (M3U8) stream
|
|
187
|
+
C.CONTENT_TYPE_HLS -> {
|
|
188
|
+
HlsMediaSource.Factory(dataSourceFactory)
|
|
189
|
+
.createMediaSource(mediaItem)
|
|
190
|
+
}
|
|
191
|
+
// SmoothStreaming (ISM) stream
|
|
192
|
+
C.CONTENT_TYPE_SS -> {
|
|
193
|
+
SsMediaSource.Factory(dataSourceFactory)
|
|
194
|
+
.createMediaSource(mediaItem)
|
|
195
|
+
}
|
|
196
|
+
// Progressive HTTP file (MP4, MP3, etc.)
|
|
197
|
+
C.CONTENT_TYPE_OTHER -> {
|
|
198
|
+
ProgressiveMediaSource.Factory(dataSourceFactory)
|
|
199
|
+
.createMediaSource(mediaItem)
|
|
200
|
+
}
|
|
201
|
+
// Fallback to progressive for unknown types
|
|
202
|
+
else -> {
|
|
203
|
+
ProgressiveMediaSource.Factory(dataSourceFactory)
|
|
204
|
+
.createMediaSource(mediaItem)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Sets the video source (with optional DRM support) and prepares the player.
|
|
212
|
+
* Called from the ViewManager when the JS "source" prop changes.
|
|
213
|
+
*
|
|
214
|
+
* @param uriString The URI of the media source to play.
|
|
215
|
+
* @param licenseUrl If provided, enables DRM playback with this license URL.
|
|
216
|
+
* @param headers Optional headers to add to DRM license requests.
|
|
217
|
+
*/
|
|
218
|
+
fun setSource(
|
|
219
|
+
uriString: String?,
|
|
220
|
+
type: String?,
|
|
221
|
+
licenseUrl: String?,
|
|
222
|
+
headers: Map<String, String>?
|
|
223
|
+
) {
|
|
224
|
+
// Return early if no valid URI is provided
|
|
90
225
|
if (uriString.isNullOrEmpty()) return
|
|
226
|
+
|
|
227
|
+
// Store the URI string for reference
|
|
91
228
|
sourceUri = uriString
|
|
92
229
|
|
|
93
|
-
// Ensure
|
|
230
|
+
// Ensure the ExoPlayer instance is initialized before use
|
|
94
231
|
initializePlayer()
|
|
95
232
|
|
|
96
|
-
//
|
|
97
|
-
val
|
|
98
|
-
|
|
233
|
+
// Parse the URI string into a Uri object
|
|
234
|
+
val uri = Uri.parse(uriString)
|
|
235
|
+
|
|
236
|
+
// Build the MediaItem, adding DRM configuration if a license URL is given.
|
|
237
|
+
// This includes setting the license URL and multi-session support for DRM streams.
|
|
238
|
+
// If no license URL is provided, a simple MediaItem is created from the URI.
|
|
239
|
+
val mediaItem =
|
|
240
|
+
if (!licenseUrl.isNullOrEmpty()) {
|
|
241
|
+
val drmBuilder = MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
|
|
242
|
+
.setLicenseUri(licenseUrl) // set the license URL
|
|
243
|
+
.setMultiSession(true) // allow multiple DRM sessions if the stream requires them
|
|
244
|
+
|
|
245
|
+
headers?.let {
|
|
246
|
+
drmBuilder.setLicenseRequestHeaders(it)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
MediaItem.Builder()
|
|
250
|
+
.setUri(uri)
|
|
251
|
+
.setDrmConfiguration(drmBuilder.build())
|
|
252
|
+
.build()
|
|
253
|
+
} else {
|
|
254
|
+
// No DRM: simple MediaItem from URI
|
|
255
|
+
MediaItem.fromUri(uri)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build the appropriate MediaSource (handles progressive, DASH/HLS/SmoothStreaming)
|
|
259
|
+
// and assign it to ExoPlayer. Prepare ExoPlayer for playback.
|
|
260
|
+
val mediaSource = buildMediaSource(uri, mediaItem, type)
|
|
261
|
+
exoPlayer?.setMediaSource(mediaSource)
|
|
99
262
|
exoPlayer?.prepare()
|
|
100
263
|
}
|
|
101
264
|
|
|
@@ -45,14 +45,36 @@ class Media3PlayerViewManager :
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Handles the "source" prop for Media3PlayerView.
|
|
49
|
+
* Extracts the "uri", optional DRM license URL, and headers from the ReadableMap and forwards
|
|
50
|
+
* them to the view for loading the media source and DRM configuration.
|
|
50
51
|
*/
|
|
51
52
|
@ReactProp(name = "source")
|
|
52
53
|
override fun setSource(view: Media3PlayerView, source: ReadableMap?) {
|
|
53
54
|
val uri = source?.getString("uri")
|
|
55
|
+
val type = source?.getString("type")
|
|
56
|
+
var licenseUrl: String? = null
|
|
57
|
+
var headers: Map<String, String>? = null
|
|
58
|
+
val drmMap = source?.getMap("drm")
|
|
59
|
+
|
|
60
|
+
if (drmMap != null) {
|
|
61
|
+
licenseUrl = drmMap.getString("licenseUrl")
|
|
62
|
+
val headersArray = drmMap.getArray("headers")
|
|
63
|
+
if (headersArray != null) {
|
|
64
|
+
val map = mutableMapOf<String, String>()
|
|
65
|
+
for (i in 0 until headersArray.size()) {
|
|
66
|
+
val entry = headersArray.getMap(i)
|
|
67
|
+
val key = entry?.getString("key")
|
|
68
|
+
val value = entry?.getString("value")
|
|
69
|
+
if (key != null && value != null) {
|
|
70
|
+
map[key] = value
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
headers = map
|
|
74
|
+
}
|
|
75
|
+
}
|
|
54
76
|
if (!uri.isNullOrEmpty()) {
|
|
55
|
-
view.setSource(uri)
|
|
77
|
+
view.setSource(uri, type, licenseUrl, headers)
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
// Import the base ViewProps type from React Native to extend the native component's props
|
|
2
2
|
import type {ViewProps} from 'react-native';
|
|
3
3
|
// Import the DirectEventHandler type used for native-to-JS event callback typings
|
|
4
|
-
import type {DirectEventHandler} from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
import type {DirectEventHandler, WithDefault} from 'react-native/Libraries/Types/CodegenTypes';
|
|
5
5
|
// Import codegenNativeComponent to register the native UI component for use in React Native
|
|
6
6
|
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
7
7
|
|
|
8
|
+
type HeaderEntry = Readonly<{
|
|
9
|
+
key: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
type DRMConfig = Readonly<{
|
|
14
|
+
licenseUrl: string;
|
|
15
|
+
headers?: ReadonlyArray<HeaderEntry>;
|
|
16
|
+
}>;
|
|
17
|
+
// Source defines the media source for the player component.
|
|
18
|
+
// - uri: The URI of the media to be played (required).
|
|
19
|
+
// - drm: (Optional) DRM configuration for protected content.
|
|
20
|
+
type Source = Readonly<{
|
|
21
|
+
uri: string;
|
|
22
|
+
type?: WithDefault<'dash' | 'hls' | 'mp4', 'mp4'>;
|
|
23
|
+
drm?: DRMConfig;
|
|
24
|
+
}>;
|
|
8
25
|
// Event type emitted when the player is ready
|
|
9
26
|
type OnReadyEvent = Readonly<{}>;
|
|
10
27
|
// Event type emitted when playback reaches the end
|
|
@@ -17,7 +34,7 @@ type OnErrorEvent = Readonly<{
|
|
|
17
34
|
// Props supported by the native Media3PlayerView component
|
|
18
35
|
export interface NativeProps extends ViewProps {
|
|
19
36
|
// Source URI for the media to play
|
|
20
|
-
source?:
|
|
37
|
+
source?: Source;
|
|
21
38
|
// Whether playback should start automatically when ready
|
|
22
39
|
autoplay?: boolean;
|
|
23
40
|
// Whether playback should currently be running (true = play, false = pause)
|