Merge branch 'master' into master
This commit is contained in:
commit
fab55f4d5c
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
### Current behavior
|
||||||
|
Describe what happens when you encounter this issue.
|
||||||
|
|
||||||
|
### Reproduction steps
|
||||||
|
A 1, 2, 3, etc. list of what's needed to see the issue happen.
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
Describe what you wanted to happen
|
||||||
|
|
||||||
|
### Platform
|
||||||
|
Which player are you experiencing the problem on:
|
||||||
|
* iOS
|
||||||
|
* Android ExoPlayer
|
||||||
|
* Android MediaPlayer
|
||||||
|
* Windows UWP
|
||||||
|
* Windows WPF
|
||||||
|
|
||||||
|
### Video sample
|
||||||
|
If possible, include a link to the video that has the problem that can be streamed or downloaded from.
|
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
## Changelog
|
||||||
|
|
||||||
|
### Version 2.2.0
|
||||||
|
* Text track selection support for iOS & ExoPlayer [#1049](https://github.com/react-native-community/react-native-video/pull/1049)
|
||||||
|
* Support outputting to a TextureView on Android ExoPlayer [#1058](https://github.com/react-native-community/react-native-video/pull/1058)
|
||||||
|
* Support changing the left/right balance on Android MediaPlayer [#1051](https://github.com/react-native-community/react-native-video/pull/1051)
|
||||||
|
* Prevent multiple onEnd notifications on iOS [#832](https://github.com/react-native-community/react-native-video/pull/832)
|
||||||
|
* Fix doing a partial swipe on iOS causing a black screen [#1048](https://github.com/react-native-community/react-native-video/pull/1048)
|
||||||
|
* Fix crash when switching to a new source on iOS [#974](https://github.com/react-native-community/react-native-video/pull/974)
|
||||||
|
* Add cookie support for ExoPlayer [#922](https://github.com/react-native-community/react-native-video/pull/922)
|
||||||
|
* Remove ExoPlayer onMetadata that wasn't being used [#1040](https://github.com/react-native-community/react-native-video/pull/1040)
|
||||||
|
* Fix bug where setting the progress interval on iOS didn't work [#800](https://github.com/react-native-community/react-native-video/pull/800)
|
||||||
|
* Support setting the poster resize mode [#595](https://github.com/react-native-community/react-native-video/pull/595)
|
306
README.md
306
README.md
@ -5,11 +5,27 @@ A `<Video>` component for react-native, as seen in
|
|||||||
|
|
||||||
Requires react-native >= 0.40.0, for RN support of 0.19.0 - 0.39.0 please use a pre 1.0 version.
|
Requires react-native >= 0.40.0, for RN support of 0.19.0 - 0.39.0 please use a pre 1.0 version.
|
||||||
|
|
||||||
### Add it to your project
|
## TOC
|
||||||
|
|
||||||
Run `npm i -S react-native-video`
|
* [Installation](#installation)
|
||||||
|
* [Usage](#usage)
|
||||||
|
|
||||||
#### iOS
|
## Installation
|
||||||
|
|
||||||
|
Using npm:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install --save react-native-video
|
||||||
|
```
|
||||||
|
|
||||||
|
or using yarn:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn add react-native-video
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>iOS</summary>
|
||||||
|
|
||||||
Run `react-native link` to link the react-native-video library.
|
Run `react-native link` to link the react-native-video library.
|
||||||
|
|
||||||
@ -28,8 +44,10 @@ If you would like to allow other apps to play music over your video component, a
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
Note: you can also use the `ignoreSilentSwitch` prop, shown below.
|
Note: you can also use the `ignoreSilentSwitch` prop, shown below.
|
||||||
|
</details>
|
||||||
|
|
||||||
#### tvOS
|
<details>
|
||||||
|
<summary>tvOS</summary>
|
||||||
|
|
||||||
Run `react-native link` to link the react-native-video library.
|
Run `react-native link` to link the react-native-video library.
|
||||||
|
|
||||||
@ -50,10 +68,10 @@ Scroll to « Linked Frameworks and Libraries » and tap on the + button
|
|||||||
Select RCTVideo-tvOS
|
Select RCTVideo-tvOS
|
||||||
|
|
||||||
<img src="./docs/tvOS-step-4.jpg" width="40%">
|
<img src="./docs/tvOS-step-4.jpg" width="40%">
|
||||||
|
</details>
|
||||||
|
|
||||||
That’s all, you can use react-native-video for your tvOS application
|
<details>
|
||||||
|
<summary>Android</summary>
|
||||||
#### Android
|
|
||||||
|
|
||||||
Run `react-native link` to link the react-native-video library.
|
Run `react-native link` to link the react-native-video library.
|
||||||
|
|
||||||
@ -61,11 +79,21 @@ Or if you have trouble, make the following additions to the given files manually
|
|||||||
|
|
||||||
**android/settings.gradle**
|
**android/settings.gradle**
|
||||||
|
|
||||||
|
The newer ExoPlayer library will work for most people.
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
include ':react-native-video'
|
||||||
|
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need to use the old Android MediaPlayer based player, use the following instead:
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
include ':react-native-video'
|
include ':react-native-video'
|
||||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
**android/app/build.gradle**
|
**android/app/build.gradle**
|
||||||
|
|
||||||
```gradle
|
```gradle
|
||||||
@ -94,8 +122,10 @@ protected List<ReactPackage> getPackages() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
#### Windows
|
<details>
|
||||||
|
<summary>Windows</summary>
|
||||||
|
|
||||||
Make the following additions to the given files manually:
|
Make the following additions to the given files manually:
|
||||||
|
|
||||||
@ -104,19 +134,17 @@ Make the following additions to the given files manually:
|
|||||||
Add the `ReactNativeVideo` project to your solution.
|
Add the `ReactNativeVideo` project to your solution.
|
||||||
|
|
||||||
1. Open the solution in Visual Studio 2015
|
1. Open the solution in Visual Studio 2015
|
||||||
2. Right-click Solution icon in Solution Explorer > Add > Existing Project...
|
2. Right-click Solution icon in Solution Explorer > Add > Existing Project
|
||||||
3.
|
* UWP: Select `node_modules\react-native-video\windows\ReactNativeVideo\ReactNativeVideo.csproj`
|
||||||
UWP: Select `node_modules\react-native-video\windows\ReactNativeVideo\ReactNativeVideo.csproj`
|
* WPF: Select `node_modules\react-native-video\windows\ReactNativeVideo.Net46\ReactNativeVideo.Net46.csproj`
|
||||||
WPF: Select `node_modules\react-native-video\windows\ReactNativeVideo.Net46\ReactNativeVideo.Net46.csproj`
|
|
||||||
|
|
||||||
**windows/myapp/myapp.csproj**
|
**windows/myapp/myapp.csproj**
|
||||||
|
|
||||||
Add a reference to `ReactNativeVideo` to your main application project. From Visual Studio 2015:
|
Add a reference to `ReactNativeVideo` to your main application project. From Visual Studio 2015:
|
||||||
|
|
||||||
1. Right-click main application project > Add > Reference...
|
1. Right-click main application project > Add > Reference...
|
||||||
2.
|
* UWP: Check `ReactNativeVideo` from Solution Projects.
|
||||||
UWP: Check `ReactNativeVideo` from Solution Projects.
|
* WPF: Check `ReactNativeVideo.Net46` from Solution Projects.
|
||||||
WPF: Check `ReactNativeVideo.Net46` from Solution Projects.
|
|
||||||
|
|
||||||
**MainPage.cs**
|
**MainPage.cs**
|
||||||
|
|
||||||
@ -143,6 +171,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -155,28 +184,25 @@ using System.Collections.Generic;
|
|||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
this.player = ref
|
this.player = ref
|
||||||
}} // Store reference
|
}} // Store reference
|
||||||
rate={1.0} // 0 is paused, 1 is normal.
|
onBuffer={this.onBuffer} // Callback when remote video is buffering
|
||||||
volume={1.0} // 0 is muted, 1 is normal.
|
onEnd={this.onEnd} // Callback when playback finishes
|
||||||
muted={false} // Mutes the audio entirely.
|
onError={this.videoError} // Callback when video cannot be loaded
|
||||||
paused={false} // Pauses playback entirely.
|
onFullscreenPlayerWillPresent={this.fullScreenPlayerWillPresent} // Callback before fullscreen starts
|
||||||
resizeMode="cover" // Fill the whole screen at aspect ratio.*
|
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
|
||||||
repeat={true} // Repeat forever.
|
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
|
||||||
playInBackground={false} // Audio continues to play when app entering background.
|
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped
|
||||||
playWhenInactive={false} // [iOS] Video continues to play when control or notification center are shown.
|
|
||||||
ignoreSilentSwitch={"ignore"} // [iOS] ignore | obey - When 'ignore', audio will still play with the iOS hard silent switch set to silent. When 'obey', audio will toggle with the switch. When not specified, will inherit audio settings as usual.
|
|
||||||
progressUpdateInterval={250.0} // [iOS] Interval to fire onProgress (default to ~250ms)
|
|
||||||
onLoadStart={this.loadStart} // Callback when video starts to load
|
onLoadStart={this.loadStart} // Callback when video starts to load
|
||||||
onLoad={this.setDuration} // Callback when video loads
|
onLoad={this.setDuration} // Callback when video loads
|
||||||
onProgress={this.setTime} // Callback every ~250ms with currentTime
|
onProgress={this.setTime} // Callback every ~250ms with currentTime
|
||||||
onEnd={this.onEnd} // Callback when playback finishes
|
|
||||||
onError={this.videoError} // Callback when video cannot be loaded
|
|
||||||
onBuffer={this.onBuffer} // Callback when remote video is buffering
|
|
||||||
onTimedMetadata={this.onTimedMetadata} // Callback when the stream receive some metadata
|
onTimedMetadata={this.onTimedMetadata} // Callback when the stream receive some metadata
|
||||||
style={styles.backgroundVideo} />
|
style={styles.backgroundVideo} />
|
||||||
|
|
||||||
// Later to trigger fullscreen
|
// Later to trigger fullscreen
|
||||||
this.player.presentFullscreenPlayer()
|
this.player.presentFullscreenPlayer()
|
||||||
|
|
||||||
|
// Disable fullscreen
|
||||||
|
this.player.dismissFullscreenPlayer()
|
||||||
|
|
||||||
// To set video position in seconds (seek)
|
// To set video position in seconds (seek)
|
||||||
this.player.seek(0)
|
this.player.seek(0)
|
||||||
|
|
||||||
@ -192,38 +218,193 @@ var styles = StyleSheet.create({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
- * *For iOS you also need to specify muted for this to work*
|
### Configurable props
|
||||||
|
* [ignoreSilentSwitch](#ignoresilentswitch)
|
||||||
|
* [muted](#muted)
|
||||||
|
* [paused](#paused)
|
||||||
|
* [playInBackground](#playinbackground)
|
||||||
|
* [playWhenInactive](#playwheninactive)
|
||||||
|
* [poster](#poster)
|
||||||
|
* [posterResizeMode](#posterresizemode)
|
||||||
|
* [progressUpdateInterval](#progressupdateinterval)
|
||||||
|
* [rate](#rate)
|
||||||
|
* [repeat](#repeat)
|
||||||
|
* [resizeMode](#resizemode)
|
||||||
|
* [selectedTextTrack](#selectedtexttrack)
|
||||||
|
* [stereoPan](#stereopan)
|
||||||
|
* [useTextureView](#usetextureview)
|
||||||
|
* [volume](#volume)
|
||||||
|
|
||||||
## Android Expansion File Usage
|
#### ignoreSilentSwitch
|
||||||
|
Controls the iOS silent switch behavior
|
||||||
|
* **"inherit" (default)** - Use the default AVPlayer behavior
|
||||||
|
* **"ignore"** - Play audio even if the silent switch is set
|
||||||
|
* **"obey"** - Don't play audio if the silent switch is set
|
||||||
|
|
||||||
```javascript
|
Platforms: iOS
|
||||||
// Within your render function, assuming you have a file called
|
|
||||||
// "background.mp4" in your expansion file. Just add your main and (if applicable) patch version
|
#### muted
|
||||||
<Video source={{uri: "background", mainVer: 1, patchVer: 0}} // Looks for .mp4 file (background.mp4) in the given expansion version.
|
Controls whether the audio is muted
|
||||||
rate={1.0} // 0 is paused, 1 is normal.
|
* **false (default)** - Don't mute audio
|
||||||
volume={1.0} // 0 is muted, 1 is normal.
|
* **true** - Mute audio
|
||||||
muted={false} // Mutes the audio entirely.
|
|
||||||
paused={false} // Pauses playback entirely.
|
Platforms: all
|
||||||
resizeMode="cover" // Fill the whole screen at aspect ratio.
|
|
||||||
repeat={true} // Repeat forever.
|
#### paused
|
||||||
onLoadStart={this.loadStart} // Callback when video starts to load
|
Controls whether the media is paused
|
||||||
onLoad={this.setDuration} // Callback when video loads
|
* **false (default)** - Pause the media
|
||||||
onProgress={this.setTime} // Callback every ~250ms with currentTime
|
* **true** - Don't pause the media
|
||||||
onEnd={this.onEnd} // Callback when playback finishes
|
|
||||||
onError={this.videoError} // Callback when video cannot be loaded
|
Platforms: all
|
||||||
style={styles.backgroundVideo} />
|
|
||||||
|
#### playInBackground
|
||||||
|
Determine whether the media should continue playing while the app is in the background. This allows customers to continue listening to the audio.
|
||||||
|
* **false (default)** - Don't continue playing the media
|
||||||
|
* **true** - Continue playing the media
|
||||||
|
|
||||||
|
To use this feature on iOS, you must:
|
||||||
|
* [Enable Background Audio](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionBasics/AudioSessionBasics.html#//apple_ref/doc/uid/TP40007875-CH3-SW3) in your Xcode project
|
||||||
|
* Set the ignoreSilentSwitch prop to "ignore"
|
||||||
|
|
||||||
|
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||||
|
|
||||||
|
#### playWhenInactive
|
||||||
|
Determine whether the media should continue playing when notifications or the Control Center are in front of the video.
|
||||||
|
* **false (default)** - Don't continue playing the media
|
||||||
|
* **true** - Continue playing the media
|
||||||
|
|
||||||
|
Platforms: iOS
|
||||||
|
|
||||||
|
#### poster
|
||||||
|
An image to display while the video is loading
|
||||||
|
<br>Value: string with a URL for the poster, e.g. "https://baconmockup.com/300/200/"
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
#### posterResizeMode
|
||||||
|
Determines how to resize the poster image when the frame doesn't match the raw video dimensions.
|
||||||
|
* **"contain" (default)** - Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or less than the corresponding dimension of the view (minus padding).
|
||||||
|
* **"center"** - Center the image in the view along both dimensions. If the image is larger than the view, scale it down uniformly so that it is contained in the view.
|
||||||
|
* **"cover"** - Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding).
|
||||||
|
* **"none"** - Don't apply resize
|
||||||
|
* **"repeat"** - Repeat the image to cover the frame of the view. The image will keep its size and aspect ratio. (iOS only)
|
||||||
|
* **"stretch"** - Scale width and height independently, This may change the aspect ratio of the src.
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
#### progressUpdateInterval
|
||||||
|
Delay in milliseconds between onProgress events in milliseconds.
|
||||||
|
|
||||||
|
Default: 250.0
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
### rate
|
||||||
|
Speed at which the media should play.
|
||||||
|
* **0.0** - Pauses the video
|
||||||
|
* **1.0** - Play at normal speed
|
||||||
|
* **Other values** - Slow down or speed up playback
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
Note: For Android MediaPlayer, rate is only supported on Android 6.0 and higher devices.
|
||||||
|
|
||||||
|
#### repeat
|
||||||
|
Determine whether to repeat the video when the end is reached
|
||||||
|
* **false (default)** - Don't repeat the video
|
||||||
|
* **true** - Repeat the video
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
#### resizeMode
|
||||||
|
Determines how to resize the video when the frame doesn't match the raw video dimensions.
|
||||||
|
* **"none" (default)** - Don't apply resize
|
||||||
|
* **"contain"** - Scale the video uniformly (maintain the video's aspect ratio) so that both dimensions (width and height) of the video will be equal to or less than the corresponding dimension of the view (minus padding).
|
||||||
|
* **"cover"** - Scale the video uniformly (maintain the video's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding).
|
||||||
|
* **"stretch"** - Scale width and height independently, This may change the aspect ratio of the src.
|
||||||
|
|
||||||
|
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP
|
||||||
|
|
||||||
|
#### selectedTextTrack
|
||||||
|
Configure which text track (caption or subtitle), if any, is shown.
|
||||||
|
|
||||||
// Later on in your styles..
|
|
||||||
var styles = Stylesheet.create({
|
|
||||||
backgroundVideo: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
selectedTextTrack={{
|
||||||
|
type: Type,
|
||||||
|
value: Value
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
selectedTextTrack={{
|
||||||
|
type: "title",
|
||||||
|
value: "English Subtitles"
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Type | Value | Description
|
||||||
|
--- | --- | ---
|
||||||
|
"system" (default) | N/A | Display captions only if the system preference for captions is enabled
|
||||||
|
"disabled" | N/A | Don't display a text track
|
||||||
|
"title" | string | Display the text track with the title specified as the Value, e.g. "French 1"
|
||||||
|
"language" | string | Display the text track with the language specified as the Value, e.g. "fr"
|
||||||
|
"index" | number | Display the text track with the index specified as the value, e.g. 0
|
||||||
|
|
||||||
|
Both iOS & Android offer Settings to enable Captions for hearing impaired people. If "system" is selected and the Captions Setting is enabled, iOS/Android will look for a caption that matches that customer's language and display it.
|
||||||
|
|
||||||
|
If a track matching the specified Type (and Value if appropriate) is unavailable, no text track will be displayed. If multiple tracks match the criteria, the first match will be used.
|
||||||
|
|
||||||
|
Platforms: Android ExoPlayer, iOS
|
||||||
|
|
||||||
|
#### stereoPan
|
||||||
|
Adjust the balance of the left and right audio channels. Any value between –1.0 and 1.0 is accepted.
|
||||||
|
* **-1.0** - Full left
|
||||||
|
* **0.0 (default)** - Center
|
||||||
|
* **1.0** - Full right
|
||||||
|
|
||||||
|
Platforms: Android MediaPlayer
|
||||||
|
|
||||||
|
#### useTextureView
|
||||||
|
Output to a TextureView instead of the default SurfaceView. In general, you will want to use SurfaceView because it is more efficient and provides better performance. However, SurfaceViews has two limitations:
|
||||||
|
* It can't be animated, transformed or scaled
|
||||||
|
* You can't overlay multiple SurfaceViews
|
||||||
|
|
||||||
|
useTextureView can only be set at same time you're setting the source.
|
||||||
|
|
||||||
|
* **false (default)** - Use a SurfaceView
|
||||||
|
* **true** - Use a TextureView
|
||||||
|
|
||||||
|
Platforms: Android ExoPlayer
|
||||||
|
|
||||||
|
#### volume
|
||||||
|
Adjust the volume.
|
||||||
|
* **1.0 (default)** - Play at full volume
|
||||||
|
* **0.0** - Mute the audio
|
||||||
|
* **Other values** - Reduce volume
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
### Additional props
|
||||||
|
|
||||||
|
To see the full list of available props, you can check the [propTypes](https://github.com/react-native-community/react-native-video/blob/master/Video.js#L246) of the Video.js component.
|
||||||
|
|
||||||
|
- By default, iOS 9+ will only load encrypted HTTPS urls. If you need to load content from a webserver that only supports HTTP, you will need to modify your Info.plist file and add the following entry:
|
||||||
|
|
||||||
|
<img src="./docs/AppTransportSecuritySetting.png" width="50%">
|
||||||
|
|
||||||
|
For more detailed info check this [article](https://cocoacasts.com/how-to-add-app-transport-security-exception-domains)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Android Expansion File Usage
|
||||||
|
Within your render function, assuming you have a file called
|
||||||
|
"background.mp4" in your expansion file. Just add your main and (if applicable) patch version
|
||||||
|
```
|
||||||
|
<Video
|
||||||
|
source={{uri: "background", mainVer: 1, patchVer: 0}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
This will look for an .mp4 file (background.mp4) in the given expansion version.
|
||||||
|
|
||||||
### Load files with the RN Asset System
|
### Load files with the RN Asset System
|
||||||
|
|
||||||
@ -231,10 +412,7 @@ The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-nati
|
|||||||
|
|
||||||
```
|
```
|
||||||
<Video
|
<Video
|
||||||
repeat
|
|
||||||
resizeMode='cover'
|
|
||||||
source={require('../assets/video/turntable.mp4')}
|
source={require('../assets/video/turntable.mp4')}
|
||||||
style={styles.backgroundVideo}
|
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -242,16 +420,6 @@ The asset system [introduced in RN `0.14`](http://www.reactnative.com/react-nati
|
|||||||
|
|
||||||
To enable audio to play in background on iOS the audio session needs to be set to `AVAudioSessionCategoryPlayback`. See [Apple documentation][3] for additional details. (NOTE: there is now a ticket to [expose this as a prop]( https://github.com/react-native-community/react-native-video/issues/310) )
|
To enable audio to play in background on iOS the audio session needs to be set to `AVAudioSessionCategoryPlayback`. See [Apple documentation][3] for additional details. (NOTE: there is now a ticket to [expose this as a prop]( https://github.com/react-native-community/react-native-video/issues/310) )
|
||||||
|
|
||||||
## Static Methods
|
|
||||||
|
|
||||||
`seek(seconds)`
|
|
||||||
|
|
||||||
Seeks the video to the specified time (in seconds). Access using a ref to the component
|
|
||||||
|
|
||||||
`presentFullscreenPlayer()`
|
|
||||||
|
|
||||||
Toggles a fullscreen player. Access using a ref to the component.
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- See an [Example integration][1] in `react-native-login` *note that this example uses an older version of this library, before we used `export default` -- if you use `require` you will need to do `require('react-native-video').default` as per instructions above.*
|
- See an [Example integration][1] in `react-native-login` *note that this example uses an older version of this library, before we used `export default` -- if you use `require` you will need to do `require('react-native-video').default` as per instructions above.*
|
||||||
|
16
Video.js
16
Video.js
@ -1,6 +1,6 @@
|
|||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {StyleSheet, requireNativeComponent, NativeModules, View, Image} from 'react-native';
|
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image} from 'react-native';
|
||||||
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
||||||
import VideoResizeMode from './VideoResizeMode.js';
|
import VideoResizeMode from './VideoResizeMode.js';
|
||||||
|
|
||||||
@ -218,7 +218,7 @@ export default class Video extends Component {
|
|||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
resizeMode: 'contain',
|
resizeMode: this.props.posterResizeMode || 'contain'
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -272,10 +272,19 @@ Video.propTypes = {
|
|||||||
]),
|
]),
|
||||||
resizeMode: PropTypes.string,
|
resizeMode: PropTypes.string,
|
||||||
poster: PropTypes.string,
|
poster: PropTypes.string,
|
||||||
|
posterResizeMode: Image.propTypes.resizeMode,
|
||||||
repeat: PropTypes.bool,
|
repeat: PropTypes.bool,
|
||||||
|
selectedTextTrack: PropTypes.shape({
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number
|
||||||
|
])
|
||||||
|
}),
|
||||||
paused: PropTypes.bool,
|
paused: PropTypes.bool,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
|
stereoPan: PropTypes.number,
|
||||||
rate: PropTypes.number,
|
rate: PropTypes.number,
|
||||||
playInBackground: PropTypes.bool,
|
playInBackground: PropTypes.bool,
|
||||||
playWhenInactive: PropTypes.bool,
|
playWhenInactive: PropTypes.bool,
|
||||||
@ -284,6 +293,7 @@ Video.propTypes = {
|
|||||||
controls: PropTypes.bool,
|
controls: PropTypes.bool,
|
||||||
currentTime: PropTypes.number,
|
currentTime: PropTypes.number,
|
||||||
progressUpdateInterval: PropTypes.number,
|
progressUpdateInterval: PropTypes.number,
|
||||||
|
useTextureView: PropTypes.bool,
|
||||||
onLoadStart: PropTypes.func,
|
onLoadStart: PropTypes.func,
|
||||||
onLoad: PropTypes.func,
|
onLoad: PropTypes.func,
|
||||||
onBuffer: PropTypes.func,
|
onBuffer: PropTypes.func,
|
||||||
@ -308,7 +318,7 @@ Video.propTypes = {
|
|||||||
translateX: PropTypes.number,
|
translateX: PropTypes.number,
|
||||||
translateY: PropTypes.number,
|
translateY: PropTypes.number,
|
||||||
rotation: PropTypes.number,
|
rotation: PropTypes.number,
|
||||||
...View.propTypes,
|
...ViewPropTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RCTVideo = requireNativeComponent('RCTVideo', Video, {
|
const RCTVideo = requireNativeComponent('RCTVideo', Video, {
|
||||||
|
1
ViewPropTypes
Normal file
1
ViewPropTypes
Normal file
@ -0,0 +1 @@
|
|||||||
|
M Video.js
|
@ -12,9 +12,9 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
provided 'com.facebook.react:react-native:+'
|
provided 'com.facebook.react:react-native:+'
|
||||||
compile 'com.google.android.exoplayer:exoplayer:r2.4.0'
|
compile 'com.google.android.exoplayer:exoplayer:2.7.3'
|
||||||
compile('com.google.android.exoplayer:extension-okhttp:r2.4.0') {
|
compile('com.google.android.exoplayer:extension-okhttp:2.7.3') {
|
||||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||||
}
|
}
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.4.2'
|
compile 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package com.brentvatne.exoplayer;
|
package com.brentvatne.exoplayer;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactContext;
|
||||||
|
import com.facebook.react.modules.network.CookieJarContainer;
|
||||||
|
import com.facebook.react.modules.network.ForwardingCookieHandler;
|
||||||
import com.facebook.react.modules.network.OkHttpClientProvider;
|
import com.facebook.react.modules.network.OkHttpClientProvider;
|
||||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
|
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
@ -10,6 +14,10 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
|||||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.JavaNetCookieJar;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
public class DataSourceUtil {
|
public class DataSourceUtil {
|
||||||
|
|
||||||
private DataSourceUtil() {
|
private DataSourceUtil() {
|
||||||
@ -23,14 +31,14 @@ public class DataSourceUtil {
|
|||||||
DataSourceUtil.userAgent = userAgent;
|
DataSourceUtil.userAgent = userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUserAgent(Context context) {
|
public static String getUserAgent(ReactContext context) {
|
||||||
if (userAgent == null) {
|
if (userAgent == null) {
|
||||||
userAgent = Util.getUserAgent(context.getApplicationContext(), "ReactNativeVideo");
|
userAgent = Util.getUserAgent(context, "ReactNativeVideo");
|
||||||
}
|
}
|
||||||
return userAgent;
|
return userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DataSource.Factory getRawDataSourceFactory(Context context) {
|
public static DataSource.Factory getRawDataSourceFactory(ReactContext context) {
|
||||||
if (rawDataSourceFactory == null) {
|
if (rawDataSourceFactory == null) {
|
||||||
rawDataSourceFactory = buildRawDataSourceFactory(context);
|
rawDataSourceFactory = buildRawDataSourceFactory(context);
|
||||||
}
|
}
|
||||||
@ -41,7 +49,7 @@ public class DataSourceUtil {
|
|||||||
DataSourceUtil.rawDataSourceFactory = factory;
|
DataSourceUtil.rawDataSourceFactory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DataSource.Factory getDefaultDataSourceFactory(Context context, DefaultBandwidthMeter bandwidthMeter) {
|
public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
|
||||||
if (defaultDataSourceFactory == null) {
|
if (defaultDataSourceFactory == null) {
|
||||||
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter);
|
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter);
|
||||||
}
|
}
|
||||||
@ -52,18 +60,21 @@ public class DataSourceUtil {
|
|||||||
DataSourceUtil.defaultDataSourceFactory = factory;
|
DataSourceUtil.defaultDataSourceFactory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DataSource.Factory buildRawDataSourceFactory(Context context) {
|
private static DataSource.Factory buildRawDataSourceFactory(ReactContext context) {
|
||||||
return new RawResourceDataSourceFactory(context.getApplicationContext());
|
return new RawResourceDataSourceFactory(context.getApplicationContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DataSource.Factory buildDataSourceFactory(Context context, DefaultBandwidthMeter bandwidthMeter) {
|
private static DataSource.Factory buildDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
|
||||||
Context appContext = context.getApplicationContext();
|
return new DefaultDataSourceFactory(context, bandwidthMeter,
|
||||||
return new DefaultDataSourceFactory(appContext, bandwidthMeter,
|
buildHttpDataSourceFactory(context, bandwidthMeter));
|
||||||
buildHttpDataSourceFactory(appContext, bandwidthMeter));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpDataSource.Factory buildHttpDataSourceFactory(Context context, DefaultBandwidthMeter bandwidthMeter) {
|
private static HttpDataSource.Factory buildHttpDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter) {
|
||||||
return new OkHttpDataSourceFactory(OkHttpClientProvider.getOkHttpClient(), getUserAgent(context), bandwidthMeter);
|
OkHttpClient client = OkHttpClientProvider.getOkHttpClient();
|
||||||
|
CookieJarContainer container = (CookieJarContainer) client.cookieJar();
|
||||||
|
ForwardingCookieHandler handler = new ForwardingCookieHandler(context);
|
||||||
|
container.setCookieJar(new JavaNetCookieJar(handler));
|
||||||
|
return new OkHttpDataSourceFactory(client, getUserAgent(context), bandwidthMeter);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,6 @@ import com.google.android.exoplayer2.ExoPlayer;
|
|||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
|
||||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.TextRenderer;
|
import com.google.android.exoplayer2.text.TextRenderer;
|
||||||
@ -31,12 +29,16 @@ import java.util.List;
|
|||||||
@TargetApi(16)
|
@TargetApi(16)
|
||||||
public final class ExoPlayerView extends FrameLayout {
|
public final class ExoPlayerView extends FrameLayout {
|
||||||
|
|
||||||
private final View surfaceView;
|
private View surfaceView;
|
||||||
private final View shutterView;
|
private final View shutterView;
|
||||||
private final SubtitleView subtitleLayout;
|
private final SubtitleView subtitleLayout;
|
||||||
private final AspectRatioFrameLayout layout;
|
private final AspectRatioFrameLayout layout;
|
||||||
private final ComponentListener componentListener;
|
private final ComponentListener componentListener;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
|
private Context context;
|
||||||
|
private ViewGroup.LayoutParams layoutParams;
|
||||||
|
|
||||||
|
private boolean useTextureView = false;
|
||||||
|
|
||||||
public ExoPlayerView(Context context) {
|
public ExoPlayerView(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@ -49,9 +51,9 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
boolean useTextureView = false;
|
this.context = context;
|
||||||
|
|
||||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
layoutParams = new ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
|
||||||
@ -65,25 +67,45 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
layout.setLayoutParams(aspectRatioParams);
|
layout.setLayoutParams(aspectRatioParams);
|
||||||
|
|
||||||
shutterView = new View(getContext());
|
shutterView = new View(getContext());
|
||||||
shutterView.setLayoutParams(params);
|
shutterView.setLayoutParams(layoutParams);
|
||||||
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
|
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
|
||||||
|
|
||||||
subtitleLayout = new SubtitleView(context);
|
subtitleLayout = new SubtitleView(context);
|
||||||
subtitleLayout.setLayoutParams(params);
|
subtitleLayout.setLayoutParams(layoutParams);
|
||||||
subtitleLayout.setUserDefaultStyle();
|
subtitleLayout.setUserDefaultStyle();
|
||||||
subtitleLayout.setUserDefaultTextSize();
|
subtitleLayout.setUserDefaultTextSize();
|
||||||
|
|
||||||
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
|
updateSurfaceView();
|
||||||
view.setLayoutParams(params);
|
|
||||||
surfaceView = view;
|
|
||||||
|
|
||||||
layout.addView(surfaceView, 0, params);
|
layout.addView(shutterView, 1, layoutParams);
|
||||||
layout.addView(shutterView, 1, params);
|
layout.addView(subtitleLayout, 2, layoutParams);
|
||||||
layout.addView(subtitleLayout, 2, params);
|
|
||||||
|
|
||||||
addViewInLayout(layout, 0, aspectRatioParams);
|
addViewInLayout(layout, 0, aspectRatioParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setVideoView() {
|
||||||
|
if (surfaceView instanceof TextureView) {
|
||||||
|
player.setVideoTextureView((TextureView) surfaceView);
|
||||||
|
} else if (surfaceView instanceof SurfaceView) {
|
||||||
|
player.setVideoSurfaceView((SurfaceView) surfaceView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSurfaceView() {
|
||||||
|
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
|
||||||
|
view.setLayoutParams(layoutParams);
|
||||||
|
|
||||||
|
surfaceView = view;
|
||||||
|
if (layout.getChildAt(0) != null) {
|
||||||
|
layout.removeViewAt(0);
|
||||||
|
}
|
||||||
|
layout.addView(surfaceView, 0, layoutParams);
|
||||||
|
|
||||||
|
if (this.player != null) {
|
||||||
|
setVideoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
|
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
|
||||||
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
|
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
|
||||||
@ -100,20 +122,14 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
this.player.setVideoListener(null);
|
this.player.setVideoListener(null);
|
||||||
this.player.removeListener(componentListener);
|
this.player.removeListener(componentListener);
|
||||||
this.player.setVideoSurface(null);
|
this.player.setVideoSurface(null);
|
||||||
this.player.setMetadataOutput(componentListener);
|
|
||||||
}
|
}
|
||||||
this.player = player;
|
this.player = player;
|
||||||
shutterView.setVisibility(VISIBLE);
|
shutterView.setVisibility(VISIBLE);
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
if (surfaceView instanceof TextureView) {
|
setVideoView();
|
||||||
player.setVideoTextureView((TextureView) surfaceView);
|
|
||||||
} else if (surfaceView instanceof SurfaceView) {
|
|
||||||
player.setVideoSurfaceView((SurfaceView) surfaceView);
|
|
||||||
}
|
|
||||||
player.setVideoListener(componentListener);
|
player.setVideoListener(componentListener);
|
||||||
player.addListener(componentListener);
|
player.addListener(componentListener);
|
||||||
player.setTextOutput(componentListener);
|
player.setTextOutput(componentListener);
|
||||||
player.setMetadataOutput(componentListener);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +156,11 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
return surfaceView;
|
return surfaceView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseTextureView(boolean useTextureView) {
|
||||||
|
this.useTextureView = useTextureView;
|
||||||
|
updateSurfaceView();
|
||||||
|
}
|
||||||
|
|
||||||
private final Runnable measureAndLayout = new Runnable() {
|
private final Runnable measureAndLayout = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@ -167,7 +188,7 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class ComponentListener implements SimpleExoPlayer.VideoListener,
|
private final class ComponentListener implements SimpleExoPlayer.VideoListener,
|
||||||
TextRenderer.Output, ExoPlayer.EventListener, MetadataRenderer.Output {
|
TextRenderer.Output, ExoPlayer.EventListener {
|
||||||
|
|
||||||
// TextRenderer.Output implementation
|
// TextRenderer.Output implementation
|
||||||
|
|
||||||
@ -212,12 +233,12 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity() {
|
public void onPositionDiscontinuity(int reason) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,8 +253,18 @@ public final class ExoPlayerView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadata(Metadata metadata) {
|
public void onSeekProcessed() {
|
||||||
Log.d("onMetadata", "onMetadata");
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
|
// Do nothing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.brentvatne.exoplayer;
|
package com.brentvatne.exoplayer;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -8,11 +9,14 @@ import android.os.Handler;
|
|||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import com.brentvatne.react.R;
|
import com.brentvatne.react.R;
|
||||||
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
|
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
|
||||||
import com.brentvatne.receiver.BecomingNoisyListener;
|
import com.brentvatne.receiver.BecomingNoisyListener;
|
||||||
|
import com.facebook.react.bridge.Dynamic;
|
||||||
import com.facebook.react.bridge.LifecycleEventListener;
|
import com.facebook.react.bridge.LifecycleEventListener;
|
||||||
import com.facebook.react.uimanager.ThemedReactContext;
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
@ -22,6 +26,7 @@ import com.google.android.exoplayer2.ExoPlayer;
|
|||||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
@ -41,6 +46,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
|
|||||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
|
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
@ -52,6 +58,7 @@ import java.net.CookieHandler;
|
|||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
import java.lang.Math;
|
import java.lang.Math;
|
||||||
|
import java.lang.Object;
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class ReactExoplayerView extends FrameLayout implements
|
class ReactExoplayerView extends FrameLayout implements
|
||||||
@ -85,6 +92,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private int resumeWindow;
|
private int resumeWindow;
|
||||||
private long resumePosition;
|
private long resumePosition;
|
||||||
private boolean loadVideoStarted;
|
private boolean loadVideoStarted;
|
||||||
|
private boolean isFullscreen;
|
||||||
private boolean isInBackground;
|
private boolean isInBackground;
|
||||||
private boolean isPaused = true;
|
private boolean isPaused = true;
|
||||||
private boolean isBuffering;
|
private boolean isBuffering;
|
||||||
@ -94,9 +102,12 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private Uri srcUri;
|
private Uri srcUri;
|
||||||
private String extension;
|
private String extension;
|
||||||
private boolean repeat;
|
private boolean repeat;
|
||||||
|
private String textTrackType;
|
||||||
|
private Dynamic textTrackValue;
|
||||||
private boolean disableFocus;
|
private boolean disableFocus;
|
||||||
private float mProgressUpdateInterval = 250.0f;
|
private float mProgressUpdateInterval = 250.0f;
|
||||||
private boolean playInBackground = false;
|
private boolean playInBackground = false;
|
||||||
|
private boolean useTextureView = false;
|
||||||
// \ End props
|
// \ End props
|
||||||
|
|
||||||
// React
|
// React
|
||||||
@ -114,7 +125,8 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
&& player.getPlayWhenReady()
|
&& player.getPlayWhenReady()
|
||||||
) {
|
) {
|
||||||
long pos = player.getCurrentPosition();
|
long pos = player.getCurrentPosition();
|
||||||
eventEmitter.progressChanged(pos, player.getBufferedPercentage());
|
long bufferedDuration = player.getBufferedPercentage() * player.getDuration();
|
||||||
|
eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration());
|
||||||
msg = obtainMessage(SHOW_PROGRESS);
|
msg = obtainMessage(SHOW_PROGRESS);
|
||||||
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
|
sendMessageDelayed(msg, Math.round(mProgressUpdateInterval));
|
||||||
}
|
}
|
||||||
@ -125,9 +137,9 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
public ReactExoplayerView(ThemedReactContext context) {
|
public ReactExoplayerView(ThemedReactContext context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
this.themedReactContext = context;
|
||||||
createViews();
|
createViews();
|
||||||
this.eventEmitter = new VideoEventEmitter(context);
|
this.eventEmitter = new VideoEventEmitter(context);
|
||||||
this.themedReactContext = context;
|
|
||||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
themedReactContext.addLifecycleEventListener(this);
|
themedReactContext.addLifecycleEventListener(this);
|
||||||
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
|
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
|
||||||
@ -334,6 +346,9 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onStopPlayback() {
|
private void onStopPlayback() {
|
||||||
|
if (isFullscreen) {
|
||||||
|
setFullscreen(false);
|
||||||
|
}
|
||||||
setKeepScreenOn(false);
|
setKeepScreenOn(false);
|
||||||
audioManager.abandonAudioFocus(this);
|
audioManager.abandonAudioFocus(this);
|
||||||
}
|
}
|
||||||
@ -357,7 +372,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
* @return A new DataSource factory.
|
* @return A new DataSource factory.
|
||||||
*/
|
*/
|
||||||
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
|
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
|
||||||
return DataSourceUtil.getDefaultDataSourceFactory(getContext(), useBandwidthMeter ? BANDWIDTH_METER : null);
|
return DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, useBandwidthMeter ? BANDWIDTH_METER : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioManager.OnAudioFocusChangeListener implementation
|
// AudioManager.OnAudioFocusChangeListener implementation
|
||||||
@ -438,6 +453,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private void videoLoaded() {
|
private void videoLoaded() {
|
||||||
if (loadVideoStarted) {
|
if (loadVideoStarted) {
|
||||||
loadVideoStarted = false;
|
loadVideoStarted = false;
|
||||||
|
setSelectedTextTrack(textTrackType, textTrackValue);
|
||||||
Format videoFormat = player.getVideoFormat();
|
Format videoFormat = player.getVideoFormat();
|
||||||
int width = videoFormat != null ? videoFormat.width : 0;
|
int width = videoFormat != null ? videoFormat.width : 0;
|
||||||
int height = videoFormat != null ? videoFormat.height : 0;
|
int height = videoFormat != null ? videoFormat.height : 0;
|
||||||
@ -459,17 +475,38 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity() {
|
public void onPositionDiscontinuity(int reason) {
|
||||||
if (playerNeedsSource) {
|
if (playerNeedsSource) {
|
||||||
// This will only occur if the user has performed a seek whilst in the error state. Update the
|
// This will only occur if the user has performed a seek whilst in the error state. Update the
|
||||||
// resume position so that if the user then retries, playback will resume from the position to
|
// resume position so that if the user then retries, playback will resume from the position to
|
||||||
// which they seeked.
|
// which they seeked.
|
||||||
updateResumePosition();
|
updateResumePosition();
|
||||||
}
|
}
|
||||||
|
// When repeat is turned on, reaching the end of the video will not cause a state change
|
||||||
|
// so we need to explicitly detect it.
|
||||||
|
if (reason == ExoPlayer.DISCONTINUITY_REASON_PERIOD_TRANSITION
|
||||||
|
&& player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
|
||||||
|
eventEmitter.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSeekProcessed() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRepeatModeChanged(int repeatMode) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -486,6 +523,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
@Override
|
@Override
|
||||||
public void onPlayerError(ExoPlaybackException e) {
|
public void onPlayerError(ExoPlaybackException e) {
|
||||||
String errorString = null;
|
String errorString = null;
|
||||||
|
Exception ex = e;
|
||||||
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
|
||||||
Exception cause = e.getRendererException();
|
Exception cause = e.getRendererException();
|
||||||
if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
|
if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
|
||||||
@ -508,8 +546,12 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (e.type == ExoPlaybackException.TYPE_SOURCE) {
|
||||||
|
ex = e.getSourceException();
|
||||||
|
errorString = getResources().getString(R.string.unrecognized_media_format);
|
||||||
|
}
|
||||||
if (errorString != null) {
|
if (errorString != null) {
|
||||||
eventEmitter.error(errorString, e);
|
eventEmitter.error(errorString, ex);
|
||||||
}
|
}
|
||||||
playerNeedsSource = true;
|
playerNeedsSource = true;
|
||||||
if (isBehindLiveWindow(e)) {
|
if (isBehindLiveWindow(e)) {
|
||||||
@ -534,6 +576,16 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTextTrackRendererIndex() {
|
||||||
|
int rendererCount = player.getRendererCount();
|
||||||
|
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
|
||||||
|
if (player.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) {
|
||||||
|
return rendererIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return C.INDEX_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMetadata(Metadata metadata) {
|
public void onMetadata(Metadata metadata) {
|
||||||
eventEmitter.timedMetadata(metadata);
|
eventEmitter.timedMetadata(metadata);
|
||||||
@ -548,7 +600,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
this.srcUri = uri;
|
this.srcUri = uri;
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(getContext(), BANDWIDTH_METER);
|
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER);
|
||||||
|
|
||||||
if (!isOriginalSourceNull && !isSourceEqual) {
|
if (!isOriginalSourceNull && !isSourceEqual) {
|
||||||
reloadSource();
|
reloadSource();
|
||||||
@ -567,7 +619,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
this.srcUri = uri;
|
this.srcUri = uri;
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(getContext());
|
this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(this.themedReactContext);
|
||||||
|
|
||||||
if (!isOriginalSourceNull && !isSourceEqual) {
|
if (!isOriginalSourceNull && !isSourceEqual) {
|
||||||
reloadSource();
|
reloadSource();
|
||||||
@ -585,9 +637,70 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setRepeatModifier(boolean repeat) {
|
public void setRepeatModifier(boolean repeat) {
|
||||||
|
if (player != null) {
|
||||||
|
if (repeat) {
|
||||||
|
player.setRepeatMode(Player.REPEAT_MODE_ONE);
|
||||||
|
} else {
|
||||||
|
player.setRepeatMode(Player.REPEAT_MODE_OFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.repeat = repeat;
|
this.repeat = repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSelectedTextTrack(String type, Dynamic value) {
|
||||||
|
textTrackType = type;
|
||||||
|
textTrackValue = value;
|
||||||
|
|
||||||
|
int index = getTextTrackRendererIndex();
|
||||||
|
if (index == C.INDEX_UNSET) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
if (info == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackGroupArray groups = info.getTrackGroups(index);
|
||||||
|
int trackIndex = C.INDEX_UNSET;
|
||||||
|
if (TextUtils.isEmpty(type)) {
|
||||||
|
// Do nothing
|
||||||
|
} else if (type.equals("disabled")) {
|
||||||
|
trackSelector.setSelectionOverride(index, groups, null);
|
||||||
|
return;
|
||||||
|
} else if (type.equals("language")) {
|
||||||
|
for (int i = 0; i < groups.length; ++i) {
|
||||||
|
Format format = groups.get(i).getFormat(0);
|
||||||
|
if (format.language != null && format.language.equals(value.asString())) {
|
||||||
|
trackIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type.equals("title")) {
|
||||||
|
for (int i = 0; i < groups.length; ++i) {
|
||||||
|
Format format = groups.get(i).getFormat(0);
|
||||||
|
if (format.id != null && format.id.equals(value.asString())) {
|
||||||
|
trackIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type.equals("index")) {
|
||||||
|
trackIndex = value.asInt();
|
||||||
|
} else { // default. invalid type or "system"
|
||||||
|
trackSelector.clearSelectionOverrides(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackIndex == C.INDEX_UNSET) {
|
||||||
|
trackSelector.clearSelectionOverrides(trackIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingTrackSelector.SelectionOverride override
|
||||||
|
= new MappingTrackSelector.SelectionOverride(
|
||||||
|
new FixedTrackSelection.Factory(), trackIndex, 0);
|
||||||
|
trackSelector.setSelectionOverride(index, groups, override);
|
||||||
|
}
|
||||||
|
|
||||||
public void setPausedModifier(boolean paused) {
|
public void setPausedModifier(boolean paused) {
|
||||||
isPaused = paused;
|
isPaused = paused;
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
@ -636,4 +749,41 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
public void setDisableFocus(boolean disableFocus) {
|
public void setDisableFocus(boolean disableFocus) {
|
||||||
this.disableFocus = disableFocus;
|
this.disableFocus = disableFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFullscreen(boolean fullscreen) {
|
||||||
|
if (fullscreen == isFullscreen) {
|
||||||
|
return; // Avoid generating events when nothing is changing
|
||||||
|
}
|
||||||
|
isFullscreen = fullscreen;
|
||||||
|
|
||||||
|
Activity activity = themedReactContext.getCurrentActivity();
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Window window = activity.getWindow();
|
||||||
|
View decorView = window.getDecorView();
|
||||||
|
int uiOptions;
|
||||||
|
if (isFullscreen) {
|
||||||
|
if (Util.SDK_INT >= 19) { // 4.4+
|
||||||
|
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| SYSTEM_UI_FLAG_FULLSCREEN;
|
||||||
|
} else {
|
||||||
|
uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| SYSTEM_UI_FLAG_FULLSCREEN;
|
||||||
|
}
|
||||||
|
eventEmitter.fullscreenWillPresent();
|
||||||
|
decorView.setSystemUiVisibility(uiOptions);
|
||||||
|
eventEmitter.fullscreenDidPresent();
|
||||||
|
} else {
|
||||||
|
uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
|
||||||
|
eventEmitter.fullscreenWillDismiss();
|
||||||
|
decorView.setSystemUiVisibility(uiOptions);
|
||||||
|
eventEmitter.fullscreenDidDismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseTextureView(boolean useTextureView) {
|
||||||
|
exoPlayerView.setUseTextureView(useTextureView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Dynamic;
|
||||||
import com.facebook.react.bridge.ReadableMap;
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
import com.facebook.react.common.MapBuilder;
|
import com.facebook.react.common.MapBuilder;
|
||||||
import com.facebook.react.uimanager.ThemedReactContext;
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
@ -24,6 +25,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
|||||||
private static final String PROP_SRC_TYPE = "type";
|
private static final String PROP_SRC_TYPE = "type";
|
||||||
private static final String PROP_RESIZE_MODE = "resizeMode";
|
private static final String PROP_RESIZE_MODE = "resizeMode";
|
||||||
private static final String PROP_REPEAT = "repeat";
|
private static final String PROP_REPEAT = "repeat";
|
||||||
|
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
|
||||||
|
private static final String PROP_SELECTED_TEXT_TRACK_TYPE = "type";
|
||||||
|
private static final String PROP_SELECTED_TEXT_TRACK_VALUE = "value";
|
||||||
private static final String PROP_PAUSED = "paused";
|
private static final String PROP_PAUSED = "paused";
|
||||||
private static final String PROP_MUTED = "muted";
|
private static final String PROP_MUTED = "muted";
|
||||||
private static final String PROP_VOLUME = "volume";
|
private static final String PROP_VOLUME = "volume";
|
||||||
@ -32,6 +36,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
|||||||
private static final String PROP_RATE = "rate";
|
private static final String PROP_RATE = "rate";
|
||||||
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
||||||
private static final String PROP_DISABLE_FOCUS = "disableFocus";
|
private static final String PROP_DISABLE_FOCUS = "disableFocus";
|
||||||
|
private static final String PROP_FULLSCREEN = "fullscreen";
|
||||||
|
private static final String PROP_USE_TEXTURE_VIEW = "useTextureView";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@ -115,6 +121,16 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
|||||||
videoView.setRepeatModifier(repeat);
|
videoView.setRepeatModifier(repeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
|
||||||
|
public void setSelectedTextTrack(final ReactExoplayerView videoView,
|
||||||
|
@Nullable ReadableMap selectedTextTrack) {
|
||||||
|
String typeString = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)
|
||||||
|
? selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) : null;
|
||||||
|
Dynamic value = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)
|
||||||
|
? selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) : null;
|
||||||
|
videoView.setSelectedTextTrack(typeString, value);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
||||||
public void setPaused(final ReactExoplayerView videoView, final boolean paused) {
|
public void setPaused(final ReactExoplayerView videoView, final boolean paused) {
|
||||||
videoView.setPausedModifier(paused);
|
videoView.setPausedModifier(paused);
|
||||||
@ -155,6 +171,16 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
|||||||
videoView.setDisableFocus(disableFocus);
|
videoView.setDisableFocus(disableFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = PROP_FULLSCREEN, defaultBoolean = false)
|
||||||
|
public void setFullscreen(final ReactExoplayerView videoView, final boolean fullscreen) {
|
||||||
|
videoView.setFullscreen(fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = PROP_USE_TEXTURE_VIEW, defaultBoolean = false)
|
||||||
|
public void setUseTextureView(final ReactExoplayerView videoView, final boolean useTextureView) {
|
||||||
|
videoView.setUseTextureView(useTextureView);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean startsWithValidScheme(String uriString) {
|
private boolean startsWithValidScheme(String uriString) {
|
||||||
return uriString.startsWith("http://")
|
return uriString.startsWith("http://")
|
||||||
|| uriString.startsWith("https://")
|
|| uriString.startsWith("https://")
|
||||||
|
@ -31,6 +31,11 @@ class VideoEventEmitter {
|
|||||||
private static final String EVENT_PROGRESS = "onVideoProgress";
|
private static final String EVENT_PROGRESS = "onVideoProgress";
|
||||||
private static final String EVENT_SEEK = "onVideoSeek";
|
private static final String EVENT_SEEK = "onVideoSeek";
|
||||||
private static final String EVENT_END = "onVideoEnd";
|
private static final String EVENT_END = "onVideoEnd";
|
||||||
|
private static final String EVENT_FULLSCREEN_WILL_PRESENT = "onVideoFullscreenPlayerWillPresent";
|
||||||
|
private static final String EVENT_FULLSCREEN_DID_PRESENT = "onVideoFullscreenPlayerDidPresent";
|
||||||
|
private static final String EVENT_FULLSCREEN_WILL_DISMISS = "onVideoFullscreenPlayerWillDismiss";
|
||||||
|
private static final String EVENT_FULLSCREEN_DID_DISMISS = "onVideoFullscreenPlayerDidDismiss";
|
||||||
|
|
||||||
private static final String EVENT_STALLED = "onPlaybackStalled";
|
private static final String EVENT_STALLED = "onPlaybackStalled";
|
||||||
private static final String EVENT_RESUME = "onPlaybackResume";
|
private static final String EVENT_RESUME = "onPlaybackResume";
|
||||||
private static final String EVENT_READY = "onReadyForDisplay";
|
private static final String EVENT_READY = "onReadyForDisplay";
|
||||||
@ -48,6 +53,10 @@ class VideoEventEmitter {
|
|||||||
EVENT_PROGRESS,
|
EVENT_PROGRESS,
|
||||||
EVENT_SEEK,
|
EVENT_SEEK,
|
||||||
EVENT_END,
|
EVENT_END,
|
||||||
|
EVENT_FULLSCREEN_WILL_PRESENT,
|
||||||
|
EVENT_FULLSCREEN_DID_PRESENT,
|
||||||
|
EVENT_FULLSCREEN_WILL_DISMISS,
|
||||||
|
EVENT_FULLSCREEN_DID_DISMISS,
|
||||||
EVENT_STALLED,
|
EVENT_STALLED,
|
||||||
EVENT_RESUME,
|
EVENT_RESUME,
|
||||||
EVENT_READY,
|
EVENT_READY,
|
||||||
@ -67,6 +76,10 @@ class VideoEventEmitter {
|
|||||||
EVENT_PROGRESS,
|
EVENT_PROGRESS,
|
||||||
EVENT_SEEK,
|
EVENT_SEEK,
|
||||||
EVENT_END,
|
EVENT_END,
|
||||||
|
EVENT_FULLSCREEN_WILL_PRESENT,
|
||||||
|
EVENT_FULLSCREEN_DID_PRESENT,
|
||||||
|
EVENT_FULLSCREEN_WILL_DISMISS,
|
||||||
|
EVENT_FULLSCREEN_DID_DISMISS,
|
||||||
EVENT_STALLED,
|
EVENT_STALLED,
|
||||||
EVENT_RESUME,
|
EVENT_RESUME,
|
||||||
EVENT_READY,
|
EVENT_READY,
|
||||||
@ -89,6 +102,7 @@ class VideoEventEmitter {
|
|||||||
|
|
||||||
private static final String EVENT_PROP_DURATION = "duration";
|
private static final String EVENT_PROP_DURATION = "duration";
|
||||||
private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
||||||
|
private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
|
||||||
private static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
private static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
||||||
private static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
private static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
||||||
private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize";
|
private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize";
|
||||||
@ -141,10 +155,11 @@ class VideoEventEmitter {
|
|||||||
receiveEvent(EVENT_LOAD, event);
|
receiveEvent(EVENT_LOAD, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void progressChanged(double currentPosition, double bufferedDuration) {
|
void progressChanged(double currentPosition, double bufferedDuration, double seekableDuration) {
|
||||||
WritableMap event = Arguments.createMap();
|
WritableMap event = Arguments.createMap();
|
||||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||||
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D);
|
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D);
|
||||||
|
event.putDouble(EVENT_PROP_SEEKABLE_DURATION, seekableDuration / 1000D);
|
||||||
receiveEvent(EVENT_PROGRESS, event);
|
receiveEvent(EVENT_PROGRESS, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +188,22 @@ class VideoEventEmitter {
|
|||||||
receiveEvent(EVENT_END, null);
|
receiveEvent(EVENT_END, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fullscreenWillPresent() {
|
||||||
|
receiveEvent(EVENT_FULLSCREEN_WILL_PRESENT, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fullscreenDidPresent() {
|
||||||
|
receiveEvent(EVENT_FULLSCREEN_DID_PRESENT, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fullscreenWillDismiss() {
|
||||||
|
receiveEvent(EVENT_FULLSCREEN_WILL_DISMISS, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fullscreenDidDismiss() {
|
||||||
|
receiveEvent(EVENT_FULLSCREEN_DID_DISMISS, null);
|
||||||
|
}
|
||||||
|
|
||||||
void error(String errorString, Exception exception) {
|
void error(String errorString, Exception exception) {
|
||||||
WritableMap error = Arguments.createMap();
|
WritableMap error = Arguments.createMap();
|
||||||
error.putString(EVENT_PROP_ERROR_STRING, errorString);
|
error.putString(EVENT_PROP_ERROR_STRING, errorString);
|
||||||
|
@ -9,4 +9,6 @@
|
|||||||
|
|
||||||
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
||||||
|
|
||||||
|
<string name="unrecognized_media_format">Unrecognized media format</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -5,6 +5,7 @@ import android.content.res.AssetFileDescriptor;
|
|||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@ -27,6 +28,7 @@ import java.io.IOException;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.lang.Math;
|
import java.lang.Math;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnPreparedListener, MediaPlayer
|
public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnPreparedListener, MediaPlayer
|
||||||
@ -64,6 +66,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
|
|
||||||
public static final String EVENT_PROP_DURATION = "duration";
|
public static final String EVENT_PROP_DURATION = "duration";
|
||||||
public static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
public static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
|
||||||
|
public static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration";
|
||||||
public static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
public static final String EVENT_PROP_CURRENT_TIME = "currentTime";
|
||||||
public static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
public static final String EVENT_PROP_SEEK_TIME = "seekTime";
|
||||||
public static final String EVENT_PROP_NATURALSIZE = "naturalSize";
|
public static final String EVENT_PROP_NATURALSIZE = "naturalSize";
|
||||||
@ -93,8 +96,10 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
private boolean mPaused = false;
|
private boolean mPaused = false;
|
||||||
private boolean mMuted = false;
|
private boolean mMuted = false;
|
||||||
private float mVolume = 1.0f;
|
private float mVolume = 1.0f;
|
||||||
|
private float mStereoPan = 0.0f;
|
||||||
private float mProgressUpdateInterval = 250.0f;
|
private float mProgressUpdateInterval = 250.0f;
|
||||||
private float mRate = 1.0f;
|
private float mRate = 1.0f;
|
||||||
|
private float mActiveRate = 1.0f;
|
||||||
private boolean mPlayInBackground = false;
|
private boolean mPlayInBackground = false;
|
||||||
private boolean mActiveStatePauseStatus = false;
|
private boolean mActiveStatePauseStatus = false;
|
||||||
private boolean mActiveStatePauseStatusInitialized = false;
|
private boolean mActiveStatePauseStatusInitialized = false;
|
||||||
@ -127,6 +132,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
WritableMap event = Arguments.createMap();
|
WritableMap event = Arguments.createMap();
|
||||||
event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0);
|
event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0);
|
||||||
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable
|
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable
|
||||||
|
event.putDouble(EVENT_PROP_SEEKABLE_DURATION, mVideoDuration / 1000.0);
|
||||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_PROGRESS.toString(), event);
|
mEventEmitter.receiveEvent(getId(), Events.EVENT_PROGRESS.toString(), event);
|
||||||
|
|
||||||
// Check for update after an interval
|
// Check for update after an interval
|
||||||
@ -297,6 +303,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
WritableMap event = Arguments.createMap();
|
WritableMap event = Arguments.createMap();
|
||||||
event.putMap(ReactVideoViewManager.PROP_SRC, src);
|
event.putMap(ReactVideoViewManager.PROP_SRC, src);
|
||||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD_START.toString(), event);
|
mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD_START.toString(), event);
|
||||||
|
isCompleted = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
prepareAsync(this);
|
prepareAsync(this);
|
||||||
@ -343,6 +350,10 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
} else {
|
} else {
|
||||||
if (!mMediaPlayer.isPlaying()) {
|
if (!mMediaPlayer.isPlaying()) {
|
||||||
start();
|
start();
|
||||||
|
// Setting the rate unpauses, so we have to wait for an unpause
|
||||||
|
if (mRate != mActiveRate) {
|
||||||
|
setRateModifier(mRate);
|
||||||
|
}
|
||||||
|
|
||||||
// Also Start the Progress Update Handler
|
// Also Start the Progress Update Handler
|
||||||
mProgressUpdateHandler.post(mProgressUpdateRunnable);
|
mProgressUpdateHandler.post(mProgressUpdateRunnable);
|
||||||
@ -350,6 +361,14 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reduces the volume based on stereoPan
|
||||||
|
private float calulateRelativeVolume() {
|
||||||
|
float relativeVolume = (mVolume * (1 - Math.abs(mStereoPan)));
|
||||||
|
// only one decimal allowed
|
||||||
|
BigDecimal roundRelativeVolume = new BigDecimal(relativeVolume).setScale(1, BigDecimal.ROUND_HALF_UP);
|
||||||
|
return roundRelativeVolume.floatValue();
|
||||||
|
}
|
||||||
|
|
||||||
public void setMutedModifier(final boolean muted) {
|
public void setMutedModifier(final boolean muted) {
|
||||||
mMuted = muted;
|
mMuted = muted;
|
||||||
|
|
||||||
@ -359,7 +378,14 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
|
|
||||||
if (mMuted) {
|
if (mMuted) {
|
||||||
setVolume(0, 0);
|
setVolume(0, 0);
|
||||||
|
} else if (mStereoPan < 0) {
|
||||||
|
// louder on the left channel
|
||||||
|
setVolume(mVolume, calulateRelativeVolume());
|
||||||
|
} else if (mStereoPan > 0) {
|
||||||
|
// louder on the right channel
|
||||||
|
setVolume(calulateRelativeVolume(), mVolume);
|
||||||
} else {
|
} else {
|
||||||
|
// same volume on both channels
|
||||||
setVolume(mVolume, mVolume);
|
setVolume(mVolume, mVolume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,6 +395,11 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
setMutedModifier(mMuted);
|
setMutedModifier(mMuted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setStereoPan(final float stereoPan) {
|
||||||
|
mStereoPan = stereoPan;
|
||||||
|
setMutedModifier(mMuted);
|
||||||
|
}
|
||||||
|
|
||||||
public void setProgressUpdateInterval(final float progressUpdateInterval) {
|
public void setProgressUpdateInterval(final float progressUpdateInterval) {
|
||||||
mProgressUpdateInterval = progressUpdateInterval;
|
mProgressUpdateInterval = progressUpdateInterval;
|
||||||
}
|
}
|
||||||
@ -377,8 +408,14 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
mRate = rate;
|
mRate = rate;
|
||||||
|
|
||||||
if (mMediaPlayerValid) {
|
if (mMediaPlayerValid) {
|
||||||
// TODO: Implement this.
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android");
|
if (!mPaused) { // Applying the rate while paused will cause the video to start
|
||||||
|
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
|
||||||
|
mActiveRate = rate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,7 +425,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
setPausedModifier(mPaused);
|
setPausedModifier(mPaused);
|
||||||
setMutedModifier(mMuted);
|
setMutedModifier(mMuted);
|
||||||
setProgressUpdateInterval(mProgressUpdateInterval);
|
setProgressUpdateInterval(mProgressUpdateInterval);
|
||||||
// setRateModifier(mRate);
|
setRateModifier(mRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPlayInBackground(final boolean playInBackground) {
|
public void setPlayInBackground(final boolean playInBackground) {
|
||||||
|
@ -30,6 +30,7 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
|
|||||||
public static final String PROP_PAUSED = "paused";
|
public static final String PROP_PAUSED = "paused";
|
||||||
public static final String PROP_MUTED = "muted";
|
public static final String PROP_MUTED = "muted";
|
||||||
public static final String PROP_VOLUME = "volume";
|
public static final String PROP_VOLUME = "volume";
|
||||||
|
public static final String PROP_STEREO_PAN = "stereoPan";
|
||||||
public static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
|
public static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
|
||||||
public static final String PROP_SEEK = "seek";
|
public static final String PROP_SEEK = "seek";
|
||||||
public static final String PROP_RATE = "rate";
|
public static final String PROP_RATE = "rate";
|
||||||
@ -124,6 +125,11 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
|
|||||||
videoView.setVolumeModifier(volume);
|
videoView.setVolumeModifier(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = PROP_STEREO_PAN)
|
||||||
|
public void setStereoPan(final ReactVideoView videoView, final float stereoPan) {
|
||||||
|
videoView.setStereoPan(stereoPan);
|
||||||
|
}
|
||||||
|
|
||||||
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
|
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
|
||||||
public void setProgressUpdateInterval(final ReactVideoView videoView, final float progressUpdateInterval) {
|
public void setProgressUpdateInterval(final ReactVideoView videoView, final float progressUpdateInterval) {
|
||||||
videoView.setProgressUpdateInterval(progressUpdateInterval);
|
videoView.setProgressUpdateInterval(progressUpdateInterval);
|
||||||
|
BIN
docs/AppTransportSecuritySetting.png
Normal file
BIN
docs/AppTransportSecuritySetting.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
@ -40,6 +40,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
BOOL _muted;
|
BOOL _muted;
|
||||||
BOOL _paused;
|
BOOL _paused;
|
||||||
BOOL _repeat;
|
BOOL _repeat;
|
||||||
|
NSDictionary * _selectedTextTrack;
|
||||||
BOOL _playbackStalled;
|
BOOL _playbackStalled;
|
||||||
BOOL _playInBackground;
|
BOOL _playInBackground;
|
||||||
BOOL _playWhenInactive;
|
BOOL _playWhenInactive;
|
||||||
@ -93,7 +94,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
playerLayer.showsPlaybackControls = NO;
|
playerLayer.showsPlaybackControls = NO;
|
||||||
playerLayer.rctDelegate = self;
|
playerLayer.rctDelegate = self;
|
||||||
playerLayer.view.frame = self.bounds;
|
playerLayer.view.frame = self.bounds;
|
||||||
playerLayer.player = _player;
|
playerLayer.player = player;
|
||||||
playerLayer.view.frame = self.bounds;
|
playerLayer.view.frame = self.bounds;
|
||||||
return playerLayer;
|
return playerLayer;
|
||||||
}
|
}
|
||||||
@ -124,6 +125,17 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
return (kCMTimeRangeZero);
|
return (kCMTimeRangeZero);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-(void)addPlayerTimeObserver
|
||||||
|
{
|
||||||
|
const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000;
|
||||||
|
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m
|
||||||
|
// of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html
|
||||||
|
__weak RCTVideo *weakSelf = self;
|
||||||
|
_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(progressUpdateIntervalMS, NSEC_PER_SEC)
|
||||||
|
queue:NULL
|
||||||
|
usingBlock:^(CMTime time) { [weakSelf sendProgressUpdate]; }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/* Cancels the previously registered time observer. */
|
/* Cancels the previously registered time observer. */
|
||||||
-(void)removePlayerTimeObserver
|
-(void)removePlayerTimeObserver
|
||||||
@ -140,8 +152,8 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
- (void)dealloc
|
- (void)dealloc
|
||||||
{
|
{
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
[self removePlayerItemObservers];
|
|
||||||
[self removePlayerLayer];
|
[self removePlayerLayer];
|
||||||
|
[self removePlayerItemObservers];
|
||||||
[_player removeObserver:self forKeyPath:playbackRate context:nil];
|
[_player removeObserver:self forKeyPath:playbackRate context:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +200,9 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
CMTime currentTime = _player.currentTime;
|
CMTime currentTime = _player.currentTime;
|
||||||
const Float64 duration = CMTimeGetSeconds(playerDuration);
|
const Float64 duration = CMTimeGetSeconds(playerDuration);
|
||||||
const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime);
|
const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime);
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}];
|
||||||
|
|
||||||
if( currentTimeSecs >= 0 && self.onVideoProgress) {
|
if( currentTimeSecs >= 0 && self.onVideoProgress) {
|
||||||
self.onVideoProgress(@{
|
self.onVideoProgress(@{
|
||||||
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)],
|
@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)],
|
||||||
@ -262,13 +277,13 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
|
|
||||||
- (void)setSrc:(NSDictionary *)source
|
- (void)setSrc:(NSDictionary *)source
|
||||||
{
|
{
|
||||||
|
[self removePlayerLayer];
|
||||||
[self removePlayerTimeObserver];
|
[self removePlayerTimeObserver];
|
||||||
[self removePlayerItemObservers];
|
[self removePlayerItemObservers];
|
||||||
_playerItem = [self playerItemForSource:source];
|
_playerItem = [self playerItemForSource:source];
|
||||||
[self addPlayerItemObservers];
|
[self addPlayerItemObservers];
|
||||||
|
|
||||||
[_player pause];
|
[_player pause];
|
||||||
[self removePlayerLayer];
|
|
||||||
[_playerViewController.view removeFromSuperview];
|
[_playerViewController.view removeFromSuperview];
|
||||||
_playerViewController = nil;
|
_playerViewController = nil;
|
||||||
|
|
||||||
@ -283,13 +298,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
[_player addObserver:self forKeyPath:playbackRate options:0 context:nil];
|
[_player addObserver:self forKeyPath:playbackRate options:0 context:nil];
|
||||||
_playbackRateObserverRegistered = YES;
|
_playbackRateObserverRegistered = YES;
|
||||||
|
|
||||||
const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000;
|
[self addPlayerTimeObserver];
|
||||||
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html
|
|
||||||
__weak RCTVideo *weakSelf = self;
|
|
||||||
_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(progressUpdateIntervalMS, NSEC_PER_SEC)
|
|
||||||
queue:NULL
|
|
||||||
usingBlock:^(CMTime time) { [weakSelf sendProgressUpdate]; }
|
|
||||||
];
|
|
||||||
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
//Perform on next run loop, otherwise onVideoLoadStart is nil
|
//Perform on next run loop, otherwise onVideoLoadStart is nil
|
||||||
@ -454,10 +463,17 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
- (void)attachListeners
|
- (void)attachListeners
|
||||||
{
|
{
|
||||||
// listen for end of file
|
// listen for end of file
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:AVPlayerItemDidPlayToEndTimeNotification
|
||||||
|
object:[_player currentItem]];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(playerItemDidReachEnd:)
|
selector:@selector(playerItemDidReachEnd:)
|
||||||
name:AVPlayerItemDidPlayToEndTimeNotification
|
name:AVPlayerItemDidPlayToEndTimeNotification
|
||||||
object:[_player currentItem]];
|
object:[_player currentItem]];
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:AVPlayerItemPlaybackStalledNotification
|
||||||
|
object:nil];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(playbackStalled:)
|
selector:@selector(playbackStalled:)
|
||||||
name:AVPlayerItemPlaybackStalledNotification
|
name:AVPlayerItemPlaybackStalledNotification
|
||||||
@ -482,6 +498,8 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
AVPlayerItem *item = [notification object];
|
AVPlayerItem *item = [notification object];
|
||||||
[item seekToTime:kCMTimeZero];
|
[item seekToTime:kCMTimeZero];
|
||||||
[self applyModifiers];
|
[self applyModifiers];
|
||||||
|
} else {
|
||||||
|
[self removePlayerTimeObserver];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,7 +579,12 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
if (CMTimeCompare(current, cmSeekTime) != 0) {
|
if (CMTimeCompare(current, cmSeekTime) != 0) {
|
||||||
if (!wasPaused) [_player pause];
|
if (!wasPaused) [_player pause];
|
||||||
[_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) {
|
[_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) {
|
||||||
if (!wasPaused) [_player play];
|
if (!_timeObserver) {
|
||||||
|
[self addPlayerTimeObserver];
|
||||||
|
}
|
||||||
|
if (!wasPaused) {
|
||||||
|
[self setPaused:false];
|
||||||
|
}
|
||||||
if(self.onVideoSeek) {
|
if(self.onVideoSeek) {
|
||||||
self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)],
|
self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)],
|
||||||
@"seekTime": [NSNumber numberWithFloat:seekTime],
|
@"seekTime": [NSNumber numberWithFloat:seekTime],
|
||||||
@ -607,6 +630,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
[_player setMuted:NO];
|
[_player setMuted:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[self setSelectedTextTrack:_selectedTextTrack];
|
||||||
[self setResizeMode:_resizeMode];
|
[self setResizeMode:_resizeMode];
|
||||||
[self setRepeat:_repeat];
|
[self setRepeat:_repeat];
|
||||||
[self setPaused:_paused];
|
[self setPaused:_paused];
|
||||||
@ -617,6 +641,50 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
_repeat = repeat;
|
_repeat = repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack {
|
||||||
|
_selectedTextTrack = selectedTextTrack;
|
||||||
|
NSString *type = selectedTextTrack[@"type"];
|
||||||
|
AVMediaSelectionGroup *group = [_player.currentItem.asset
|
||||||
|
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
|
||||||
|
AVMediaSelectionOption *option;
|
||||||
|
|
||||||
|
if ([type isEqualToString:@"disabled"]) {
|
||||||
|
// Do nothing. We want to ensure option is nil
|
||||||
|
} else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) {
|
||||||
|
NSString *value = selectedTextTrack[@"value"];
|
||||||
|
for (int i = 0; i < group.options.count; ++i) {
|
||||||
|
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i];
|
||||||
|
NSString *optionValue;
|
||||||
|
if ([type isEqualToString:@"language"]) {
|
||||||
|
optionValue = [currentOption extendedLanguageTag];
|
||||||
|
} else {
|
||||||
|
optionValue = [[[currentOption commonMetadata]
|
||||||
|
valueForKey:@"value"]
|
||||||
|
objectAtIndex:0];
|
||||||
|
}
|
||||||
|
if ([value isEqualToString:optionValue]) {
|
||||||
|
option = currentOption;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//} else if ([type isEqualToString:@"default"]) {
|
||||||
|
// option = group.defaultOption; */
|
||||||
|
} else if ([type isEqualToString:@"index"]) {
|
||||||
|
if ([selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) {
|
||||||
|
int index = [selectedTextTrack[@"value"] intValue];
|
||||||
|
if (group.options.count > index) {
|
||||||
|
option = [group.options objectAtIndex:index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // default. invalid type or "system"
|
||||||
|
[_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a match isn't found, option will be nil and text tracks will be disabled
|
||||||
|
[_player.currentItem selectMediaOption:option inMediaSelectionGroup:group];
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)getFullscreen
|
- (BOOL)getFullscreen
|
||||||
{
|
{
|
||||||
return _fullscreenPlayerPresented;
|
return _fullscreenPlayerPresented;
|
||||||
@ -721,6 +789,11 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
- (void)setProgressUpdateInterval:(float)progressUpdateInterval
|
- (void)setProgressUpdateInterval:(float)progressUpdateInterval
|
||||||
{
|
{
|
||||||
_progressUpdateInterval = progressUpdateInterval;
|
_progressUpdateInterval = progressUpdateInterval;
|
||||||
|
|
||||||
|
if (_timeObserver) {
|
||||||
|
[self removePlayerTimeObserver];
|
||||||
|
[self addPlayerTimeObserver];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)removePlayerLayer
|
- (void)removePlayerLayer
|
||||||
|
@ -22,6 +22,7 @@ RCT_EXPORT_MODULE();
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
|
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
|
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(muted, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(muted, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);
|
||||||
@ -62,4 +63,9 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock);
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (BOOL)requiresMainQueueSetup
|
||||||
|
{
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -9,12 +9,8 @@
|
|||||||
- (void)viewDidDisappear:(BOOL)animated
|
- (void)viewDidDisappear:(BOOL)animated
|
||||||
{
|
{
|
||||||
[super viewDidDisappear:animated];
|
[super viewDidDisappear:animated];
|
||||||
|
[_rctDelegate videoPlayerViewControllerWillDismiss:self];
|
||||||
[_rctDelegate videoPlayerViewControllerDidDismiss:self];
|
[_rctDelegate videoPlayerViewControllerDidDismiss:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillDisappear:(BOOL)animated {
|
|
||||||
[_rctDelegate videoPlayerViewControllerWillDismiss:self];
|
|
||||||
[super viewWillDisappear:animated];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
10
package.json
10
package.json
@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "react-native-video",
|
"name": "react-native-video",
|
||||||
"version": "2.0.0",
|
"version": "2.2.0",
|
||||||
"description": "A <Video /> element for react-native",
|
"description": "A <Video /> element for react-native",
|
||||||
"main": "Video.js",
|
"main": "Video.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Brent Vatne <brentvatne@gmail.com> (https://github.com/brentvatne)",
|
"author": "Brent Vatne <brentvatne@gmail.com> (https://github.com/brentvatne)",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Isaiah Grey",
|
||||||
|
"email": "isaiahgrey@gmail.com"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Johannes Lumpe",
|
"name": "Johannes Lumpe",
|
||||||
"email": "johannes@lum.pe"
|
"email": "johannes@lum.pe"
|
||||||
@ -13,6 +17,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Baris Sencan",
|
"name": "Baris Sencan",
|
||||||
"email": "baris.sncn@gmail.com"
|
"email": "baris.sncn@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hampton Maxwell",
|
||||||
|
"email": "me@hamptonmaxwell.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -328,7 +328,7 @@ namespace ReactNativeVideo
|
|||||||
private readonly JObject _eventData;
|
private readonly JObject _eventData;
|
||||||
|
|
||||||
public ReactVideoEvent(string eventName, int viewTag, JObject eventData)
|
public ReactVideoEvent(string eventName, int viewTag, JObject eventData)
|
||||||
: base(viewTag, TimeSpan.FromTicks(Environment.TickCount))
|
: base(viewTag)
|
||||||
{
|
{
|
||||||
_eventName = eventName;
|
_eventName = eventName;
|
||||||
_eventData = eventData;
|
_eventData = eventData;
|
||||||
|
@ -334,7 +334,7 @@ namespace ReactNativeVideo
|
|||||||
private readonly JObject _eventData;
|
private readonly JObject _eventData;
|
||||||
|
|
||||||
public ReactVideoEvent(string eventName, int viewTag, JObject eventData)
|
public ReactVideoEvent(string eventName, int viewTag, JObject eventData)
|
||||||
: base(viewTag, TimeSpan.FromTicks(Environment.TickCount))
|
: base(viewTag)
|
||||||
{
|
{
|
||||||
_eventName = eventName;
|
_eventName = eventName;
|
||||||
_eventData = eventData;
|
_eventData = eventData;
|
||||||
|
Loading…
Reference in New Issue
Block a user