Merge branch 'master' into master
This commit is contained in:
commit
c67dd7b8ef
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,5 +1,21 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### Next Version
|
||||||
|
* Inherit Android buildtools and SDK version from the root project [#1081](https://github.com/react-native-community/react-native-video/pull/1081)
|
||||||
|
* Automatically play on ExoPlayer when the paused prop is not set [#1083](https://github.com/react-native-community/react-native-video/pull/1083)
|
||||||
|
* Preserve Android MediaPlayer paused prop when backgrounding [#1082](https://github.com/react-native-community/react-native-video/pull/1082)
|
||||||
|
|
||||||
|
### Version 2.3.1
|
||||||
|
* Revert PR to inherit Android SDK versions from root project. Re-add in 3.0 [#1080](https://github.com/react-native-community/react-native-video/pull/1080)
|
||||||
|
|
||||||
|
### Version 2.3.0
|
||||||
|
* Support allowsExternalPlayback on iOS [#1057](https://github.com/react-native-community/react-native-video/pull/1057)
|
||||||
|
* Inherit Android buildtools and SDK version from the root project [#999](https://github.com/react-native-community/react-native-video/pull/999)
|
||||||
|
* Fix bug that caused ExoPlayer to start paused if playInBackground was set [#833](https://github.com/react-native-community/react-native-video/pull/833)
|
||||||
|
* Fix crash if clearing an observer on iOS that was already cleared [#1075](https://github.com/react-native-community/react-native-video/pull/1075)
|
||||||
|
* Add audioOnly prop for music files [#1039](https://github.com/react-native-community/react-native-video/pull/1039)
|
||||||
|
* Support seeking with more exact tolerance on iOS [#1076](https://github.com/react-native-community/react-native-video/pull/1076)
|
||||||
|
|
||||||
### Version 2.2.0
|
### Version 2.2.0
|
||||||
* Text track selection support for iOS & ExoPlayer [#1049](https://github.com/react-native-community/react-native-video/pull/1049)
|
* 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 outputting to a TextureView on Android ExoPlayer [#1058](https://github.com/react-native-community/react-native-video/pull/1058)
|
||||||
|
211
README.md
211
README.md
@ -5,10 +5,14 @@ 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.
|
||||||
|
|
||||||
|
### Version 3.0 breaking changes
|
||||||
|
Version 3.0 features a number of changes to existing behavior. See [Updating](#updating) for changes.
|
||||||
|
|
||||||
## TOC
|
## TOC
|
||||||
|
|
||||||
* [Installation](#installation)
|
* [Installation](#installation)
|
||||||
* [Usage](#usage)
|
* [Usage](#usage)
|
||||||
|
* [Updating](#updating)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -191,8 +195,6 @@ using System.Collections.Generic;
|
|||||||
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
|
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
|
||||||
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
|
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
|
||||||
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped
|
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped
|
||||||
onLoadStart={this.loadStart} // Callback when video starts to load
|
|
||||||
onLoad={this.setDuration} // Callback when video loads
|
|
||||||
onProgress={this.setTime} // Callback every ~250ms with currentTime
|
onProgress={this.setTime} // Callback every ~250ms with currentTime
|
||||||
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} />
|
||||||
@ -219,6 +221,8 @@ var styles = StyleSheet.create({
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Configurable props
|
### Configurable props
|
||||||
|
* [allowsExternalPlayback](#allowsexternalplayback)
|
||||||
|
* [audioOnly](#audioonly)
|
||||||
* [ignoreSilentSwitch](#ignoresilentswitch)
|
* [ignoreSilentSwitch](#ignoresilentswitch)
|
||||||
* [muted](#muted)
|
* [muted](#muted)
|
||||||
* [paused](#paused)
|
* [paused](#paused)
|
||||||
@ -232,9 +236,35 @@ var styles = StyleSheet.create({
|
|||||||
* [resizeMode](#resizemode)
|
* [resizeMode](#resizemode)
|
||||||
* [selectedTextTrack](#selectedtexttrack)
|
* [selectedTextTrack](#selectedtexttrack)
|
||||||
* [stereoPan](#stereopan)
|
* [stereoPan](#stereopan)
|
||||||
|
* [textTracks](#texttracks)
|
||||||
* [useTextureView](#usetextureview)
|
* [useTextureView](#usetextureview)
|
||||||
* [volume](#volume)
|
* [volume](#volume)
|
||||||
|
|
||||||
|
### Event props
|
||||||
|
* [onLoad](#onload)
|
||||||
|
* [onLoadStart](#onloadstart)
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
* [seek](#seek)
|
||||||
|
|
||||||
|
### Configurable props
|
||||||
|
|
||||||
|
#### allowsExternalPlayback
|
||||||
|
Indicates whether the player allows switching to external playback mode such as AirPlay or HDMI.
|
||||||
|
* **true (default)** - allow switching to external playback mode
|
||||||
|
* **false** - Don't allow switching to external playback mode
|
||||||
|
|
||||||
|
Platforms: iOS
|
||||||
|
|
||||||
|
#### audioOnly
|
||||||
|
Indicates whether the player should only play the audio track and instead of displaying the video track, show the poster instead.
|
||||||
|
* **false (default)** - Display the video as normal
|
||||||
|
* **true** - Show the poster and play the audio
|
||||||
|
|
||||||
|
For this to work, the poster prop must be set.
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
#### ignoreSilentSwitch
|
#### ignoreSilentSwitch
|
||||||
Controls the iOS silent switch behavior
|
Controls the iOS silent switch behavior
|
||||||
* **"inherit" (default)** - Use the default AVPlayer behavior
|
* **"inherit" (default)** - Use the default AVPlayer behavior
|
||||||
@ -351,7 +381,7 @@ Type | Value | Description
|
|||||||
"language" | string | Display the text track with the language specified as the Value, e.g. "fr"
|
"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
|
"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.
|
Both iOS & Android (only 4.4 and higher) 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.
|
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.
|
||||||
|
|
||||||
@ -365,6 +395,40 @@ Adjust the balance of the left and right audio channels. Any value between –1
|
|||||||
|
|
||||||
Platforms: Android MediaPlayer
|
Platforms: Android MediaPlayer
|
||||||
|
|
||||||
|
#### textTracks
|
||||||
|
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
|
||||||
|
|
||||||
|
Property | Description
|
||||||
|
--- | ---
|
||||||
|
title | Descriptive name for the track
|
||||||
|
language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language
|
||||||
|
type | Mime type of the track<br> * TextTrackType.SRT - .srt SubRip Subtitle<br> * TextTrackType.TTML - .ttml TTML<br> * TextTrackType.VTT - .vtt WebVTT
|
||||||
|
uri | URL for the text track. Currently, only tracks hosted on a webserver are supported
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
import { TextTrackType }, Video from 'react-native-video';
|
||||||
|
|
||||||
|
textTracks={[
|
||||||
|
{
|
||||||
|
title: "English CC",
|
||||||
|
language: "en",
|
||||||
|
type: "text/vtt", TextTrackType.VTT,
|
||||||
|
uri: "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Spanish Subtitles",
|
||||||
|
language: "es",
|
||||||
|
type: "application/x-subrip", TextTrackType.SRT,
|
||||||
|
uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
This isn't support on iOS because AVPlayer doesn't support it. Text tracks must be loaded as part of an HLS playlist.
|
||||||
|
|
||||||
|
Platforms: Android ExoPlayer
|
||||||
|
|
||||||
#### useTextureView
|
#### 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:
|
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
|
* It can't be animated, transformed or scaled
|
||||||
@ -385,6 +449,107 @@ Adjust the volume.
|
|||||||
|
|
||||||
Platforms: all
|
Platforms: all
|
||||||
|
|
||||||
|
### Event props
|
||||||
|
|
||||||
|
#### onLoad
|
||||||
|
Callback function that is called when the media is loaded and ready to play.
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
|
||||||
|
Property | Type | Description
|
||||||
|
--- | --- | ---
|
||||||
|
currentPosition | number | Time in seconds where the media will start
|
||||||
|
duration | number | Length of the media in seconds
|
||||||
|
naturalSize | object | Properties:<br> * width - Width in pixels that the video was encoded at<br> * height - Height in pixels that the video was encoded at<br> * orientation - "portrait" or "landscape"
|
||||||
|
textTracks | array | An array of text track info objects with the following properties:<br> * index - Index number<br> * title - Description of the track<br> * language - 2 letter [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code<br> * type - Mime type of track
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
canPlaySlowForward: true,
|
||||||
|
canPlayReverse: false,
|
||||||
|
canPlaySlowReverse: false,
|
||||||
|
canPlayFastForward: false,
|
||||||
|
canStepForward: false,
|
||||||
|
canStepBackward: false,
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 5910.208984375,
|
||||||
|
naturalSize: {
|
||||||
|
height: 1080
|
||||||
|
orientation: 'landscape'
|
||||||
|
width: '1920'
|
||||||
|
},
|
||||||
|
textTracks: [
|
||||||
|
{ title: '#1 French', language: 'fr', index: 0, type: 'text/vtt' },
|
||||||
|
{ title: '#2 English CC', language: 'en', index: 1, type: 'text/vtt' },
|
||||||
|
{ title: '#3 English Director Commentary', language: 'en', index: 2, type: 'text/vtt' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
#### onLoadStart
|
||||||
|
Callback function that is called when the media starts loading.
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
|
||||||
|
Property | Description
|
||||||
|
--- | ---
|
||||||
|
isNetwork | Boolean indicating if the media is being loaded from the network
|
||||||
|
type | Type of the media. Not available on Windows
|
||||||
|
uri | URI for the media source. Not available on Windows
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
isNetwork: true,
|
||||||
|
type: '',
|
||||||
|
uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
Methods operate on a ref to the Video element. You can create a ref using code like:
|
||||||
|
```
|
||||||
|
return (
|
||||||
|
<Video source={...}
|
||||||
|
ref => (this.player = ref) />
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### seek()
|
||||||
|
`seek(seconds)`
|
||||||
|
|
||||||
|
Seek to the specified position represented by seconds. seconds is a float value.
|
||||||
|
|
||||||
|
`seek()` can only be called after the `onLoad` event has fired.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
this.player.seek(200); // Seek to 3 minutes, 20 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
Platforms: all
|
||||||
|
|
||||||
|
##### Exact seek
|
||||||
|
|
||||||
|
By default iOS seeks within 100 milliseconds of the target position. If you need more accuracy, you can use the seek with tolerance method:
|
||||||
|
|
||||||
|
`seek(seconds, tolerance)`
|
||||||
|
|
||||||
|
tolerance is the max distance in milliseconds from the seconds position that's allowed. Using a more exact tolerance can cause seeks to take longer. If you want to seek exactly, set tolerance to 0.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accuracy
|
||||||
|
```
|
||||||
|
|
||||||
|
Platforms: iOS
|
||||||
|
|
||||||
|
|
||||||
### Additional props
|
### 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.
|
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.
|
||||||
@ -437,9 +602,47 @@ To enable audio to play in background on iOS the audio session needs to be set t
|
|||||||
|
|
||||||
- [Lumpen Radio](https://github.com/jhabdas/lumpen-radio) contains another example integration using local files and full screen background video.
|
- [Lumpen Radio](https://github.com/jhabdas/lumpen-radio) contains another example integration using local files and full screen background video.
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
### Version 3.0
|
||||||
|
|
||||||
|
#### All platforms now auto-play
|
||||||
|
Previously, on Android ExoPlayer if the paused prop was not set, the media would not automatically start playing. The only way it would work was if you set `paused={false}`. This has been changed to automatically play if paused is not set so that the behavior is consistent across platforms.
|
||||||
|
|
||||||
|
#### All platforms now keep their paused state when returning from the background
|
||||||
|
Previously, on Android MediaPlayer if you setup an AppState event when the app went into the background and set a paused prop so that when you returned to the app the video would be paused it would be ignored.
|
||||||
|
|
||||||
|
Note, Windows does not have a concept of an app going into the background, so this doesn't apply there.
|
||||||
|
|
||||||
|
#### Use Android SDK 27 by default
|
||||||
|
Version 3.0 updates the Android build tools and SDK to version 27. React Native is in the process of [switchting over](https://github.com/facebook/react-native/issues/18095#issuecomment-395596130) to SDK 27 in preparation for Google's requirement that new Android apps [use SDK 26](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html) by August 2018.
|
||||||
|
|
||||||
|
You will either need to install the version 27 SDK and version 27.0.3 buildtools or modify your build.gradle file to configure react-native-video to use the same build settings as the rest of your app as described below.
|
||||||
|
|
||||||
|
##### Using app build settings
|
||||||
|
You will need to create a `project.ext` section in the top-level build.gradle file (not app/build.gradle). Fill in the values from the example below using the values found in your app/build.gradle file.
|
||||||
|
```
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
... // Various other settings go here
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
... // Various other settings go here
|
||||||
|
|
||||||
|
project.ext {
|
||||||
|
compileSdkVersion = 23
|
||||||
|
buildToolsVersion = "23.0.1"
|
||||||
|
|
||||||
|
minSdkVersion = 16
|
||||||
|
targetSdkVersion = 22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## TODOS
|
## TODOS
|
||||||
|
|
||||||
- [ ] Add support for captions
|
|
||||||
- [ ] Add support for playing multiple videos in a sequence (will interfere with current `repeat` implementation)
|
- [ ] Add support for playing multiple videos in a sequence (will interfere with current `repeat` implementation)
|
||||||
- [x] Callback to get buffering progress for remote videos
|
- [x] Callback to get buffering progress for remote videos
|
||||||
- [ ] Bring API closer to HTML5 `<Video>` [reference](http://devdocs.io/html/element/video)
|
- [ ] Bring API closer to HTML5 `<Video>` [reference](http://devdocs.io/html/element/video)
|
||||||
|
7
TextTrackType.js
Normal file
7
TextTrackType.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import keyMirror from 'keymirror';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
SRT: 'application/x-subrip',
|
||||||
|
TTML: 'application/ttml+xml',
|
||||||
|
VTT: 'text/vtt'
|
||||||
|
};
|
39
Video.js
39
Video.js
@ -1,7 +1,8 @@
|
|||||||
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, ViewPropTypes, Image} from 'react-native';
|
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform} from 'react-native';
|
||||||
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
||||||
|
import TextTrackType from './TextTrackType';
|
||||||
import VideoResizeMode from './VideoResizeMode.js';
|
import VideoResizeMode from './VideoResizeMode.js';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@ -10,6 +11,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export { TextTrackType };
|
||||||
|
|
||||||
export default class Video extends Component {
|
export default class Video extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -47,8 +50,17 @@ export default class Video extends Component {
|
|||||||
return strObj;
|
return strObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
seek = (time) => {
|
seek = (time, tolerance = 100) => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
this.setNativeProps({
|
||||||
|
seek: {
|
||||||
|
time,
|
||||||
|
tolerance
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.setNativeProps({ seek: time });
|
this.setNativeProps({ seek: time });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
presentFullscreenPlayer = () => {
|
presentFullscreenPlayer = () => {
|
||||||
@ -88,7 +100,7 @@ export default class Video extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_onSeek = (event) => {
|
_onSeek = (event) => {
|
||||||
if (this.state.showPoster) {
|
if (this.state.showPoster && !this.props.audioOnly) {
|
||||||
this.setState({showPoster: false});
|
this.setState({showPoster: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +164,7 @@ export default class Video extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_onPlaybackRateChange = (event) => {
|
_onPlaybackRateChange = (event) => {
|
||||||
if (this.state.showPoster && (event.nativeEvent.playbackRate !== 0)) {
|
if (this.state.showPoster && event.nativeEvent.playbackRate !== 0 && !this.props.audioOnly) {
|
||||||
this.setState({showPoster: false});
|
this.setState({showPoster: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +283,10 @@ export default class Video extends Component {
|
|||||||
Video.propTypes = {
|
Video.propTypes = {
|
||||||
/* Native only */
|
/* Native only */
|
||||||
src: PropTypes.object,
|
src: PropTypes.object,
|
||||||
seek: PropTypes.number,
|
seek: PropTypes.oneOfType([
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.object
|
||||||
|
]),
|
||||||
fullscreen: PropTypes.bool,
|
fullscreen: PropTypes.bool,
|
||||||
onVideoLoadStart: PropTypes.func,
|
onVideoLoadStart: PropTypes.func,
|
||||||
onVideoLoad: PropTypes.func,
|
onVideoLoad: PropTypes.func,
|
||||||
@ -298,6 +313,7 @@ Video.propTypes = {
|
|||||||
poster: PropTypes.string,
|
poster: PropTypes.string,
|
||||||
posterResizeMode: Image.propTypes.resizeMode,
|
posterResizeMode: Image.propTypes.resizeMode,
|
||||||
repeat: PropTypes.bool,
|
repeat: PropTypes.bool,
|
||||||
|
allowsExternalPlayback: PropTypes.bool,
|
||||||
selectedTextTrack: PropTypes.shape({
|
selectedTextTrack: PropTypes.shape({
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([
|
||||||
@ -305,6 +321,18 @@ Video.propTypes = {
|
|||||||
PropTypes.number
|
PropTypes.number
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
|
textTracks: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
title: PropTypes.string,
|
||||||
|
uri: PropTypes.string.isRequired,
|
||||||
|
type: PropTypes.oneOf([
|
||||||
|
TextTrackType.SRT,
|
||||||
|
TextTrackType.TTML,
|
||||||
|
TextTrackType.VTT,
|
||||||
|
]),
|
||||||
|
language: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
),
|
||||||
paused: PropTypes.bool,
|
paused: PropTypes.bool,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
@ -315,6 +343,7 @@ Video.propTypes = {
|
|||||||
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
|
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
|
||||||
disableFocus: PropTypes.bool,
|
disableFocus: PropTypes.bool,
|
||||||
controls: PropTypes.bool,
|
controls: PropTypes.bool,
|
||||||
|
audioOnly: PropTypes.bool,
|
||||||
currentTime: PropTypes.number,
|
currentTime: PropTypes.number,
|
||||||
progressUpdateInterval: PropTypes.number,
|
progressUpdateInterval: PropTypes.number,
|
||||||
useTextureView: PropTypes.bool,
|
useTextureView: PropTypes.bool,
|
||||||
|
@ -42,6 +42,5 @@ https://github.com/google/ExoPlayer
|
|||||||
|
|
||||||
## Unimplemented props
|
## Unimplemented props
|
||||||
|
|
||||||
- `playInBackground={true}`
|
|
||||||
- `rate={1.0}`
|
|
||||||
- Expansion file - `source={{ mainVer: 1, patchVer: 0 }}`
|
- Expansion file - `source={{ mainVer: 1, patchVer: 0 }}`
|
||||||
|
|
||||||
|
@ -1,17 +1,28 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
def _ext = rootProject.ext
|
||||||
|
|
||||||
|
def _reactNativeVersion = _ext.has('reactNative') ? _ext.reactNative : '+'
|
||||||
|
def _compileSdkVersion = _ext.has('compileSdkVersion') ? _ext.compileSdkVersion : 27
|
||||||
|
def _buildToolsVersion = _ext.has('buildToolsVersion') ? _ext.buildToolsVersion : '27.0.3'
|
||||||
|
def _minSdkVersion = _ext.has('minSdkVersion') ? _ext.minSdkVersion : 16
|
||||||
|
def _targetSdkVersion = _ext.has('targetSdkVersion') ? _ext.targetSdkVersion : 27
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion _compileSdkVersion
|
||||||
buildToolsVersion "25.0.2"
|
buildToolsVersion _buildToolsVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion _minSdkVersion
|
||||||
targetSdkVersion 23
|
targetSdkVersion _targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
provided 'com.facebook.react:react-native:+'
|
//noinspection GradleDynamicVersion
|
||||||
|
provided "com.facebook.react:react-native:${_reactNativeVersion}"
|
||||||
compile 'com.google.android.exoplayer:exoplayer:2.7.3'
|
compile 'com.google.android.exoplayer:exoplayer:2.7.3'
|
||||||
compile('com.google.android.exoplayer:extension-okhttp:2.7.3') {
|
compile('com.google.android.exoplayer:extension-okhttp:2.7.3') {
|
||||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||||
|
@ -16,9 +16,13 @@ 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.Arguments;
|
||||||
import com.facebook.react.bridge.Dynamic;
|
import com.facebook.react.bridge.Dynamic;
|
||||||
import com.facebook.react.bridge.LifecycleEventListener;
|
import com.facebook.react.bridge.LifecycleEventListener;
|
||||||
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
import com.facebook.react.bridge.ReadableMap;
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import com.facebook.react.bridge.WritableArray;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
import com.facebook.react.uimanager.ThemedReactContext;
|
import com.facebook.react.uimanager.ThemedReactContext;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||||
@ -37,8 +41,9 @@ import com.google.android.exoplayer2.metadata.Metadata;
|
|||||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||||
import com.google.android.exoplayer2.source.LoopingMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||||
@ -53,6 +58,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
|
|||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
import com.google.android.exoplayer2.upstream.DataSource;
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
@ -61,6 +67,7 @@ import java.net.CookiePolicy;
|
|||||||
import java.lang.Math;
|
import java.lang.Math;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.lang.Object;
|
import java.lang.Object;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class ReactExoplayerView extends FrameLayout implements
|
class ReactExoplayerView extends FrameLayout implements
|
||||||
@ -95,7 +102,8 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private long resumePosition;
|
private long resumePosition;
|
||||||
private boolean loadVideoStarted;
|
private boolean loadVideoStarted;
|
||||||
private boolean isFullscreen;
|
private boolean isFullscreen;
|
||||||
private boolean isPaused = true;
|
private boolean isInBackground;
|
||||||
|
private boolean isPaused;
|
||||||
private boolean isBuffering;
|
private boolean isBuffering;
|
||||||
private float rate = 1f;
|
private float rate = 1f;
|
||||||
|
|
||||||
@ -105,6 +113,7 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
private boolean repeat;
|
private boolean repeat;
|
||||||
private String textTrackType;
|
private String textTrackType;
|
||||||
private Dynamic textTrackValue;
|
private Dynamic textTrackValue;
|
||||||
|
private ReadableArray textTracks;
|
||||||
private boolean disableFocus;
|
private boolean disableFocus;
|
||||||
private float mProgressUpdateInterval = 250.0f;
|
private float mProgressUpdateInterval = 250.0f;
|
||||||
private boolean playInBackground = false;
|
private boolean playInBackground = false;
|
||||||
@ -189,14 +198,15 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onHostResume() {
|
public void onHostResume() {
|
||||||
if (playInBackground) {
|
if (!playInBackground || !isInBackground) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPlayWhenReady(!isPaused);
|
setPlayWhenReady(!isPaused);
|
||||||
}
|
}
|
||||||
|
isInBackground = false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onHostPause() {
|
public void onHostPause() {
|
||||||
|
isInBackground = true;
|
||||||
if (playInBackground) {
|
if (playInBackground) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -231,8 +241,19 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
player.setPlaybackParameters(params);
|
player.setPlaybackParameters(params);
|
||||||
}
|
}
|
||||||
if (playerNeedsSource && srcUri != null) {
|
if (playerNeedsSource && srcUri != null) {
|
||||||
MediaSource mediaSource = buildMediaSource(srcUri, extension);
|
ArrayList<MediaSource> mediaSourceList = buildTextSources();
|
||||||
mediaSource = repeat ? new LoopingMediaSource(mediaSource) : mediaSource;
|
MediaSource videoSource = buildMediaSource(srcUri, extension);
|
||||||
|
MediaSource mediaSource;
|
||||||
|
if (mediaSourceList.size() == 0) {
|
||||||
|
mediaSource = videoSource;
|
||||||
|
} else {
|
||||||
|
mediaSourceList.add(0, videoSource);
|
||||||
|
MediaSource[] textSourceArray = mediaSourceList.toArray(
|
||||||
|
new MediaSource[mediaSourceList.size()]
|
||||||
|
);
|
||||||
|
mediaSource = new MergingMediaSource(textSourceArray);
|
||||||
|
}
|
||||||
|
|
||||||
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
|
||||||
if (haveResumePosition) {
|
if (haveResumePosition) {
|
||||||
player.seekTo(resumeWindow, resumePosition);
|
player.seekTo(resumeWindow, resumePosition);
|
||||||
@ -266,6 +287,32 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ArrayList<MediaSource> buildTextSources() {
|
||||||
|
ArrayList<MediaSource> textSources = new ArrayList<>();
|
||||||
|
if (textTracks == null) {
|
||||||
|
return textSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < textTracks.size(); ++i) {
|
||||||
|
ReadableMap textTrack = textTracks.getMap(i);
|
||||||
|
String language = textTrack.getString("language");
|
||||||
|
String title = textTrack.hasKey("title")
|
||||||
|
? textTrack.getString("title") : language + " " + i;
|
||||||
|
Uri uri = Uri.parse(textTrack.getString("uri"));
|
||||||
|
MediaSource textSource = buildTextSource(title, uri, textTrack.getString("type"),
|
||||||
|
language);
|
||||||
|
if (textSource != null) {
|
||||||
|
textSources.add(textSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return textSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaSource buildTextSource(String title, Uri uri, String mimeType, String language) {
|
||||||
|
Format textFormat = Format.createTextSampleFormat(title, mimeType, Format.NO_VALUE, language);
|
||||||
|
return new SingleSampleMediaSource(uri, mediaDataSourceFactory, textFormat, C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
private void releasePlayer() {
|
private void releasePlayer() {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
isPaused = player.getPlayWhenReady();
|
isPaused = player.getPlayWhenReady();
|
||||||
@ -456,10 +503,33 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
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;
|
||||||
eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height);
|
eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height,
|
||||||
|
getTextTrackInfo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WritableArray getTextTrackInfo() {
|
||||||
|
WritableArray textTracks = Arguments.createArray();
|
||||||
|
|
||||||
|
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||||
|
int index = getTextTrackRendererIndex();
|
||||||
|
if (info == null || index == C.INDEX_UNSET) {
|
||||||
|
return textTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackGroupArray groups = info.getTrackGroups(index);
|
||||||
|
for (int i = 0; i < groups.length; ++i) {
|
||||||
|
Format format = groups.get(i).getFormat(0);
|
||||||
|
WritableMap textTrack = Arguments.createMap();
|
||||||
|
textTrack.putInt("index", i);
|
||||||
|
textTrack.putString("title", format.id != null ? format.id : "");
|
||||||
|
textTrack.putString("type", format.sampleMimeType);
|
||||||
|
textTrack.putString("language", format.language != null ? format.language : "");
|
||||||
|
textTracks.pushMap(textTrack);
|
||||||
|
}
|
||||||
|
return textTracks;
|
||||||
|
}
|
||||||
|
|
||||||
private void onBuffering(boolean buffering) {
|
private void onBuffering(boolean buffering) {
|
||||||
if (isBuffering == buffering) {
|
if (isBuffering == buffering) {
|
||||||
return;
|
return;
|
||||||
@ -627,6 +697,11 @@ class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTextTracks(ReadableArray textTracks) {
|
||||||
|
this.textTracks = textTracks;
|
||||||
|
reloadSource();
|
||||||
|
}
|
||||||
|
|
||||||
private void reloadSource() {
|
private void reloadSource() {
|
||||||
playerNeedsSource = true;
|
playerNeedsSource = true;
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
|
@ -5,6 +5,7 @@ import android.net.Uri;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.facebook.react.bridge.Dynamic;
|
import com.facebook.react.bridge.Dynamic;
|
||||||
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
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;
|
||||||
@ -30,6 +31,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
|||||||
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
|
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_TYPE = "type";
|
||||||
private static final String PROP_SELECTED_TEXT_TRACK_VALUE = "value";
|
private static final String PROP_SELECTED_TEXT_TRACK_VALUE = "value";
|
||||||
|
private static final String PROP_TEXT_TRACKS = "textTracks";
|
||||||
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";
|
||||||
@ -128,13 +130,23 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
|||||||
@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
|
@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
|
||||||
public void setSelectedTextTrack(final ReactExoplayerView videoView,
|
public void setSelectedTextTrack(final ReactExoplayerView videoView,
|
||||||
@Nullable ReadableMap selectedTextTrack) {
|
@Nullable ReadableMap selectedTextTrack) {
|
||||||
String typeString = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)
|
String typeString = null;
|
||||||
|
Dynamic value = null;
|
||||||
|
if (selectedTextTrack != null) {
|
||||||
|
typeString = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)
|
||||||
? selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) : null;
|
? selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) : null;
|
||||||
Dynamic value = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)
|
value = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)
|
||||||
? selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) : null;
|
? selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) : null;
|
||||||
|
}
|
||||||
videoView.setSelectedTextTrack(typeString, value);
|
videoView.setSelectedTextTrack(typeString, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReactProp(name = PROP_TEXT_TRACKS)
|
||||||
|
public void setPropTextTracks(final ReactExoplayerView videoView,
|
||||||
|
@Nullable ReadableArray textTracks) {
|
||||||
|
videoView.setTextTracks(textTracks);
|
||||||
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
@ -109,6 +109,7 @@ class VideoEventEmitter {
|
|||||||
private static final String EVENT_PROP_WIDTH = "width";
|
private static final String EVENT_PROP_WIDTH = "width";
|
||||||
private static final String EVENT_PROP_HEIGHT = "height";
|
private static final String EVENT_PROP_HEIGHT = "height";
|
||||||
private static final String EVENT_PROP_ORIENTATION = "orientation";
|
private static final String EVENT_PROP_ORIENTATION = "orientation";
|
||||||
|
private static final String EVENT_PROP_TEXT_TRACKS = "textTracks";
|
||||||
private static final String EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus";
|
private static final String EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus";
|
||||||
private static final String EVENT_PROP_IS_BUFFERING = "isBuffering";
|
private static final String EVENT_PROP_IS_BUFFERING = "isBuffering";
|
||||||
private static final String EVENT_PROP_PLAYBACK_RATE = "playbackRate";
|
private static final String EVENT_PROP_PLAYBACK_RATE = "playbackRate";
|
||||||
@ -128,7 +129,8 @@ class VideoEventEmitter {
|
|||||||
receiveEvent(EVENT_LOAD_START, null);
|
receiveEvent(EVENT_LOAD_START, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void load(double duration, double currentPosition, int videoWidth, int videoHeight) {
|
void load(double duration, double currentPosition, int videoWidth, int videoHeight,
|
||||||
|
WritableArray textTracks) {
|
||||||
WritableMap event = Arguments.createMap();
|
WritableMap event = Arguments.createMap();
|
||||||
event.putDouble(EVENT_PROP_DURATION, duration / 1000D);
|
event.putDouble(EVENT_PROP_DURATION, duration / 1000D);
|
||||||
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
|
||||||
@ -143,6 +145,8 @@ class VideoEventEmitter {
|
|||||||
}
|
}
|
||||||
event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize);
|
event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize);
|
||||||
|
|
||||||
|
event.putArray(EVENT_PROP_TEXT_TRACKS, textTracks);
|
||||||
|
|
||||||
// TODO: Actually check if you can.
|
// TODO: Actually check if you can.
|
||||||
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
|
||||||
event.putBoolean(EVENT_PROP_SLOW_FORWARD, true);
|
event.putBoolean(EVENT_PROP_SLOW_FORWARD, true);
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
def _ext = rootProject.ext
|
||||||
|
|
||||||
|
def _reactNativeVersion = _ext.has('reactNative') ? _ext.reactNative : '+'
|
||||||
|
def _compileSdkVersion = _ext.has('compileSdkVersion') ? _ext.compileSdkVersion : 27
|
||||||
|
def _buildToolsVersion = _ext.has('buildToolsVersion') ? _ext.buildToolsVersion : '27.0.3'
|
||||||
|
def _minSdkVersion = _ext.has('minSdkVersion') ? _ext.minSdkVersion : 16
|
||||||
|
def _targetSdkVersion = _ext.has('targetSdkVersion') ? _ext.targetSdkVersion : 27
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileSdkVersion _compileSdkVersion
|
||||||
buildToolsVersion "25.0.2"
|
buildToolsVersion _buildToolsVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion _minSdkVersion
|
||||||
targetSdkVersion 25
|
targetSdkVersion _targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
ndk {
|
ndk {
|
||||||
@ -17,6 +25,6 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDynamicVersion
|
//noinspection GradleDynamicVersion
|
||||||
provided 'com.facebook.react:react-native:+'
|
provided "com.facebook.react:react-native:${_reactNativeVersion}"
|
||||||
compile 'com.yqritc:android-scalablevideoview:1.0.4'
|
compile 'com.yqritc:android-scalablevideoview:1.0.4'
|
||||||
}
|
}
|
||||||
|
@ -105,8 +105,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
private float mRate = 1.0f;
|
private float mRate = 1.0f;
|
||||||
private float mActiveRate = 1.0f;
|
private float mActiveRate = 1.0f;
|
||||||
private boolean mPlayInBackground = false;
|
private boolean mPlayInBackground = false;
|
||||||
private boolean mActiveStatePauseStatus = false;
|
private boolean mBackgroundPaused = false;
|
||||||
private boolean mActiveStatePauseStatusInitialized = false;
|
|
||||||
|
|
||||||
private int mMainVer = 0;
|
private int mMainVer = 0;
|
||||||
private int mPatchVer = 0;
|
private int mPatchVer = 0;
|
||||||
@ -132,7 +131,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
if (mMediaPlayerValid && !isCompleted &&!mPaused) {
|
if (mMediaPlayerValid && !isCompleted && !mPaused && !mBackgroundPaused) {
|
||||||
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
|
||||||
@ -348,11 +347,6 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
|
|
||||||
mPaused = paused;
|
mPaused = paused;
|
||||||
|
|
||||||
if ( !mActiveStatePauseStatusInitialized ) {
|
|
||||||
mActiveStatePauseStatus = mPaused;
|
|
||||||
mActiveStatePauseStatusInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mMediaPlayerValid) {
|
if (!mMediaPlayerValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -424,8 +418,16 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
if (mMediaPlayerValid) {
|
if (mMediaPlayerValid) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (!mPaused) { // Applying the rate while paused will cause the video to start
|
if (!mPaused) { // Applying the rate while paused will cause the video to start
|
||||||
|
/* Per https://stackoverflow.com/questions/39442522/setplaybackparams-causes-illegalstateexception
|
||||||
|
* Some devices throw an IllegalStateException if you set the rate without first calling reset()
|
||||||
|
* TODO: Call reset() then reinitialize the player
|
||||||
|
*/
|
||||||
|
try {
|
||||||
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
|
mMediaPlayer.setPlaybackParams(mMediaPlayer.getPlaybackParams().setSpeed(rate));
|
||||||
mActiveRate = rate;
|
mActiveRate = rate;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(ReactVideoViewManager.REACT_CLASS, "Unable to set rate, unsupported on this device");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0");
|
Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android versions below 6.0");
|
||||||
@ -603,25 +605,27 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onHostPause() {
|
public void onHostPause() {
|
||||||
if (mMediaPlayer != null && !mPlayInBackground) {
|
if (mMediaPlayerValid && !mPaused && !mPlayInBackground) {
|
||||||
mActiveStatePauseStatus = mPaused;
|
/* Pause the video in background
|
||||||
|
* Don't update the paused prop, developers should be able to update it on background
|
||||||
// Pause the video in background
|
* so that when you return to the app the video is paused
|
||||||
setPausedModifier(true);
|
*/
|
||||||
|
mBackgroundPaused = true;
|
||||||
|
mMediaPlayer.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onHostResume() {
|
public void onHostResume() {
|
||||||
if (mMediaPlayer != null && !mPlayInBackground) {
|
mBackgroundPaused = false;
|
||||||
|
if (mMediaPlayerValid && !mPlayInBackground && !mPaused) {
|
||||||
new Handler().post(new Runnable() {
|
new Handler().post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Restore original state
|
// Restore original state
|
||||||
setPausedModifier(mActiveStatePauseStatus);
|
setPausedModifier(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
BOOL _playerItemObserversSet;
|
BOOL _playerItemObserversSet;
|
||||||
BOOL _playerBufferEmpty;
|
BOOL _playerBufferEmpty;
|
||||||
AVPlayerLayer *_playerLayer;
|
AVPlayerLayer *_playerLayer;
|
||||||
|
BOOL _playerLayerObserverSet;
|
||||||
AVPlayerViewController *_playerViewController;
|
AVPlayerViewController *_playerViewController;
|
||||||
NSURL *_videoURL;
|
NSURL *_videoURL;
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
BOOL _muted;
|
BOOL _muted;
|
||||||
BOOL _paused;
|
BOOL _paused;
|
||||||
BOOL _repeat;
|
BOOL _repeat;
|
||||||
|
BOOL _allowsExternalPlayback;
|
||||||
NSDictionary * _selectedTextTrack;
|
NSDictionary * _selectedTextTrack;
|
||||||
BOOL _playbackStalled;
|
BOOL _playbackStalled;
|
||||||
BOOL _playInBackground;
|
BOOL _playInBackground;
|
||||||
@ -67,6 +69,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
_controls = NO;
|
_controls = NO;
|
||||||
_playerBufferEmpty = YES;
|
_playerBufferEmpty = YES;
|
||||||
_playInBackground = false;
|
_playInBackground = false;
|
||||||
|
_allowsExternalPlayback = YES;
|
||||||
_playWhenInactive = false;
|
_playWhenInactive = false;
|
||||||
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
|
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
|
||||||
|
|
||||||
@ -420,6 +423,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
@"height": height,
|
@"height": height,
|
||||||
@"orientation": orientation
|
@"orientation": orientation
|
||||||
},
|
},
|
||||||
|
@"textTracks": [self getTextTrackInfo],
|
||||||
@"target": self.reactTag});
|
@"target": self.reactTag});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -530,6 +534,12 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
_playInBackground = playInBackground;
|
_playInBackground = playInBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback
|
||||||
|
{
|
||||||
|
_allowsExternalPlayback = allowsExternalPlayback;
|
||||||
|
_player.allowsExternalPlayback = _allowsExternalPlayback;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setPlayWhenInactive:(BOOL)playWhenInactive
|
- (void)setPlayWhenInactive:(BOOL)playWhenInactive
|
||||||
{
|
{
|
||||||
_playWhenInactive = playWhenInactive;
|
_playWhenInactive = playWhenInactive;
|
||||||
@ -566,21 +576,28 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
|
|
||||||
- (void)setCurrentTime:(float)currentTime
|
- (void)setCurrentTime:(float)currentTime
|
||||||
{
|
{
|
||||||
[self setSeek: currentTime];
|
NSDictionary *info = @{
|
||||||
|
@"time": [NSNumber numberWithFloat:currentTime],
|
||||||
|
@"tolerance": [NSNumber numberWithInt:100]
|
||||||
|
};
|
||||||
|
[self setSeek:info];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setSeek:(float)seekTime
|
- (void)setSeek:(NSDictionary *)info
|
||||||
{
|
{
|
||||||
int timeScale = 10000;
|
NSNumber *seekTime = info[@"time"];
|
||||||
|
NSNumber *seekTolerance = info[@"tolerance"];
|
||||||
|
|
||||||
|
int timeScale = 1000;
|
||||||
|
|
||||||
AVPlayerItem *item = _player.currentItem;
|
AVPlayerItem *item = _player.currentItem;
|
||||||
if (item && item.status == AVPlayerItemStatusReadyToPlay) {
|
if (item && item.status == AVPlayerItemStatusReadyToPlay) {
|
||||||
// TODO check loadedTimeRanges
|
// TODO check loadedTimeRanges
|
||||||
|
|
||||||
CMTime cmSeekTime = CMTimeMakeWithSeconds(seekTime, timeScale);
|
CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale);
|
||||||
CMTime current = item.currentTime;
|
CMTime current = item.currentTime;
|
||||||
// TODO figure out a good tolerance level
|
// TODO figure out a good tolerance level
|
||||||
CMTime tolerance = CMTimeMake(1000, timeScale);
|
CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale);
|
||||||
BOOL wasPaused = _paused;
|
BOOL wasPaused = _paused;
|
||||||
|
|
||||||
if (CMTimeCompare(current, cmSeekTime) != 0) {
|
if (CMTimeCompare(current, cmSeekTime) != 0) {
|
||||||
@ -594,7 +611,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
}
|
}
|
||||||
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": seekTime,
|
||||||
@"target": self.reactTag});
|
@"target": self.reactTag});
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
@ -605,7 +622,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
} else {
|
} else {
|
||||||
// TODO: See if this makes sense and if so, actually implement it
|
// TODO: See if this makes sense and if so, actually implement it
|
||||||
_pendingSeek = true;
|
_pendingSeek = true;
|
||||||
_pendingSeekTime = seekTime;
|
_pendingSeekTime = [seekTime floatValue];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,6 +659,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
[self setRepeat:_repeat];
|
[self setRepeat:_repeat];
|
||||||
[self setPaused:_paused];
|
[self setPaused:_paused];
|
||||||
[self setControls:_controls];
|
[self setControls:_controls];
|
||||||
|
[self setAllowsExternalPlayback:_allowsExternalPlayback];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setRepeat:(BOOL)repeat {
|
- (void)setRepeat:(BOOL)repeat {
|
||||||
@ -692,6 +710,29 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
[_player.currentItem selectMediaOption:option inMediaSelectionGroup:group];
|
[_player.currentItem selectMediaOption:option inMediaSelectionGroup:group];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSArray *)getTextTrackInfo
|
||||||
|
{
|
||||||
|
NSMutableArray *textTracks = [[NSMutableArray alloc] init];
|
||||||
|
AVMediaSelectionGroup *group = [_player.currentItem.asset
|
||||||
|
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
|
||||||
|
for (int i = 0; i < group.options.count; ++i) {
|
||||||
|
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i];
|
||||||
|
NSString *title = @"";
|
||||||
|
NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"];
|
||||||
|
if (values.count > 0) {
|
||||||
|
title = [values objectAtIndex:0];
|
||||||
|
}
|
||||||
|
NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @"";
|
||||||
|
NSDictionary *textTrack = @{
|
||||||
|
@"index": [NSNumber numberWithInt:i],
|
||||||
|
@"title": title,
|
||||||
|
@"language": language
|
||||||
|
};
|
||||||
|
[textTracks addObject:textTrack];
|
||||||
|
}
|
||||||
|
return textTracks;
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)getFullscreen
|
- (BOOL)getFullscreen
|
||||||
{
|
{
|
||||||
return _fullscreenPlayerPresented;
|
return _fullscreenPlayerPresented;
|
||||||
@ -768,6 +809,7 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
// resize mode must be set before layer is added
|
// resize mode must be set before layer is added
|
||||||
[self setResizeMode:_resizeMode];
|
[self setResizeMode:_resizeMode];
|
||||||
[_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
|
[_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil];
|
||||||
|
_playerLayerObserverSet = YES;
|
||||||
|
|
||||||
[self.layer addSublayer:_playerLayer];
|
[self.layer addSublayer:_playerLayer];
|
||||||
self.layer.needsDisplayOnBoundsChange = YES;
|
self.layer.needsDisplayOnBoundsChange = YES;
|
||||||
@ -806,7 +848,10 @@ static NSString *const timedMetadata = @"timedMetadata";
|
|||||||
- (void)removePlayerLayer
|
- (void)removePlayerLayer
|
||||||
{
|
{
|
||||||
[_playerLayer removeFromSuperlayer];
|
[_playerLayer removeFromSuperlayer];
|
||||||
|
if (_playerLayerObserverSet) {
|
||||||
[_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath];
|
[_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath];
|
||||||
|
_playerLayerObserverSet = NO;
|
||||||
|
}
|
||||||
_playerLayer = nil;
|
_playerLayer = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(allowsExternalPlayback, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary);
|
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);
|
||||||
@ -31,7 +32,7 @@ RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
|
|||||||
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(seek, float);
|
RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(currentTime, float);
|
RCT_EXPORT_VIEW_PROPERTY(currentTime, float);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL);
|
RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL);
|
||||||
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-native-video",
|
"name": "react-native-video",
|
||||||
"version": "2.2.0",
|
"version": "2.3.1",
|
||||||
"description": "A <Video /> element for react-native",
|
"description": "A <Video /> element for react-native",
|
||||||
"main": "Video.js",
|
"main": "Video.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -40,5 +40,10 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node_modules/.bin/eslint *.js"
|
"test": "node_modules/.bin/eslint *.js"
|
||||||
|
},
|
||||||
|
"rnpm": {
|
||||||
|
"android": {
|
||||||
|
"sourceDir": "./android-exoplayer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user