├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── lint.yml
├── .gitignore
├── .npmignore
├── .prettierrc.js
├── CONTRIBUTING.md
├── LICENSE
├── babel.config.js
├── docs
├── about.mdx
├── basic_usage.mdx
├── install.mdx
├── module_methods.mdx
├── navigation-crash.mdx
├── play-store-compatibility.mdx
├── props.mdx
├── ref_methods.mdx
├── remove-context-share.mdx
├── self_host.mdx
└── show-elapsed-time.mdx
├── iframe.html
├── index.d.ts
├── package.json
├── readme.md
├── src
├── PlayerScripts.js
├── WebView.native.js
├── WebView.web.js
├── YoutubeIframe.js
├── constants.js
├── index.js
├── oEmbed.js
└── utils.js
├── website
├── .gitignore
├── README.md
├── babel.config.js
├── docusaurus.config.js
├── generateIframe.js
├── package.json
├── sidebars.js
├── src
│ └── css
│ │ └── custom.css
├── static
│ ├── .nojekyll
│ ├── iframe.html
│ ├── iframe_v2.html
│ └── img
│ │ ├── demo.gif
│ │ ├── favicon.ico
│ │ └── logo.svg
└── yarn.lock
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native-community',
4 | };
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
22 | **Smartphone (please complete the following information):**
23 | - Device: [e.g. iPhone6]
24 | - OS + version: [e.g. iOS 13.5 | Android 10]
25 | - `react-native-youtube-iframe` version
26 | - `react-native-webview` version
27 | - `Expo` verison [if using expo]
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: run eslint on code
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: install modules
12 | run: yarn
13 | - name: run lint
14 | run: yarn lint
15 | env:
16 | CI: true
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | .idea
4 | dist
5 | .env
6 | .npmrc
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | docs/*
2 | website/*
3 | .eslintrc.js
4 | .prettierrc.js
5 | .github/*
6 | .vscode/*
7 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: false,
3 | jsxBracketSameLine: true,
4 | singleQuote: true,
5 | trailingComma: 'all',
6 | arrowParens: 'avoid',
7 | };
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing guide
2 |
3 | 1. create an issue to discuss the change
4 | 2. create a PR if change makes sense after the discussion
5 | 3. PR review + merge!
6 |
7 | ## package manager
8 |
9 | Use `yarn`, do not create package-lock.json from npm
10 |
11 | ## code lint
12 |
13 | The repo uses **eslint** and **prettier** to enforce some style and lint rules. Make sure that `yarn lint` runs successfully on your branch.
14 |
15 | ## Local project setup
16 |
17 | ### library setup
18 |
19 | 1. clone the repo (of your fork) in a separate folder
20 | 2. run `yarn` in the root directory
21 | 3. run `yarn build` in the root directory
22 |
23 | (this should create a `dist` folder)
24 |
25 | ### test app setup
26 |
27 | 1. open or create a new react native project (expo and RN CLI both work)
28 | 2. add a dependency in package.json like
29 |
30 | ```json
31 | {
32 | "dependencies": {
33 | "react-native-youtube-iframe": "path/to/cloned/folder"
34 | }
35 | }
36 | ```
37 |
38 | 3. run the app and use the iframe as stated in the docs
39 |
40 | ### making changes
41 |
42 | 1. make a change in the cloned folder
43 | 2. run `yarn build`
44 | 3. reload / refresh test app
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ananthu Kanive
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | if (api) {
3 | api.cache(false);
4 | }
5 |
6 | return {
7 | presets: ['module:metro-react-native-babel-preset'],
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/docs/about.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: about
3 | title: React Native Youtube-iframe
4 | slug: /
5 | ---
6 |
7 | import useBaseUrl from '@docusaurus/useBaseUrl';
8 |
9 |  
10 |
11 | A wrapper of the **Youtube-iframe API** built for react native.
12 |
13 | - ✅ Works seamlessly on both ios and android platforms
14 | - ✅ Does not rely on the native youtube service on android (prevents unexpected crashes, works on phones without the youtube app)
15 | - ✅ Uses the webview player which is known to be more stable compared to the native youtube app
16 | - ✅ Access to a vast API provided through the iframe youtube API
17 | - ✅ Supports multiple youtube player instances in a single page
18 | - ✅ Fetch basic video metadata without API keys (uses oEmbed)
19 | - ✅ Works on modals and overlay components
20 | - ✅ Expo support
21 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/basic_usage.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: basic-usage
3 | title: Basic Usage
4 | ---
5 |
6 | This snippet renders a Youtube video with a button that can play or pause the video. When the player has finished playing it, an alert is triggered.
7 |
8 | ```jsx
9 | import React, { useState, useCallback, useRef } from "react";
10 | import { Button, View, Alert } from "react-native";
11 | import YoutubePlayer from "react-native-youtube-iframe";
12 |
13 | export default function App() {
14 | const [playing, setPlaying] = useState(false);
15 |
16 | const onStateChange = useCallback((state) => {
17 | if (state === "ended") {
18 | setPlaying(false);
19 | Alert.alert("video has finished playing!");
20 | }
21 | }, []);
22 |
23 | const togglePlaying = useCallback(() => {
24 | setPlaying((prev) => !prev);
25 | }, []);
26 |
27 | return (
28 |
29 |
35 |
36 |
37 | );
38 | }
39 | ```
--------------------------------------------------------------------------------
/docs/install.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: install
3 | title: Installation
4 | ---
5 |
6 | :::note
7 | This package uses react-hooks and therefore will need
8 |
9 | **react-native 0.59 or above**
10 | (recommended - 0.60 or above)
11 | :::
12 |
13 | 1. First install `react-native-webview`.
14 |
15 | - React Native CLI app - [Instructions](https://github.com/react-native-community/react-native-webview/blob/master/docs/Getting-Started.md)
16 |
17 | - React Native **`0.60` and above**, install the latest version of react-native-webview.
18 | - React Native **below `0.60`**, react-native-webview version `6.11.1` is the last version that supports it.
19 |
20 | - Expo App - [Instructions](https://docs.expo.io/versions/latest/sdk/webview/)
21 |
22 | 2. Install
23 |
24 | :::info yarn - recommended
25 |
26 | `yarn add react-native-youtube-iframe`
27 | :::
28 |
29 | :::danger npm
30 |
31 | `npm install react-native-youtube-iframe`
32 |
33 | npm has some issues with peer dependencies where it tries to install all peer dependencies by default. If your project does not use react 17, it might case install failures. To get around this, use
34 |
35 | `npm install react-native-youtube-iframe --legacy-peer-deps`
36 |
--------------------------------------------------------------------------------
/docs/module_methods.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: module-methods
3 | title: Module Methods
4 | ---
5 |
6 | export const Type = ({children, color}) => (
7 |
15 | {children}
16 |
17 | );
18 |
19 | ### `getYoutubeMeta`
20 |
21 | getYoutubeMeta(videoId: String): Promise[youtubeMeta]
22 |
23 | Fetch metadata of a youtube video using the oEmbed Spec - https://oembed.com/#section7
24 |
25 | metadata returned -
26 |
27 | | field | type | explanation |
28 | | ---------------- | ------ | -------------------------------------------------- |
29 | | author_name | String | The name of the author/owner of the video. |
30 | | author_url | String | youtube channel link of the video. |
31 | | height | Number | The height in pixels required to display the HTML. |
32 | | html | String | The HTML required to embed a video player. |
33 | | provider_name | String | The name of the resource provider. |
34 | | provider_url | String | The url of the resource provider. |
35 | | thumbnail_height | Number | The height of the video thumbnail. |
36 | | thumbnail_url | String | The url of the resource provider. |
37 | | thumbnail_width | Number | The width of the video thumbnail. |
38 | | title | String | youtube video title. |
39 | | type | String | The oEmbed version number. |
40 | | version | String | The resource type. |
41 | | width | Number | The width in pixels required to display the HTML. |
42 |
43 | example -
44 |
45 | ```javascript
46 | import {Alert} from 'react-native';
47 | import {getYoutubeMeta} from 'react-native-youtube-iframe';
48 |
49 | getYoutubeMeta('sNhhvQGsMEc').then(meta => {
50 | Alert.alert('title of the video : ' + meta.title);
51 | });
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/navigation-crash.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: navigation-crash
3 | title: Crash when used with react-navigation
4 | ---
5 |
6 | - The YoutubePlayer causes a crash immediately on navigating to another screen, and also sometimes when navigating to the screen that contains the component.
7 | - crashes are observed only on android
8 | - only some android versions have this issue (likely tied to google chrome version)
9 |
10 | :::info
11 | This will happen if you have any webviews inside a screen.
12 |
13 | This is not exclusive to the youtube player.
14 | :::
15 |
16 | ## Solutions
17 |
18 | :::tip Pick one that fits your need
19 | Don't try to implement ALL of them.
20 | :::
21 |
22 | ### 1. Tweak react-navigation config
23 |
24 | #### use a transition animations that only involves translations. ([documentation](https://reactnavigation.org/docs/stack-navigator/#pre-made-configs))
25 |
26 | ```jsx
27 |
33 | ```
34 |
35 | #### disable transition animations on screens that have the youtube player. ([documentation](https://reactnavigation.org/docs/stack-navigator/#animationenabled))
36 |
37 | ```jsx
38 |
44 | ```
45 |
46 | ### 2. Tweak webview props
47 |
48 | #### change webview opacity to `0.99` ([issue comment](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/110#issuecomment-779848787))
49 |
50 | ```jsx
51 |
56 | ```
57 |
58 | #### set `renderToHardwareTextureAndroid` ([issue comment](https://github.com/react-native-webview/react-native-webview/issues/811#issuecomment-748611465))
59 |
60 | ```jsx
61 |
68 | ```
69 |
70 | #### tweak `androidLayerType` ([issue comment](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/110#issuecomment-786603325))
71 |
72 | ```jsx
73 |
81 | ```
82 |
83 | ## github threads to follow
84 |
85 | - [issue #110](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/110)
86 | - [issue #101](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/101)
87 | - [rn-webview issue #811](https://github.com/react-native-webview/react-native-webview/issues/811)
88 |
89 | ## should the library handle this?
90 |
91 | no.
92 |
93 | All these are workarounds, and it's the responsibility of core libraries to fix the root cause.
94 |
--------------------------------------------------------------------------------
/docs/play-store-compatibility.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: play-store-compatibility
3 | title: Google Play Store
4 | ---
5 |
6 | Google is very strict about apps on the play store that have youtube content on it. It absolutely does not allow background play in any form. Make sure that your app complies with [youtube's terms of service](https://developers.google.com/youtube/terms/api-services-terms-of-service).
7 |
8 | A few users have faced app rejections and the best way to avoid any issues is to make sure that the video stops when your app. ([#72](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/72), [#105](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/105)).
9 |
10 | - goes to the background
11 | - the video is no longer on the screen
12 |
13 | Use the [AppState](https://reactnative.dev/docs/appstate) api to stop the video when the app is not "active" and use navigational hooks to stop the video when a user navigates away from the screen.
14 |
--------------------------------------------------------------------------------
/docs/props.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: component-props
3 | title: Props
4 | ---
5 |
6 | export const Type = ({children, color}) => (
7 |
15 | {children}
16 |
17 | );
18 |
19 | export const Important = ({children, color}) => (
20 |
28 | {children}
29 |
30 | );
31 |
32 | ### `height`
33 |
34 | Number Required
35 |
36 | height of the webview container
37 |
38 | :::note
39 | Embedded players must have a viewport that is at least 200px by 200px. If the player displays controls, it must be large enough to fully display the controls without shrinking the viewport below the minimum size. We recommend 16:9 players are at least 480 pixels wide and 270 pixels tall.
40 | :::
41 |
42 | ---
43 |
44 | ### `width`
45 |
46 | Number
47 |
48 | width of the webview container
49 |
50 | :::tip
51 | The player will grow to fit the width of its parent, unless the parent has specified `alignItems` or `justifyContent` (depending on flex direction) in which case a width is required.
52 | :::
53 |
54 | :::note
55 | Embedded players must have a viewport that is at least 200px by 200px. If the player displays controls, it must be large enough to fully display the controls without shrinking the viewport below the minimum size. We recommend 16:9 players are at least 480 pixels wide and 270 pixels tall.
56 | :::
57 |
58 | ---
59 |
60 | ### `videoId`
61 |
62 | String
63 |
64 | Specifies the YouTube Video ID of the video to be played.
65 |
66 | ---
67 |
68 | ### `playList`
69 |
70 | String Array[String]
71 |
72 | Specifies the playlist to play. It can be either the playlist ID or a list of video IDs
73 |
74 | `playList={'PLbpi6ZahtOH6Blw3RGYpWkSByi_T7Rygb'}`
75 |
76 | or
77 |
78 | `playList={['QRt7LjqJ45k', 'fHsa9DqmId8']}`
79 |
80 | ---
81 |
82 | ### `playListStartIndex`
83 |
84 | Number
85 |
86 | Starts the playlist from the given index
87 |
88 | :::caution
89 | Works only if the playlist is a list of video IDs
90 | :::
91 |
92 | ---
93 |
94 | ### `play`
95 |
96 | Boolean
97 |
98 | Flag to tell the player to play or pause the video.
99 |
100 | Make sure you match this flag `onChangeState` to handle user pausing
101 | the video from the youtube player UI
102 |
103 | :::note autoPlay
104 | The HTML5 `` element, in certain mobile browsers (such as Chrome and Safari), only allows playback to take place if it's initiated by user interaction (such as tapping on the player).
105 |
106 | However, the webview provides APIs to overcome this and will allow auto play in most cases. Use the [forceAndroidAutoplay](#forceandroidautoplay) prop if autoplay still doesn't work. (usually is affected by older android devices)
107 | :::
108 |
109 | ---
110 |
111 | ### `ref`
112 |
113 | YoutubeIframeRef
114 |
115 | Gives access to the player reference. This can be used to access player functions.
116 |
117 | [Player Functions Documentation](component-ref-methods)
118 |
119 | ---
120 |
121 | ### `baseUrlOverride`
122 |
123 | String
124 |
125 | A link that serves the iframe code. If you want to host the code on your own domain, get the source from [here](https://github.com/LonelyCpp/react-native-youtube-iframe/blob/master/iframe.html).
126 |
127 | Default link - https://lonelycpp.github.io/react-native-youtube-iframe/iframe.html
128 |
129 | ---
130 |
131 | ### `useLocalHTML`
132 |
133 | Boolean
134 |
135 | use locally generated html to render on the webview
136 |
137 | ---
138 |
139 | ### `onChangeState`
140 |
141 | function(event: string)
142 |
143 | This event fires whenever the player's state changes. The callback is fired with an event (string) that corresponds to the new player state. Possible values are:
144 |
145 | | events | description |
146 | | --------- | ----------------------------------------------------------- |
147 | | unstarted | fired before the first video is loaded |
148 | | video cue | next video cue |
149 | | buffering | current video is in playing state but stopped for buffering |
150 | | playing | current video is playing |
151 | | paused | current video is paused |
152 | | ended | video has finished playing the video |
153 |
154 | ---
155 |
156 | ### `onReady`
157 |
158 | function(event: string)
159 |
160 | This event fires when the player has finished loading and is ready to begin receiving API calls. Your application should implement this function if you want to automatically execute certain operations, such as playing the video or displaying information about the video, as soon as the player is ready.
161 |
162 | ---
163 |
164 | ### `onError`
165 |
166 | function(error: string)
167 |
168 | This event fires if an error occurs in the player. The API will pass an error string to the event listener function.
169 | Possible values are:
170 |
171 | | errors | reason |
172 | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
173 | | invalid_parameter | The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks. |
174 | | HTML5_error | The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred. |
175 | | video_not_found | The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private. |
176 | | embed_not_allowed | The owner of the requested video does not allow it to be played in embedded players. |
177 |
178 | ---
179 |
180 | ### `onFullScreenChange`
181 |
182 | function(status: string)
183 |
184 | This event fires whenever the player goes in or out of fullscreen mode with a boolean that identifies the new fullscreen status
185 |
186 | :::caution
187 | android only, see [issue #45](https://github.com/LonelyCpp/react-native-youtube-iframe/issues/45) for work on ios support
188 | :::
189 |
190 | ---
191 |
192 | ### `onPlaybackQualityChange`
193 |
194 | function(quality: string)
195 |
196 | This event fires whenever the video playback quality changes. It might signal a change in the viewer's playback environment.
197 |
198 | The data value that the API passes to the event listener function will be a string that identifies the new playback quality. Possible values are:
199 |
200 | | Quality |
201 | | ------- |
202 | | small |
203 | | medium |
204 | | large |
205 | | hd720 |
206 | | hd1080 |
207 | | highres |
208 |
209 | ---
210 |
211 | ### `mute`
212 |
213 | Boolean
214 |
215 | Flag to tell the player to mute the video.
216 |
217 | ---
218 |
219 | ### `volume`
220 |
221 | Number
222 |
223 | Sets the volume. Accepts an integer between `0` and `100`.
224 |
225 | ---
226 |
227 | ### `playbackRate`
228 |
229 | Number
230 |
231 | This sets the suggested playback rate for the current video. If the playback rate changes, it will only change for the video that is already cued or being played.
232 |
233 | Calling this function does not guarantee that the playback rate will actually change. However, if the playback rate does change, the `onPlaybackRateChange` event will fire, and your code should respond to the event rather than the fact that it called the setPlaybackRate function.
234 |
235 | The `getAvailablePlaybackRates` method will return the possible playback rates for the currently playing video. However, if you set the suggestedRate parameter to a non-supported integer or float value, the player will round that value down to the nearest supported value in the direction of 1.
236 |
237 | ---
238 |
239 | ### `onPlaybackRateChange`
240 |
241 | function(playbackRate: Number)
242 |
243 | This event fires whenever the video playback rate changes. Your application should respond to the event and should not assume that the playback rate will automatically change when the `playbackRate` value changes. Similarly, your code should not assume that the video playback rate will only change as a result of an explicit call to setPlaybackRate.
244 |
245 | The `playbackRate` that the API passes to the event listener function will be a number that identifies the new playback rate. The `getAvailablePlaybackRates` method returns a list of the valid playback rates for the video currently cued or playing.
246 |
247 | ---
248 |
249 | ### `initialPlayerParams`
250 |
251 | InitialPlayerParams
252 |
253 | A set of parameters that are initialized when the player mounts.
254 |
255 | :::caution
256 | changing these parameters might cause the player to restart or not change at all till it is remounted
257 | :::
258 |
259 | | property | type | default | info |
260 | | ------------------ | ------- | ------- | ---------------------------------------------------------------------- |
261 | | loop | boolean | false | https://developers.google.com/youtube/player_parameters#loop |
262 | | controls | boolean | true | https://developers.google.com/youtube/player_parameters#controls |
263 | | cc_lang_pref | string | | https://developers.google.com/youtube/player_parameters#cc_lang_pref |
264 | | showClosedCaptions | boolean | false | https://developers.google.com/youtube/player_parameters#cc_load_policy |
265 | | color | string | 'red' | https://developers.google.com/youtube/player_parameters#color |
266 | | start | Number | | https://developers.google.com/youtube/player_parameters#start |
267 | | end | Number | | https://developers.google.com/youtube/player_parameters#end |
268 | | preventFullScreen | boolean | false | https://developers.google.com/youtube/player_parameters#fs |
269 | | playerLang | String | | https://developers.google.com/youtube/player_parameters#hl |
270 | | iv_load_policy | Number | | https://developers.google.com/youtube/player_parameters#iv_load_policy |
271 | | modestbranding | boolean | false | https://developers.google.com/youtube/player_parameters#modestbranding |
272 | | rel | boolean | false | https://developers.google.com/youtube/player_parameters#rel |
273 |
274 | ---
275 |
276 | ### `viewContainerStyle`
277 |
278 | A style prop that will be given to the webview container
279 |
280 | ---
281 |
282 | ### `webViewStyle`
283 |
284 | A style prop that will be given to the webview
285 |
286 | ---
287 |
288 | ### `webViewProps`
289 |
290 | Props that are supplied to the underlying webview (react-native-webview). A full list of props can be found [here](https://github.com/react-native-community/react-native-webview/blob/master/docs/Reference.md#props-index)
291 |
292 | ---
293 |
294 | ### `forceAndroidAutoplay`
295 |
296 | Boolean
297 |
298 | Changes user string to make autoplay work on the iframe player for some android devices.
299 |
300 | :::info user agent string used -
301 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36';
302 | :::
303 |
304 | ---
305 |
306 | ### `allowWebViewZoom`
307 |
308 | Boolean
309 |
310 | Controls whether the embedded webview allows user to zoom in. Defaults to `false`
311 |
312 | ---
313 |
314 | ### `contentScale`
315 |
316 | Number
317 |
318 | scale factor for initial-scale and maximum-scale in ` ` tag on the webpage. Defaults to `1.0`
319 |
320 | :::info zoom -
321 | enabling the `allowWebViewZoom` disabled the maximum-scale attribute in the webpage
322 | :::
323 |
--------------------------------------------------------------------------------
/docs/ref_methods.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: component-ref-methods
3 | title: Player Functions
4 | ---
5 |
6 | export const Type = ({children, color}) => (
7 |
15 | {children}
16 |
17 | );
18 |
19 | ### Basic usage
20 |
21 | ```jsx
22 | import React, {useRef} from 'react';
23 | import YoutubePlayer, {YoutubeIframeRef} from "react-native-youtube-iframe";
24 |
25 | const App = () => {
26 |
27 | const playerRef = useRef();
28 |
29 | // typescript
30 | // const playerRef = useRef(null);
31 |
32 | return (
33 |
39 |
40 | {
43 | playerRef.current?.getCurrentTime().then(
44 | currentTime => console.log({currentTime})
45 | );
46 |
47 | playerRef.current?.getDuration().then(
48 | getDuration => console.log({getDuration})
49 | );
50 | }}
51 | />
52 | );
53 | };
54 | ```
55 |
56 | ### `getDuration`
57 |
58 | function(): Promise[Number]
59 |
60 | returns a promise that resolves to the total duration of the video.
61 |
62 | If the currently playing video is a live event, the getDuration() function will resolve the elapsed time since the live video stream began. Specifically, this is the amount of time that the video has streamed without being reset or interrupted. In addition, this duration is commonly longer than the actual event time since streaming may begin before the event's start time.
63 |
64 | :::note
65 | getDuration() will return 0 until the video's metadata is loaded, which normally happens just after the video starts playing.
66 | :::
67 |
68 | ---
69 |
70 | ### `getCurrentTime`
71 |
72 | function(): Promise[Number]
73 |
74 | returns a promise that resolves to the elapsed time in seconds since the video started playing.
75 |
76 | ---
77 |
78 | ### `isMuted`
79 |
80 | function(): Promise[Boolean]
81 |
82 | returns a promise that resolves to true if the video is muted, false if not.
83 |
84 | ---
85 |
86 | ### `getVolume`
87 |
88 | function(): Promise[Number]
89 |
90 | returns a promise that resolves to the player's current volume, an integer between 0 and 100. Note that `getVolume()` will return the volume even if the player is muted.
91 |
92 | ---
93 |
94 | ### `getPlaybackRate`
95 |
96 | function(): Promise[Number]
97 |
98 | returns a promise that resolves to the current playback rate of the video.
99 |
100 | The default playback rate is 1, which indicates that the video is playing at normal speed. Playback rates may include values like 0.25, 0.5, 1, 1.5, and 2.
101 |
102 | ---
103 |
104 | ### `getAvailablePlaybackRates`
105 |
106 | function(): Promise[Array[Number]]
107 |
108 | returns a promise that resolves to a list of available playback rates.
109 |
110 | The array of numbers are ordered from slowest to fastest playback speed. Even if the player does not support variable playback speeds, the array should always contain at least one value (1).
111 |
112 | ---
113 |
114 | ### `seekTo`
115 |
116 | function(seconds:Number, allowSeekAhead:Boolean):Void
117 |
118 | Seeks to a specified time in the video. If the player is paused when the function is called, it will remain paused. If the function is called from another state (playing, video cued, etc.), the player will play the video.
119 | The seconds parameter identifies the time to which the player should advance.
120 |
121 | The player will advance to the closest keyframe before that time unless the player has already downloaded the portion of the video to which the user is seeking.
122 |
123 | https://developers.google.com/youtube/iframe_api_reference#seekTo
124 |
--------------------------------------------------------------------------------
/docs/remove-context-share.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: remove-context-share
3 | title: Remove Context Menu
4 | ---
5 |
6 | ---
7 |
8 | ### Removing context menu on long-press:
9 |
10 | Wrap the ` ` in a View that has `pointerEvents="none"` to disable app touch-events to the player.
11 |
12 | Then react-native's [Pressable API](https://reactnative.dev/docs/pressable) or any of the touchables to intercept presses.
13 |
14 | example:
15 |
16 | ```jsx
17 | {
19 | // handle or ignore
20 | }}
21 | onLongPress={() => {
22 | // handle or ignore
23 | }}>
24 |
25 |
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | ### Removing context menu on kebab menu (prevent share):
33 |
34 | It is not possible to change to UI of the player. You can however achieve this by placing an absolutely positioned view on the player (either fully covering the player or just tall enough to cover the title) to prevent the webview from receiving user touches. This will not remove the logo or three dots, but will make it un-interactable.
35 |
36 | example:
37 |
38 | ```jsx
39 |
40 |
41 |
53 |
54 | ```
55 |
--------------------------------------------------------------------------------
/docs/self_host.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: self-host-remote-source
3 | title: Self Host Static HTML page that handles the player
4 | ---
5 |
6 | Prior to `v2.0.0`, this package would generate HTML required for the youtube iframe and serve it as raw HTML string.
7 | This meant that the base URL would be `'about:blank'` and leading to a number of videos showing a "embed not allowed" error message.
8 |
9 | To mitigate this - the webpage had to be uploaded on to a trustable remote source (github pages).
10 | The source code of the [static HTML](https://github.com/LonelyCpp/react-native-youtube-iframe/blob/master/iframe.html) is minified and uploaded as a part of the documentation website [here](https://lonelycpp.github.io/react-native-youtube-iframe/iframe.html).
11 |
12 | For whatever reason, if you would like to host this page on your own web server - the [static HTML](https://github.com/LonelyCpp/react-native-youtube-iframe/blob/master/iframe.html) source can be hosted as it is. _(not recommended since manual update will be required)_
13 |
--------------------------------------------------------------------------------
/docs/show-elapsed-time.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: elapsed-time
3 | title: Show elapsed time
4 | ---
5 |
6 | The player provides a way to fetch current time. Combining this with `setInterval` is the right way to go, since you can control the accuracy and frequency of the reading.
7 |
8 | example:
9 |
10 | ```javascript
11 | import React, {useState, useRef, useEffect} from 'react';
12 | import {Text, View} from 'react-native';
13 | import YoutubePlayer from 'react-native-youtube-iframe';
14 |
15 | const App = () => {
16 | const [elapsed, setElapsed] = useState(0);
17 | const playerRef = useRef();
18 |
19 | useEffect(() => {
20 | const interval = setInterval(async () => {
21 | const elapsed_sec = await playerRef.current.getCurrentTime(); // this is a promise. dont forget to await
22 |
23 | // calculations
24 | const elapsed_ms = Math.floor(elapsed_sec * 1000);
25 | const ms = elapsed_ms % 1000;
26 | const min = Math.floor(elapsed_ms / 60000);
27 | const seconds = Math.floor((elapsed_ms - min * 60000) / 1000);
28 |
29 | setElapsed(
30 | min.toString().padStart(2, '0') +
31 | ':' +
32 | seconds.toString().padStart(2, '0') +
33 | ':' +
34 | ms.toString().padStart(3, '0'),
35 | );
36 | }, 100); // 100 ms refresh. increase it if you don't require millisecond precision
37 |
38 | return () => {
39 | clearInterval(interval);
40 | };
41 | }, []);
42 |
43 | return (
44 | <>
45 |
50 |
51 |
52 | {'elapsed time'}
53 | {elapsed}
54 |
55 | >
56 | );
57 | };
58 | ```
59 |
--------------------------------------------------------------------------------
/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
24 |
25 |
28 |
29 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {StyleProp, ViewStyle} from 'react-native';
3 | import {WebViewProps} from 'react-native-webview';
4 |
5 | export enum PLAYER_STATES {
6 | ENDED = 'ended',
7 | PAUSED = 'paused',
8 | PLAYING = 'playing',
9 | UNSTARTED = 'unstarted',
10 | BUFFERING = 'buffering',
11 | VIDEO_CUED = 'video cued',
12 | }
13 |
14 | export enum PLAYER_ERRORS {
15 | HTML5_ERROR = 'HTML5_error',
16 | VIDEO_NOT_FOUND = 'video_not_found',
17 | EMBED_NOT_ALLOWED = 'embed_not_allowed',
18 | INVALID_PARAMETER = 'invalid_parameter',
19 | }
20 |
21 | export interface YoutubeIframeRef {
22 | getDuration: () => Promise;
23 | getVideoUrl: () => Promise;
24 | getCurrentTime: () => Promise;
25 | isMuted: () => Promise;
26 | getVolume: () => Promise;
27 | getPlaybackRate: () => Promise;
28 | getAvailablePlaybackRates: () => Promise;
29 | seekTo: (seconds: number, allowSeekAhead: boolean) => void;
30 | }
31 |
32 | export interface InitialPlayerParams {
33 | loop?: boolean;
34 | controls?: boolean;
35 | cc_lang_pref?: string;
36 | showClosedCaptions?: boolean;
37 | color?: string;
38 | start?: Number;
39 | end?: Number;
40 | preventFullScreen?: boolean;
41 | playerLang?: String;
42 | iv_load_policy?: Number;
43 | /**
44 | * @deprecated - This parameter has no effect since August 15, 2023
45 | * https://developers.google.com/youtube/player_parameters#modestbranding
46 | */
47 | deprecated?: boolean;
48 | rel?: boolean;
49 | }
50 |
51 | export interface YoutubeIframeProps {
52 | /**
53 | * height of the webview container
54 | *
55 | * Note: Embedded players must have a viewport that is at least 200px by 200px. If the player displays controls, it must be large enough to fully display the controls without shrinking the viewport below the minimum size. We recommend 16:9 players be at least 480 pixels wide and 270 pixels tall.
56 | */
57 | height: number;
58 | /**
59 | * width of the webview container
60 | *
61 | * Note: Embedded players must have a viewport that is at least 200px by 200px. If the player displays controls, it must be large enough to fully display the controls without shrinking the viewport below the minimum size. We recommend 16:9 players be at least 480 pixels wide and 270 pixels tall.
62 | */
63 | width?: number;
64 | /**
65 | * Specifies the YouTube Video ID of the video to be played.
66 | */
67 | videoId?: string;
68 | /**
69 | * Specifies the playlist to play. It can be either the playlist ID or a list of video IDs
70 | *
71 | * @example
72 | * playList={'PLbpi6ZahtOH6Blw3RGYpWkSByi_T7Rygb'}
73 | *
74 | * @example
75 | * playList={['QRt7LjqJ45k', 'fHsa9DqmId8']}
76 | */
77 | playList?: Array | string;
78 | /**
79 | * Flag to tell the player to play or pause the video.
80 | */
81 | play?: boolean;
82 |
83 | /**
84 | * Flag to tell the player to mute the video.
85 | */
86 | mute?: boolean;
87 | /**
88 | * Sets the volume. Accepts an integer between `0` and `100`.
89 | */
90 | volume?: number;
91 | /**
92 | * A style prop that will be given to the webview container
93 | */
94 | viewContainerStyle?: StyleProp;
95 | /**
96 | * A style prop that will be given to the webview
97 | */
98 | webViewStyle?: StyleProp;
99 | /**
100 | * Props that are supplied to the underlying webview (react-native-webview). A full list of props can be found [here](https://github.com/react-native-community/react-native-webview/blob/master/docs/Reference.md#props-index)
101 | */
102 | webViewProps?: WebViewProps;
103 | /**
104 | * This sets the suggested playback rate for the current video. If the playback rate changes, it will only change for the video that is already cued or being played.
105 | */
106 | playbackRate?: number;
107 | /**
108 | * This event fires if an error occurs in the player. The API will pass an error string to the event listener function.
109 | */
110 | onError?: (error: string) => void;
111 | /**
112 | * This event fires whenever a player has finished loading and is ready.
113 | */
114 | onReady?: () => void;
115 | /**
116 | * Starts the playlist from the given index
117 | *
118 | * Works only if the playlist is a list of video IDs.
119 | */
120 | playListStartIndex?: number;
121 | initialPlayerParams?: InitialPlayerParams;
122 | /**
123 | * Changes user string to make autoplay work on the iframe player for some android devices.
124 | */
125 | forceAndroidAutoplay?: boolean;
126 | /**
127 | * callback for when the player's state changes.
128 | */
129 | onChangeState?: (event: PLAYER_STATES) => void;
130 | /**
131 | * callback for when the fullscreen option is clicked in the player. It signals the new fullscreen state of the player.
132 | */
133 | onFullScreenChange?: (status: boolean) => void;
134 | /**
135 | * callback for when the video playback quality changes. It might signal a change in the viewer's playback environment.
136 | */
137 | onPlaybackQualityChange?: (quality: string) => void;
138 | /**
139 | * callback for when the video playback rate changes.
140 | */
141 | onPlaybackRateChange?: (event: string) => void;
142 | /**
143 | * Flag to decide whether or not a user can zoom the video webview.
144 | */
145 | allowWebViewZoom?: boolean;
146 | /**
147 | * Set this React Ref to use ref functions such as getDuration.
148 | */
149 | ref?: React.MutableRefObject;
150 | /**
151 | * scale factor for initial-scale and maximum-scale in
152 | * tag on the webpage
153 | */
154 | contentScale?: number;
155 | /**
156 | * force use locally generated HTML string. defaults to `false`
157 | */
158 | useLocalHTML?: boolean;
159 | /**
160 | * url that fetches a webpage compatible with youtube iframe interface
161 | *
162 | * * defaults to : https://lonelycpp.github.io/react-native-youtube-iframe/iframe.html
163 | * * for code check "iframe.html" in package repo
164 | */
165 | baseUrlOverride?: string;
166 | }
167 |
168 | export interface YoutubeMeta {
169 | thumbnail_width: number;
170 | type: string;
171 | html: string;
172 | height: number;
173 | author_name: string;
174 | width: number;
175 | title: string;
176 | author_url: string;
177 | version: string;
178 | thumbnail_height: number;
179 | provider_url: string;
180 | thumbnail_url: string;
181 | }
182 |
183 | declare const YoutubeIframe: React.VFC;
184 |
185 | export default YoutubeIframe;
186 |
187 | export function getYoutubeMeta(id: string): Promise;
188 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-youtube-iframe",
3 | "version": "2.3.0",
4 | "description": "A simple wrapper around the youtube iframe js API for react native",
5 | "main": "dist/index.js",
6 | "types": "index.d.ts",
7 | "scripts": {
8 | "lint": "eslint src",
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "build": "rimraf ./dist && babel src --out-dir dist",
11 | "prepare": "rimraf ./dist && babel src --out-dir dist"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/LonelyCpp/react-native-youtube-iframe.git"
16 | },
17 | "keywords": [
18 | "react-native",
19 | "react-component",
20 | "react-native-component",
21 | "react",
22 | "react native",
23 | "mobile",
24 | "ios",
25 | "android",
26 | "ui",
27 | "youtube",
28 | "youtube-iframe",
29 | "iframe"
30 | ],
31 | "author": "Ananthu P Kanive",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/LonelyCpp/react-native-youtube-iframe/issues"
35 | },
36 | "homepage": "https://lonelycpp.github.io/react-native-youtube-iframe/",
37 | "peerDependencies": {
38 | "react": ">=16.8.6",
39 | "react-native": ">=0.60",
40 | "react-native-web-webview": ">=1.0.2",
41 | "react-native-webview": ">=7.0.0"
42 | },
43 | "peerDependenciesMeta": {
44 | "react-native-web-webview": {
45 | "optional": true
46 | },
47 | "react-native-webview": {
48 | "optional": true
49 | }
50 | },
51 | "devDependencies": {
52 | "@babel/cli": "^7.2.3",
53 | "@babel/core": "^7.2.2",
54 | "@react-native-community/eslint-config": "^2.0.0",
55 | "eslint": "^7.7.0",
56 | "metro-react-native-babel-preset": "^0.64.0",
57 | "rimraf": "^3.0.2"
58 | },
59 | "dependencies": {
60 | "events": "^3.2.0"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # React Native Youtube iframe
2 |
3 |  
4 |
5 | A wrapper of the Youtube IFrame player API build for react native.
6 |
7 | - ✅ Works seamlessly on both ios and android platforms
8 | - ✅ Does not rely on the native youtube service on android (prevents unexpected crashes, works on phones without the youtube app)
9 | - ✅ Uses the webview player which is known to be more stable compared to the native youtube app
10 | - ✅ Access to a vast API provided through the iframe youtube API
11 | - ✅ Supports multiple youtube player instances in a single page
12 | - ✅ Fetch basic video metadata without API keys (uses oEmbed)
13 | - ✅ Works on modals and overlay components
14 | - ✅ Expo support
15 |
16 | 
17 |
18 | ## Installation and Documentation
19 |
20 | [react-native-youtube-iframe](https://lonelycpp.github.io/react-native-youtube-iframe/)
21 |
22 | ## Contributing
23 |
24 | Pull requests are welcome!
25 |
26 | Read the [contributing guide](./CONTRIBUTING.md) for project setup and other info
27 |
28 | ## License
29 |
30 | [MIT](https://choosealicense.com/licenses/mit/)
31 |
--------------------------------------------------------------------------------
/src/PlayerScripts.js:
--------------------------------------------------------------------------------
1 | import {MUTE_MODE, PAUSE_MODE, PLAY_MODE, UNMUTE_MODE} from './constants';
2 |
3 | export const PLAYER_FUNCTIONS = {
4 | muteVideo: 'player.mute(); true;',
5 | unMuteVideo: 'player.unMute(); true;',
6 | playVideo: 'player.playVideo(); true;',
7 | pauseVideo: 'player.pauseVideo(); true;',
8 | getVideoUrlScript: `
9 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'getVideoUrl', data: player.getVideoUrl()}));
10 | true;
11 | `,
12 | durationScript: `
13 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'getDuration', data: player.getDuration()}));
14 | true;
15 | `,
16 | currentTimeScript: `
17 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'getCurrentTime', data: player.getCurrentTime()}));
18 | true;
19 | `,
20 | isMutedScript: `
21 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'isMuted', data: player.isMuted()}));
22 | true;
23 | `,
24 | getVolumeScript: `
25 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'getVolume', data: player.getVolume()}));
26 | true;
27 | `,
28 | getPlaybackRateScript: `
29 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'getPlaybackRate', data: player.getPlaybackRate()}));
30 | true;
31 | `,
32 | getAvailablePlaybackRatesScript: `
33 | window.ReactNativeWebView.postMessage(JSON.stringify({eventType: 'getAvailablePlaybackRates', data: player.getAvailablePlaybackRates()}));
34 | true;
35 | `,
36 |
37 | setVolume: volume => {
38 | return `player.setVolume(${volume}); true;`;
39 | },
40 |
41 | seekToScript: (seconds, allowSeekAhead) => {
42 | return `player.seekTo(${seconds}, ${allowSeekAhead}); true;`;
43 | },
44 |
45 | setPlaybackRate: playbackRate => {
46 | return `player.setPlaybackRate(${playbackRate}); true;`;
47 | },
48 |
49 | loadPlaylist: (playList, startIndex, play) => {
50 | const index = startIndex || 0;
51 | const func = play ? 'loadPlaylist' : 'cuePlaylist';
52 |
53 | const list = typeof playList === 'string' ? `"${playList}"` : 'undefined';
54 | const listType =
55 | typeof playList === 'string' ? `"${playlist}"` : 'undefined';
56 | const playlist = Array.isArray(playList)
57 | ? `"${playList.join(',')}"`
58 | : 'undefined';
59 |
60 | return `player.${func}({listType: ${listType}, list: ${list}, playlist: ${playlist}, index: ${index}}); true;`;
61 | },
62 |
63 | loadVideoById: (videoId, play) => {
64 | const func = play ? 'loadVideoById' : 'cueVideoById';
65 |
66 | return `player.${func}({videoId: ${JSON.stringify(videoId)}}); true;`;
67 | },
68 | };
69 |
70 | export const playMode = {
71 | [PLAY_MODE]: PLAYER_FUNCTIONS.playVideo,
72 | [PAUSE_MODE]: PLAYER_FUNCTIONS.pauseVideo,
73 | };
74 |
75 | export const soundMode = {
76 | [MUTE_MODE]: PLAYER_FUNCTIONS.muteVideo,
77 | [UNMUTE_MODE]: PLAYER_FUNCTIONS.unMuteVideo,
78 | };
79 |
80 | export const MAIN_SCRIPT = (
81 | videoId,
82 | playList,
83 | initialPlayerParams,
84 | allowWebViewZoom,
85 | contentScale,
86 | ) => {
87 | const {
88 | end,
89 | rel,
90 | color,
91 | start,
92 | playerLang,
93 | loop = false,
94 | cc_lang_pref,
95 | iv_load_policy,
96 | modestbranding,
97 | controls = true,
98 | showClosedCaptions,
99 | preventFullScreen = false,
100 | } = initialPlayerParams;
101 |
102 | // _s postfix to refer to "safe"
103 | const rel_s = rel ? 1 : 0;
104 | const loop_s = loop ? 1 : 0;
105 | const videoId_s = videoId || '';
106 | const controls_s = controls ? 1 : 0;
107 | const cc_lang_pref_s = cc_lang_pref || '';
108 | const modestbranding_s = modestbranding ? 1 : 0;
109 | const preventFullScreen_s = preventFullScreen ? 0 : 1;
110 | const showClosedCaptions_s = showClosedCaptions ? 1 : 0;
111 | const contentScale_s = typeof contentScale === 'number' ? contentScale : 1.0;
112 |
113 | const list = typeof playList === 'string' ? playList : undefined;
114 | const listType = typeof playList === 'string' ? 'playlist' : undefined;
115 | const playlist = Array.isArray(playList) ? playList.join(',') : undefined;
116 |
117 | // scale will either be "initial-scale=1.0"
118 | let scale = `initial-scale=${contentScale_s}`;
119 | if (!allowWebViewZoom) {
120 | // or "initial-scale=0.8, maximum-scale=1.0"
121 | scale += `, maximum-scale=${contentScale_s}`;
122 | }
123 |
124 | const safeData = {
125 | end,
126 | list,
127 | start,
128 | color,
129 | rel_s,
130 | loop_s,
131 | listType,
132 | playlist,
133 | videoId_s,
134 | controls_s,
135 | playerLang,
136 | iv_load_policy,
137 | contentScale_s,
138 | cc_lang_pref_s,
139 | allowWebViewZoom,
140 | modestbranding_s,
141 | preventFullScreen_s,
142 | showClosedCaptions_s,
143 | };
144 |
145 | const urlEncodedJSON = encodeURI(JSON.stringify(safeData));
146 |
147 | const listParam = list ? `list: '${list}',` : '';
148 | const listTypeParam = listType ? `listType: '${list}',` : '';
149 | const playlistParam = playList ? `playlist: '${playList}',` : '';
150 |
151 | const htmlString = `
152 |
153 |
154 |
155 |
159 |
177 |
178 |
179 |
182 |
183 |
285 |
286 |
287 | `;
288 |
289 | return {htmlString, urlEncodedJSON};
290 | };
291 |
--------------------------------------------------------------------------------
/src/WebView.native.js:
--------------------------------------------------------------------------------
1 | export {WebView} from 'react-native-webview';
2 |
--------------------------------------------------------------------------------
/src/WebView.web.js:
--------------------------------------------------------------------------------
1 | export {WebView} from 'react-native-web-webview';
2 |
--------------------------------------------------------------------------------
/src/YoutubeIframe.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | useRef,
3 | useMemo,
4 | useState,
5 | useEffect,
6 | forwardRef,
7 | useCallback,
8 | useImperativeHandle,
9 | } from 'react';
10 | import {Linking, Platform, StyleSheet, View} from 'react-native';
11 | import {EventEmitter} from 'events';
12 | import {WebView} from './WebView';
13 | import {
14 | PLAYER_ERROR,
15 | PLAYER_STATES,
16 | DEFAULT_BASE_URL,
17 | CUSTOM_USER_AGENT,
18 | } from './constants';
19 | import {MAIN_SCRIPT, PLAYER_FUNCTIONS} from './PlayerScripts';
20 | import {deepComparePlayList} from './utils';
21 |
22 | const YoutubeIframe = (props, ref) => {
23 | const {
24 | height,
25 | width,
26 | videoId,
27 | playList,
28 | play = false,
29 | mute = false,
30 | volume = 100,
31 | viewContainerStyle,
32 | webViewStyle,
33 | webViewProps,
34 | useLocalHTML,
35 | baseUrlOverride,
36 | playbackRate = 1,
37 | contentScale = 1.0,
38 | onError = _err => {},
39 | onReady = _event => {},
40 | playListStartIndex = 0,
41 | initialPlayerParams,
42 | allowWebViewZoom = false,
43 | forceAndroidAutoplay = false,
44 | onChangeState = _event => {},
45 | onFullScreenChange = _status => {},
46 | onPlaybackQualityChange = _quality => {},
47 | onPlaybackRateChange = _playbackRate => {},
48 | } = props;
49 |
50 | const [playerReady, setPlayerReady] = useState(false);
51 | const lastVideoIdRef = useRef(videoId);
52 | const lastPlayListRef = useRef(playList);
53 | const initialPlayerParamsRef = useRef(initialPlayerParams || {});
54 |
55 | const webViewRef = useRef(null);
56 | const eventEmitter = useRef(new EventEmitter());
57 |
58 | const sendPostMessage = useCallback(
59 | (eventName, meta) => {
60 | if (!playerReady) {
61 | return;
62 | }
63 |
64 | const message = JSON.stringify({eventName, meta});
65 | webViewRef.current.postMessage(message);
66 | },
67 | [playerReady],
68 | );
69 |
70 | useImperativeHandle(
71 | ref,
72 | () => ({
73 | getVideoUrl: () => {
74 | webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.getVideoUrlScript);
75 | return new Promise(resolve => {
76 | eventEmitter.current.once('getVideoUrl', resolve);
77 | });
78 | },
79 | getDuration: () => {
80 | webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.durationScript);
81 | return new Promise(resolve => {
82 | eventEmitter.current.once('getDuration', resolve);
83 | });
84 | },
85 | getCurrentTime: () => {
86 | webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.currentTimeScript);
87 | return new Promise(resolve => {
88 | eventEmitter.current.once('getCurrentTime', resolve);
89 | });
90 | },
91 | isMuted: () => {
92 | webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.isMutedScript);
93 | return new Promise(resolve => {
94 | eventEmitter.current.once('isMuted', resolve);
95 | });
96 | },
97 | getVolume: () => {
98 | webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.getVolumeScript);
99 | return new Promise(resolve => {
100 | eventEmitter.current.once('getVolume', resolve);
101 | });
102 | },
103 | getPlaybackRate: () => {
104 | webViewRef.current.injectJavaScript(
105 | PLAYER_FUNCTIONS.getPlaybackRateScript,
106 | );
107 | return new Promise(resolve => {
108 | eventEmitter.current.once('getPlaybackRate', resolve);
109 | });
110 | },
111 | getAvailablePlaybackRates: () => {
112 | webViewRef.current.injectJavaScript(
113 | PLAYER_FUNCTIONS.getAvailablePlaybackRatesScript,
114 | );
115 | return new Promise(resolve => {
116 | eventEmitter.current.once('getAvailablePlaybackRates', resolve);
117 | });
118 | },
119 | seekTo: (seconds, allowSeekAhead) => {
120 | webViewRef.current.injectJavaScript(
121 | PLAYER_FUNCTIONS.seekToScript(seconds, allowSeekAhead),
122 | );
123 | },
124 | }),
125 | [],
126 | );
127 |
128 | useEffect(() => {
129 | if (play) {
130 | sendPostMessage('playVideo', {});
131 | } else {
132 | sendPostMessage('pauseVideo', {});
133 | }
134 | }, [play, sendPostMessage]);
135 |
136 | useEffect(() => {
137 | if (mute) {
138 | sendPostMessage('muteVideo', {});
139 | } else {
140 | sendPostMessage('unMuteVideo', {});
141 | }
142 | }, [mute, sendPostMessage]);
143 |
144 | useEffect(() => {
145 | sendPostMessage('setVolume', {volume});
146 | }, [sendPostMessage, volume]);
147 |
148 | useEffect(() => {
149 | sendPostMessage('setPlaybackRate', {playbackRate});
150 | }, [sendPostMessage, playbackRate]);
151 |
152 | useEffect(() => {
153 | if (!playerReady || lastVideoIdRef.current === videoId) {
154 | // no instance of player is ready
155 | // or videoId has not changed
156 | return;
157 | }
158 |
159 | lastVideoIdRef.current = videoId;
160 |
161 | webViewRef.current.injectJavaScript(
162 | PLAYER_FUNCTIONS.loadVideoById(videoId, play),
163 | );
164 | }, [videoId, play, playerReady]);
165 |
166 | useEffect(() => {
167 | if (!playerReady) {
168 | // no instance of player is ready
169 | return;
170 | }
171 |
172 | // Also, right now, we are helping users by doing "deep" comparisons of playList prop,
173 | // but in the next major we should leave the responsibility to user (either via useMemo or moving the array outside)
174 | if (!playList || deepComparePlayList(lastPlayListRef.current, playList)) {
175 | return;
176 | }
177 |
178 | lastPlayListRef.current = playList;
179 |
180 | webViewRef.current.injectJavaScript(
181 | PLAYER_FUNCTIONS.loadPlaylist(playList, playListStartIndex, play),
182 | );
183 | }, [playList, play, playListStartIndex, playerReady]);
184 |
185 | const onWebMessage = useCallback(
186 | event => {
187 | try {
188 | const message = JSON.parse(event.nativeEvent.data);
189 |
190 | switch (message.eventType) {
191 | case 'fullScreenChange':
192 | onFullScreenChange(message.data);
193 | break;
194 | case 'playerStateChange':
195 | onChangeState(PLAYER_STATES[message.data]);
196 | break;
197 | case 'playerReady':
198 | onReady();
199 | setPlayerReady(true);
200 | break;
201 | case 'playerQualityChange':
202 | onPlaybackQualityChange(message.data);
203 | break;
204 | case 'playerError':
205 | onError(PLAYER_ERROR[message.data]);
206 | break;
207 | case 'playbackRateChange':
208 | onPlaybackRateChange(message.data);
209 | break;
210 | default:
211 | eventEmitter.current.emit(message.eventType, message.data);
212 | break;
213 | }
214 | } catch (error) {
215 | console.warn('[rn-youtube-iframe]', error);
216 | }
217 | },
218 | [
219 | onReady,
220 | onError,
221 | onChangeState,
222 | onFullScreenChange,
223 | onPlaybackRateChange,
224 | onPlaybackQualityChange,
225 | ],
226 | );
227 |
228 | const onShouldStartLoadWithRequest = useCallback(
229 | request => {
230 | try {
231 | const url = request.mainDocumentURL || request.url;
232 | if (Platform.OS === 'ios') {
233 | const iosFirstLoad = url === 'about:blank';
234 | if (iosFirstLoad) {
235 | return true;
236 | }
237 | const isYouTubeLink = url.startsWith('https://www.youtube.com/');
238 | if (isYouTubeLink) {
239 | Linking.openURL(url).catch(error => {
240 | console.warn('Error opening URL:', error);
241 | });
242 | return false;
243 | }
244 | }
245 | return url.startsWith(baseUrlOverride || DEFAULT_BASE_URL);
246 | } catch (error) {
247 | // defaults to true in case of error
248 | // returning false stops the video from loading
249 | return true;
250 | }
251 | },
252 | [baseUrlOverride],
253 | );
254 |
255 | const source = useMemo(() => {
256 | const ytScript = MAIN_SCRIPT(
257 | lastVideoIdRef.current,
258 | lastPlayListRef.current,
259 | initialPlayerParamsRef.current,
260 | allowWebViewZoom,
261 | contentScale,
262 | );
263 |
264 | if (useLocalHTML) {
265 | const res = {html: ytScript.htmlString};
266 | if (baseUrlOverride) {
267 | res.baseUrl = baseUrlOverride;
268 | }
269 | return res;
270 | }
271 |
272 | const base = baseUrlOverride || DEFAULT_BASE_URL;
273 | const data = ytScript.urlEncodedJSON;
274 |
275 | return {uri: base + '?data=' + data};
276 | }, [useLocalHTML, contentScale, baseUrlOverride, allowWebViewZoom]);
277 |
278 | return (
279 |
280 |
306 |
307 | );
308 | };
309 |
310 | const styles = StyleSheet.create({
311 | webView: {backgroundColor: 'transparent'},
312 | });
313 |
314 | export default forwardRef(YoutubeIframe);
315 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const PLAY_MODE = true;
2 | export const PAUSE_MODE = false;
3 | export const MUTE_MODE = true;
4 | export const UNMUTE_MODE = false;
5 |
6 | export const PLAYER_STATES_NAMES = {
7 | UNSTARTED: 'unstarted',
8 | ENDED: 'ended',
9 | PLAYING: 'playing',
10 | PAUSED: 'paused',
11 | BUFFERING: 'buffering',
12 | VIDEO_CUED: 'video cued',
13 | };
14 |
15 | export const PLAYER_STATES = {
16 | '-1': PLAYER_STATES_NAMES.UNSTARTED,
17 | 0: PLAYER_STATES_NAMES.ENDED,
18 | 1: PLAYER_STATES_NAMES.PLAYING,
19 | 2: PLAYER_STATES_NAMES.PAUSED,
20 | 3: PLAYER_STATES_NAMES.BUFFERING,
21 | 5: PLAYER_STATES_NAMES.VIDEO_CUED,
22 | };
23 |
24 | export const PLAYER_ERROR_NAMES = {
25 | INVALID_PARAMETER: 'invalid_parameter',
26 | HTML5_ERROR: 'HTML5_error',
27 | VIDEO_NOT_FOUND: 'video_not_found',
28 | EMBED_NOT_ALLOWED: 'embed_not_allowed',
29 | };
30 |
31 | export const PLAYER_ERROR = {
32 | 2: PLAYER_ERROR_NAMES.INVALID_PARAMETER,
33 | 5: PLAYER_ERROR_NAMES.HTML5_ERROR,
34 | 100: PLAYER_ERROR_NAMES.VIDEO_NOT_FOUND,
35 | 101: PLAYER_ERROR_NAMES.EMBED_NOT_ALLOWED,
36 | 150: PLAYER_ERROR_NAMES.EMBED_NOT_ALLOWED,
37 | };
38 |
39 | export const CUSTOM_USER_AGENT =
40 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36';
41 |
42 | export const DEFAULT_BASE_URL =
43 | 'https://lonelycpp.github.io/react-native-youtube-iframe/iframe_v2.html';
44 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import YoutubeIframe from './YoutubeIframe';
2 | import {getYoutubeMeta} from './oEmbed';
3 | import {
4 | PLAYER_STATES_NAMES as PLAYER_STATES,
5 | PLAYER_ERROR_NAMES as PLAYER_ERRORS,
6 | } from './constants';
7 |
8 | export default YoutubeIframe;
9 | export {getYoutubeMeta, PLAYER_STATES, PLAYER_ERRORS};
10 |
--------------------------------------------------------------------------------
/src/oEmbed.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} youtubeMeta
3 | * @property {String} author_name
4 | * @property {String} author_url
5 | * @property {Number} height
6 | * @property {String} html
7 | * @property {String} provider_name
8 | * @property {String} provider_url
9 | * @property {Number} thumbnail_height
10 | * @property {String} thumbnail_url
11 | * @property {Number} thumbnail_width
12 | * @property {String} title
13 | * @property {String} type
14 | * @property {String} version
15 | * @property {Number} width
16 | */
17 |
18 | /**
19 | * Fetch metadata of a youtube video using the oEmbed Spec -
20 | * https://oembed.com/#section7
21 | *
22 | * metadata -
23 | *
24 | * `thumbnail_url` - The url of the resource provider.
25 | *
26 | * `thumbnail_width` - The width of the video thumbnail.
27 | *
28 | * `thumbnail_height` - The height of the video thumbnail.
29 | *
30 | * `height` - The height in pixels required to display the HTML.
31 | *
32 | * `width` - The width in pixels required to display the HTML.
33 | *
34 | * `html` - The HTML required to embed a video player.
35 | *
36 | * `author_url` - youtube channel link of the video
37 | *
38 | * `title` - youtube video title
39 | *
40 | * `provider_name` - The name of the resource provider.
41 | *
42 | * `author_name` - The name of the author/owner of the video.
43 | *
44 | * `provider_url` - The url of the resource provider.
45 | *
46 | * `version` - The oEmbed version number.
47 | *
48 | * `type` - The resource type.
49 | *
50 | *
51 | *
52 | * @param {String} videoId - youtube video id
53 | * @returns {Promise} A promise that resolves into an object with the video metadata
54 | */
55 | export const getYoutubeMeta = async videoId => {
56 | const response = await fetch(
57 | `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`,
58 | );
59 | return await response.json();
60 | };
61 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * deep compares two values for the "playlist prop"
3 | *
4 | * @param {string | string[]} lastPlayList
5 | * @param {string | string[]} playList
6 | * @returns true if the two are equal
7 | */
8 | const deepComparePlayList = (lastPlayList, playList) => {
9 | if (lastPlayList === playList) {
10 | return true;
11 | }
12 |
13 | if (Array.isArray(lastPlayList) && Array.isArray(playList)) {
14 | return lastPlayList.join('') === playList.join('');
15 | }
16 |
17 | return false;
18 | };
19 |
20 | export {deepComparePlayList};
21 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | .docusaurus/
3 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | ```
30 | $ GIT_USER=lonelycpp CURRENT_BRANCH= USE_SSH=true yarn deploy
31 | ```
32 |
33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
34 |
--------------------------------------------------------------------------------
/website/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/website/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'React Native Youtube iframe',
3 | tagline:
4 | 'A simple, light weight wrapper around the youtube iframe javascript API for react native',
5 | url: 'https://lonelycpp.github.io',
6 | baseUrl: '/react-native-youtube-iframe/',
7 | onBrokenLinks: 'throw',
8 | favicon: 'img/favicon.ico',
9 | organizationName: 'LonelyCpp',
10 | projectName: 'react-native-youtube-iframe',
11 | themeConfig: {
12 | sidebarCollapsible: false,
13 | colorMode: {
14 | defaultMode: 'dark',
15 | },
16 | navbar: {
17 | title: 'React Native Youtube-iframe',
18 | logo: {
19 | alt: 'React Native Youtube-iframe Logo',
20 | src: 'img/logo.svg',
21 | },
22 | items: [
23 | {
24 | href: 'https://github.com/LonelyCpp/react-native-youtube-iframe',
25 | label: 'GitHub',
26 | position: 'right',
27 | },
28 | ],
29 | },
30 | googleAnalytics: {
31 | trackingID: 'UA-165995640-2',
32 | },
33 | },
34 | presets: [
35 | [
36 | '@docusaurus/preset-classic',
37 | {
38 | docs: {
39 | path: '../docs',
40 | routeBasePath: '/',
41 | sidebarPath: require.resolve('./sidebars.js'),
42 | },
43 | theme: {
44 | customCss: require.resolve('./src/css/custom.css'),
45 | },
46 | },
47 | ],
48 | ],
49 | };
50 |
--------------------------------------------------------------------------------
/website/generateIframe.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const minify = require('html-minifier').minify;
3 |
4 | const iframeContent = fs.readFileSync('../iframe.html').toString();
5 |
6 | const minified = minify(iframeContent, {
7 | minifyJS: true,
8 | minifyCSS: true,
9 | removeComments: true,
10 | useShortDoctype: true,
11 | removeOptionalTags: true,
12 | collapseWhitespace: true,
13 | removeTagWhitespace: true,
14 | });
15 |
16 | fs.writeFileSync('./static/iframe_v2.html', minified);
17 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-youtube-iframe",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "serve": "docusaurus serve",
8 | "deploy": "docusaurus deploy",
9 | "swizzle": "docusaurus swizzle",
10 | "genIframe": "node generateIframe.js",
11 | "start": "node generateIframe.js;docusaurus start",
12 | "build": "node generateIframe.js;docusaurus build"
13 | },
14 | "dependencies": {
15 | "@docusaurus/core": "^2.0.0-beta.3",
16 | "@docusaurus/preset-classic": "^2.0.0-beta.3",
17 | "@mdx-js/react": "^1.6.22",
18 | "clsx": "^1.1.1",
19 | "html-minifier": "^4.0.0",
20 | "react": "^17.0.2",
21 | "react-dom": "^17.0.2"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | },
35 | "volta": {
36 | "node": "12.22.12"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/website/sidebars.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sideBar: {
3 | About: ['about', 'install', 'basic-usage'],
4 | Documentation: [
5 | 'component-props',
6 | 'component-ref-methods',
7 | 'module-methods',
8 | 'self-host-remote-source',
9 | ],
10 | FAQ: [
11 | 'play-store-compatibility',
12 | 'remove-context-share',
13 | 'elapsed-time',
14 | 'navigation-crash',
15 | ],
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/website/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * Any CSS included here will be global. The classic template
4 | * bundles Infima by default. Infima is a CSS framework designed to
5 | * work well for content-centric websites.
6 | */
7 |
8 | /* You can override the default Infima variables here. */
9 | :root {
10 | --ifm-color-primary: #ff0000;
11 | --ifm-color-primary-dark: #e60000;
12 | --ifm-color-primary-darker: #d90000;
13 | --ifm-color-primary-darkest: #b30000;
14 | --ifm-color-primary-light: #ff1a1a;
15 | --ifm-color-primary-lighter: #ff2626;
16 | --ifm-color-primary-lightest: #ff4d4d;
17 | --ifm-code-font-size: 95%;
18 | }
19 |
20 | .docusaurus-highlight-code-line {
21 | background-color: rgb(72, 77, 91);
22 | display: block;
23 | margin: 0 calc(-1 * var(--ifm-pre-padding));
24 | padding: 0 var(--ifm-pre-padding);
25 | }
26 |
--------------------------------------------------------------------------------
/website/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LonelyCpp/react-native-youtube-iframe/f1646c1b3c436e46702be310347228cd8f2b8699/website/static/.nojekyll
--------------------------------------------------------------------------------
/website/static/iframe.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/static/iframe_v2.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/static/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LonelyCpp/react-native-youtube-iframe/f1646c1b3c436e46702be310347228cd8f2b8699/website/static/img/demo.gif
--------------------------------------------------------------------------------
/website/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LonelyCpp/react-native-youtube-iframe/f1646c1b3c436e46702be310347228cd8f2b8699/website/static/img/favicon.ico
--------------------------------------------------------------------------------
/website/static/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------