Merge branch 'master' into master
This commit is contained in:
commit
45a851e79a
26
CHANGELOG.md
26
CHANGELOG.md
@ -1,6 +1,27 @@
|
||||
## Changelog
|
||||
|
||||
### Next Version
|
||||
* Fix iOS video not displaying after switching source [#1395](https://github.com/react-native-community/react-native-video/pull/1395)
|
||||
* Add the filterEnabled flag, fixes iOS video start time regression [#1384](https://github.com/react-native-community/react-native-video/pull/1384)
|
||||
* Fix text not appearing in release builds of Android apps [#1373](https://github.com/react-native-community/react-native-video/pull/1373)
|
||||
* Update to ExoPlayer 2.9.3 [#1406](https://github.com/react-native-community/react-native-video/pull/1406)
|
||||
|
||||
### Version 4.2.0
|
||||
* Don't initialize filters on iOS unless a filter is set. This was causing a startup performance regression [#1360](https://github.com/react-native-community/react-native-video/pull/1360)
|
||||
* Support setting the maxBitRate [#1310](https://github.com/react-native-community/react-native-video/pull/1310)
|
||||
* Fix useTextureView not defaulting to true [#1383](https://github.com/react-native-community/react-native-video/pull/1383)
|
||||
* Fix crash on MediaPlayer w/ Android 4.4 & avoid memory leak [#1328](https://github.com/react-native-community/react-native-video/pull/1328)
|
||||
|
||||
### Version 4.1.0
|
||||
* Generate onSeek on Android ExoPlayer & MediaPlayer after seek completes [#1351](https://github.com/react-native-community/react-native-video/pull/1351)
|
||||
* Remove unneeded onVideoSaved event [#1350](https://github.com/react-native-community/react-native-video/pull/1350)
|
||||
* Disable AirPlay if sidecar text tracks are enabled [#1304](https://github.com/react-native-community/react-native-video/pull/1304)
|
||||
* Add possibility to remove black screen while video is loading in Exoplayer [#1355](https://github.com/react-native-community/react-native-video/pull/1355)
|
||||
|
||||
### Version 4.0.1
|
||||
* Add missing files to package.json [#1342](https://github.com/react-native-community/react-native-video/pull/1342)
|
||||
|
||||
### Version 4.0.0
|
||||
* Partial support for timed metadata on Android MediaPlayer [#707](https://github.com/react-native-community/react-native-video/pull/707)
|
||||
* Support video caching for iOS [#955](https://github.com/react-native-community/react-native-video/pull/955)
|
||||
* Video caching cleanups [#1172](https://github.com/react-native-community/react-native-video/pull/1172)
|
||||
@ -15,6 +36,11 @@
|
||||
* Add fullscreenOrientation option for iOS [#1215](https://github.com/react-native-community/react-native-video/pull/1215)
|
||||
* Update to ExoPlayer 2.9.0 [#1285](https://github.com/react-native-community/react-native-video/pull/1285)
|
||||
* Switch useTextureView to default to `true` [#1286](https://github.com/react-native-community/react-native-video/pull/1286)
|
||||
* Re-add fullscreenAutorotate prop [#1303](https://github.com/react-native-community/react-native-video/pull/1303)
|
||||
* Make seek throw a useful error for NaN values [#1283](https://github.com/react-native-community/react-native-video/pull/1283)
|
||||
* Video Filters and Save Video [#1306](https://github.com/react-native-community/react-native-video/pull/1306)
|
||||
* Fix: volume should not change on onAudioFocusChange event [#1327](https://github.com/react-native-community/react-native-video/pull/1327)
|
||||
* Update ExoPlayer to 2.9.1 and OkHTTP to 3.12.0 [#1338](https://github.com/react-native-community/react-native-video/pull/1338)
|
||||
|
||||
### Version 3.2.0
|
||||
* Basic fullscreen support for Android MediaPlayer [#1138](https://github.com/react-native-community/react-native-video/pull/1138)
|
||||
|
18
FilterType.js
Normal file
18
FilterType.js
Normal file
@ -0,0 +1,18 @@
|
||||
export default {
|
||||
NONE: '',
|
||||
INVERT: 'CIColorInvert',
|
||||
MONOCHROME: 'CIColorMonochrome',
|
||||
POSTERIZE: 'CIColorPosterize',
|
||||
FALSE: 'CIFalseColor',
|
||||
MAXIMUMCOMPONENT: 'CIMaximumComponent',
|
||||
MINIMUMCOMPONENT: 'CIMinimumComponent',
|
||||
CHROME: 'CIPhotoEffectChrome',
|
||||
FADE: 'CIPhotoEffectFade',
|
||||
INSTANT: 'CIPhotoEffectInstant',
|
||||
MONO: 'CIPhotoEffectMono',
|
||||
NOIR: 'CIPhotoEffectNoir',
|
||||
PROCESS: 'CIPhotoEffectProcess',
|
||||
TONAL: 'CIPhotoEffectTonal',
|
||||
TRANSFER: 'CIPhotoEffectTransfer',
|
||||
SEPIA: 'CISepiaTone'
|
||||
};
|
134
README.md
134
README.md
@ -259,11 +259,16 @@ var styles = StyleSheet.create({
|
||||
* [audioOnly](#audioonly)
|
||||
* [bufferConfig](#bufferconfig)
|
||||
* [controls](#controls)
|
||||
* [filter](#filter)
|
||||
* [filterEnabled](#filterEnabled)
|
||||
* [fullscreen](#fullscreen)
|
||||
* [fullscreenAutorotate](#fullscreenautorotate)
|
||||
* [fullscreenOrientation](#fullscreenorientation)
|
||||
* [headers](#headers)
|
||||
* [hideShutterView](#hideshutterview)
|
||||
* [id](#id)
|
||||
* [ignoreSilentSwitch](#ignoresilentswitch)
|
||||
* [maxBitRate](#maxbitrate)
|
||||
* [muted](#muted)
|
||||
* [paused](#paused)
|
||||
* [playInBackground](#playinbackground)
|
||||
@ -293,11 +298,13 @@ var styles = StyleSheet.create({
|
||||
* [onLoad](#onload)
|
||||
* [onLoadStart](#onloadstart)
|
||||
* [onProgress](#onprogress)
|
||||
* [onSeek](#onseek)
|
||||
* [onTimedMetadata](#ontimedmetadata)
|
||||
|
||||
### Methods
|
||||
* [dismissFullscreenPlayer](#dismissfullscreenplayer)
|
||||
* [presentFullscreenPlayer](#presentfullscreenplayer)
|
||||
* [save](#save)
|
||||
* [seek](#seek)
|
||||
|
||||
### Configurable props
|
||||
@ -349,8 +356,46 @@ Determines whether to show player controls.
|
||||
|
||||
Note on iOS, controls are always shown when in fullscreen mode.
|
||||
|
||||
Controls are not available Android because the system does not provide a stock set of controls. You will need to build your own or use a package like [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls) or [react-native-video-player](https://github.com/cornedor/react-native-video-player).
|
||||
|
||||
Platforms: iOS, react-native-dom
|
||||
|
||||
#### filter
|
||||
Add video filter
|
||||
* **FilterType.NONE (default)** - No Filter
|
||||
* **FilterType.INVERT** - CIColorInvert
|
||||
* **FilterType.MONOCHROME** - CIColorMonochrome
|
||||
* **FilterType.POSTERIZE** - CIColorPosterize
|
||||
* **FilterType.FALSE** - CIFalseColor
|
||||
* **FilterType.MAXIMUMCOMPONENT** - CIMaximumComponent
|
||||
* **FilterType.MINIMUMCOMPONENT** - CIMinimumComponent
|
||||
* **FilterType.CHROME** - CIPhotoEffectChrome
|
||||
* **FilterType.FADE** - CIPhotoEffectFade
|
||||
* **FilterType.INSTANT** - CIPhotoEffectInstant
|
||||
* **FilterType.MONO** - CIPhotoEffectMono
|
||||
* **FilterType.NOIR** - CIPhotoEffectNoir
|
||||
* **FilterType.PROCESS** - CIPhotoEffectProcess
|
||||
* **FilterType.TONAL** - CIPhotoEffectTonal
|
||||
* **FilterType.TRANSFER** - CIPhotoEffectTransfer
|
||||
* **FilterType.SEPIA** - CISepiaTone
|
||||
|
||||
For more details on these filters refer to the [iOS docs](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW55).
|
||||
|
||||
Notes:
|
||||
1. Using a filter can impact CPU usage. A workaround is to save the video with the filter and then load the saved video.
|
||||
2. Video filter is currently not supported on HLS playlists.
|
||||
3. `filterEnabled` must be set to `true`
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### filterEnabled
|
||||
Enable video filter.
|
||||
|
||||
* **false (default)** - Don't enable filter
|
||||
* **true** - Enable filter
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### fullscreen
|
||||
Controls whether the player enters fullscreen on play.
|
||||
* **false (default)** - Don't display the video in fullscreen
|
||||
@ -358,6 +403,11 @@ Controls whether the player enters fullscreen on play.
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### fullscreenAutorotate
|
||||
If a preferred [fullscreenOrientation](#fullscreenorientation) is set, causes the video to rotate to that orientation but permits rotation of the screen to orientation held by user. Defaults to TRUE.
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### fullscreenOrientation
|
||||
|
||||
* **all (default)** -
|
||||
@ -381,6 +431,14 @@ headers={{
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
|
||||
#### hideShutterView
|
||||
Controls whether the ExoPlayer shutter view (black screen while loading) is enabled.
|
||||
|
||||
* **false (default)** - Show shutter view
|
||||
* **true** - Hide shutter view
|
||||
|
||||
Platforms: Android ExoPlayer
|
||||
|
||||
#### id
|
||||
Set the DOM id element so you can use document.getElementById on web platforms. Accepts string values.
|
||||
|
||||
@ -399,6 +457,18 @@ Controls the iOS silent switch behavior
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### maxBitRate
|
||||
Sets the desired limit, in bits per second, of network bandwidth consumption when multiple video streams are available for a playlist.
|
||||
|
||||
Default: 0. Don't limit the maxBitRate.
|
||||
|
||||
Example:
|
||||
```
|
||||
maxBitRate={2000000} // 2 megabits
|
||||
```
|
||||
|
||||
Platforms: Android ExoPlayer, iOS
|
||||
|
||||
#### muted
|
||||
Controls whether the audio is muted
|
||||
* **false (default)** - Don't mute audio
|
||||
@ -548,6 +618,7 @@ Sets the media source. You can pass an asset loaded via require or an object wit
|
||||
|
||||
The docs for this prop are incomplete and will be updated as each option is investigated and tested.
|
||||
|
||||
|
||||
##### Asset loaded via require
|
||||
|
||||
Example:
|
||||
@ -565,7 +636,7 @@ A number of URI schemes are supported by passing an object with a `uri` attribut
|
||||
|
||||
Example:
|
||||
```
|
||||
source={ uri: 'https://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_10mb.mp4' }
|
||||
source={{uri: 'https://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_10mb.mp4' }}
|
||||
```
|
||||
|
||||
Platforms: all
|
||||
@ -574,7 +645,7 @@ Platforms: all
|
||||
|
||||
Example:
|
||||
```
|
||||
source={ uri: 'file:///sdcard/Movies/sintel.mp4' }
|
||||
source={{ uri: 'file:///sdcard/Movies/sintel.mp4' }}
|
||||
```
|
||||
|
||||
Note: Your app will need to request permission to read external storage if you're accessing a file outside your app.
|
||||
@ -587,7 +658,7 @@ Path to a sound file in your iTunes library. Typically shared from iTunes to you
|
||||
|
||||
Example:
|
||||
```
|
||||
source={ uri: 'ipod-library:///path/to/music.mp3' }
|
||||
source={{ uri: 'ipod-library:///path/to/music.mp3' }}
|
||||
```
|
||||
|
||||
Note: Using this feature adding an entry for NSAppleMusicUsageDescription to your Info.plist file as described [here](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html)
|
||||
@ -620,6 +691,8 @@ uri | URL for the text track. Currently, only tracks hosted on a webserver are s
|
||||
|
||||
On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.
|
||||
|
||||
Note: Due to iOS limitations, sidecar text tracks are not compatible with Airplay. If textTracks are specified, AirPlay support will be automatically disabled.
|
||||
|
||||
Example:
|
||||
```
|
||||
import { TextTrackType }, Video from 'react-native-video';
|
||||
@ -665,6 +738,7 @@ Adjust the volume.
|
||||
|
||||
Platforms: all
|
||||
|
||||
|
||||
### Event props
|
||||
|
||||
#### onAudioBecomingNoisy
|
||||
@ -812,6 +886,29 @@ Example:
|
||||
|
||||
Platforms: all
|
||||
|
||||
#### onSeek
|
||||
Callback function that is called when a seek completes.
|
||||
|
||||
Payload:
|
||||
|
||||
Property | Type | Description
|
||||
--- | --- | ---
|
||||
currentTime | number | The current time after the seek
|
||||
seekTime | number | The requested time
|
||||
|
||||
Example:
|
||||
```
|
||||
{
|
||||
currentTime: 100.5
|
||||
seekTime: 100
|
||||
}
|
||||
```
|
||||
|
||||
Both the currentTime & seekTime are reported because the video player may not seek to the exact requested position in order to improve seek performance.
|
||||
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP
|
||||
|
||||
#### onTimedMetadata
|
||||
Callback function that is called when timed metadata becomes available
|
||||
|
||||
@ -873,12 +970,39 @@ this.player.presentFullscreenPlayer();
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
|
||||
#### save
|
||||
`save(): Promise`
|
||||
|
||||
Save video to your Photos with current filter prop. Returns promise.
|
||||
|
||||
Example:
|
||||
```
|
||||
let response = await this.save();
|
||||
let path = response.uri;
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Currently only supports highest quality export
|
||||
- Currently only supports MP4 export
|
||||
- Currently only supports exporting to user's cache directory with a generated UUID filename.
|
||||
- User will need to remove the saved video through their Photos app
|
||||
- Works with cached videos as well. (Checkout video-caching example)
|
||||
- If the video is has not began buffering (e.g. there is no internet connection) then the save function will throw an error.
|
||||
- If the video is buffering then the save function promise will return after the video has finished buffering and processing.
|
||||
|
||||
Future:
|
||||
- Will support multiple qualities through options
|
||||
- Will support more formats in the future through options
|
||||
- Will support custom directory and file name through options
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### 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.
|
||||
`seek()` can only be called after the `onLoad` event has fired. Once completed, the [onSeek](#onseek) event will be called.
|
||||
|
||||
Example:
|
||||
```
|
||||
@ -903,6 +1027,8 @@ this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accurac
|
||||
Platforms: iOS
|
||||
|
||||
|
||||
|
||||
|
||||
### iOS App Transport Security
|
||||
|
||||
- By default, iOS will only load encrypted (https) urls. If you want to load content from an unencrypted (http) source, you will need to modify your Info.plist file and add the following entry:
|
||||
|
33
Video.js
33
Video.js
@ -1,8 +1,9 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform} from 'react-native';
|
||||
import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle} from 'react-native';
|
||||
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
|
||||
import TextTrackType from './TextTrackType';
|
||||
import FilterType from './FilterType';
|
||||
import VideoResizeMode from './VideoResizeMode.js';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@ -11,7 +12,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export { TextTrackType };
|
||||
export { TextTrackType, FilterType };
|
||||
|
||||
export default class Video extends Component {
|
||||
|
||||
@ -51,6 +52,8 @@ export default class Video extends Component {
|
||||
}
|
||||
|
||||
seek = (time, tolerance = 100) => {
|
||||
if (isNaN(time)) throw new Error('Specified time is not a number');
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
this.setNativeProps({
|
||||
seek: {
|
||||
@ -71,6 +74,10 @@ export default class Video extends Component {
|
||||
this.setNativeProps({ fullscreen: false });
|
||||
};
|
||||
|
||||
save = async (options?) => {
|
||||
return await NativeModules.VideoManager.save(options, findNodeHandle(this._root));
|
||||
}
|
||||
|
||||
_assignRoot = (component) => {
|
||||
this._root = component;
|
||||
};
|
||||
@ -282,6 +289,25 @@ export default class Video extends Component {
|
||||
}
|
||||
|
||||
Video.propTypes = {
|
||||
filter: PropTypes.oneOf([
|
||||
FilterType.NONE,
|
||||
FilterType.INVERT,
|
||||
FilterType.MONOCHROME,
|
||||
FilterType.POSTERIZE,
|
||||
FilterType.FALSE,
|
||||
FilterType.MAXIMUMCOMPONENT,
|
||||
FilterType.MINIMUMCOMPONENT,
|
||||
FilterType.CHROME,
|
||||
FilterType.FADE,
|
||||
FilterType.INSTANT,
|
||||
FilterType.MONO,
|
||||
FilterType.NOIR,
|
||||
FilterType.PROCESS,
|
||||
FilterType.TONAL,
|
||||
FilterType.TRANSFER,
|
||||
FilterType.SEPIA
|
||||
]),
|
||||
filterEnabled: PropTypes.bool,
|
||||
/* Native only */
|
||||
src: PropTypes.object,
|
||||
seek: PropTypes.oneOfType([
|
||||
@ -313,6 +339,7 @@ Video.propTypes = {
|
||||
// Opaque type returned by require('./video.mp4')
|
||||
PropTypes.number
|
||||
]),
|
||||
maxBitRate: PropTypes.number,
|
||||
resizeMode: PropTypes.string,
|
||||
poster: PropTypes.string,
|
||||
posterResizeMode: Image.propTypes.resizeMode,
|
||||
@ -370,9 +397,11 @@ Video.propTypes = {
|
||||
controls: PropTypes.bool,
|
||||
audioOnly: PropTypes.bool,
|
||||
currentTime: PropTypes.number,
|
||||
fullscreenAutorotate: PropTypes.bool,
|
||||
fullscreenOrientation: PropTypes.oneOf(['all','landscape','portrait']),
|
||||
progressUpdateInterval: PropTypes.number,
|
||||
useTextureView: PropTypes.bool,
|
||||
hideShutterView: PropTypes.bool,
|
||||
onLoadStart: PropTypes.func,
|
||||
onLoad: PropTypes.func,
|
||||
onBuffer: PropTypes.func,
|
||||
|
@ -18,8 +18,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
compileOnly "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}"
|
||||
implementation('com.google.android.exoplayer:exoplayer:2.9.0') {
|
||||
exclude group: 'com.android.support'
|
||||
implementation('com.google.android.exoplayer:exoplayer:2.9.3') {
|
||||
exclude group: 'com.android.support'
|
||||
}
|
||||
implementation project(':exoplayer-library-core')
|
||||
implementation project(':exoplayer-library-dash')
|
||||
@ -33,9 +33,9 @@ dependencies {
|
||||
implementation "com.android.support:support-compat:${safeExtGet('supportLibVersion', '+')}"
|
||||
implementation "com.android.support:support-media-compat:${safeExtGet('supportLibVersion', '+')}"
|
||||
|
||||
implementation('com.google.android.exoplayer:extension-okhttp:2.9.0') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
implementation('com.google.android.exoplayer:extension-okhttp:2.9.3') {
|
||||
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
|
||||
}
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||
|
||||
}
|
||||
|
@ -38,7 +38,8 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
private Context context;
|
||||
private ViewGroup.LayoutParams layoutParams;
|
||||
|
||||
private boolean useTextureView = false;
|
||||
private boolean useTextureView = true;
|
||||
private boolean hideShutterView = false;
|
||||
|
||||
public ExoPlayerView(Context context) {
|
||||
this(context, null);
|
||||
@ -106,6 +107,10 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShutterViewVisibility() {
|
||||
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
|
||||
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
|
||||
@ -157,8 +162,15 @@ public final class ExoPlayerView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setUseTextureView(boolean useTextureView) {
|
||||
this.useTextureView = useTextureView;
|
||||
updateSurfaceView();
|
||||
if (useTextureView != this.useTextureView) {
|
||||
this.useTextureView = useTextureView;
|
||||
updateSurfaceView();
|
||||
}
|
||||
}
|
||||
|
||||
public void setHideShutterView(boolean hideShutterView) {
|
||||
this.hideShutterView = hideShutterView;
|
||||
updateShutterViewVisibility();
|
||||
}
|
||||
|
||||
private final Runnable measureAndLayout = new Runnable() {
|
||||
|
@ -113,6 +113,9 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
private boolean isPaused;
|
||||
private boolean isBuffering;
|
||||
private float rate = 1f;
|
||||
private float audioVolume = 1f;
|
||||
private int maxBitRate = 0;
|
||||
private long seekTime = C.TIME_UNSET;
|
||||
|
||||
private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
|
||||
private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
|
||||
@ -134,7 +137,6 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
private boolean disableFocus;
|
||||
private float mProgressUpdateInterval = 250.0f;
|
||||
private boolean playInBackground = false;
|
||||
private boolean useTextureView = false;
|
||||
private Map<String, String> requestHeaders;
|
||||
private boolean mReportBandwidth = false;
|
||||
// \ End props
|
||||
@ -259,6 +261,9 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
if (player == null) {
|
||||
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
||||
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
|
||||
|
||||
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||
DefaultLoadControl defaultLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, -1, true);
|
||||
player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, defaultLoadControl);
|
||||
@ -472,10 +477,10 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
if (player != null) {
|
||||
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
|
||||
// Lower the volume
|
||||
player.setVolume(0.8f);
|
||||
player.setVolume(audioVolume * 0.8f);
|
||||
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
|
||||
// Raise it back to normal
|
||||
player.setVolume(1);
|
||||
player.setVolume(audioVolume * 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -652,7 +657,8 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
@Override
|
||||
public void onSeekProcessed() {
|
||||
// Do nothing.
|
||||
eventEmitter.seek(player.getCurrentPosition(), seekTime);
|
||||
seekTime = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -779,7 +785,7 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
this.srcUri = uri;
|
||||
this.extension = extension;
|
||||
this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(this.themedReactContext);
|
||||
this.mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||
|
||||
if (!isOriginalSourceNull && !isSourceEqual) {
|
||||
reloadSource();
|
||||
@ -952,21 +958,23 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
|
||||
public void setMutedModifier(boolean muted) {
|
||||
audioVolume = muted ? 0.f : 1.f;
|
||||
if (player != null) {
|
||||
player.setVolume(muted ? 0 : 1);
|
||||
player.setVolume(audioVolume);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setVolumeModifier(float volume) {
|
||||
audioVolume = volume;
|
||||
if (player != null) {
|
||||
player.setVolume(volume);
|
||||
player.setVolume(audioVolume);
|
||||
}
|
||||
}
|
||||
|
||||
public void seekTo(long positionMs) {
|
||||
if (player != null) {
|
||||
eventEmitter.seek(player.getCurrentPosition(), positionMs);
|
||||
seekTime = positionMs;
|
||||
player.seekTo(positionMs);
|
||||
}
|
||||
}
|
||||
@ -980,6 +988,14 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
}
|
||||
|
||||
public void setMaxBitRateModifier(int newMaxBitRate) {
|
||||
maxBitRate = newMaxBitRate;
|
||||
if (player != null) {
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setPlayInBackground(boolean playInBackground) {
|
||||
this.playInBackground = playInBackground;
|
||||
@ -1026,6 +1042,10 @@ class ReactExoplayerView extends FrameLayout implements
|
||||
exoPlayerView.setUseTextureView(useTextureView);
|
||||
}
|
||||
|
||||
public void setHideShutterView(boolean hideShutterView) {
|
||||
exoPlayerView.setHideShutterView(hideShutterView);
|
||||
}
|
||||
|
||||
public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs) {
|
||||
minBufferMs = newMinBufferMs;
|
||||
maxBufferMs = newMaxBufferMs;
|
||||
|
@ -48,6 +48,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_REPORT_BANDWIDTH = "reportBandwidth";
|
||||
private static final String PROP_SEEK = "seek";
|
||||
private static final String PROP_RATE = "rate";
|
||||
private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate";
|
||||
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
|
||||
private static final String PROP_DISABLE_FOCUS = "disableFocus";
|
||||
private static final String PROP_FULLSCREEN = "fullscreen";
|
||||
@ -55,6 +56,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK_TYPE = "type";
|
||||
private static final String PROP_SELECTED_VIDEO_TRACK_VALUE = "value";
|
||||
private static final String PROP_HIDE_SHUTTER_VIEW = "hideShutterView";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@ -223,6 +225,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setRateModifier(rate);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_MAXIMUM_BIT_RATE)
|
||||
public void setMaxBitRate(final ReactExoplayerView videoView, final int maxBitRate) {
|
||||
videoView.setMaxBitRateModifier(maxBitRate);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
|
||||
public void setPlayInBackground(final ReactExoplayerView videoView, final boolean playInBackground) {
|
||||
videoView.setPlayInBackground(playInBackground);
|
||||
@ -243,6 +250,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
|
||||
videoView.setUseTextureView(useTextureView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
|
||||
public void setHideShutterView(final ReactExoplayerView videoView, final boolean hideShutterView) {
|
||||
videoView.setHideShutterView(hideShutterView);
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_BUFFER_CONFIG)
|
||||
public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) {
|
||||
int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
|
||||
|
@ -47,6 +47,7 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnErrorListener,
|
||||
MediaPlayer.OnBufferingUpdateListener,
|
||||
MediaPlayer.OnSeekCompleteListener,
|
||||
MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnInfoListener,
|
||||
LifecycleEventListener,
|
||||
@ -127,6 +128,7 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
private float mProgressUpdateInterval = 250.0f;
|
||||
private float mRate = 1.0f;
|
||||
private float mActiveRate = 1.0f;
|
||||
private long mSeekTime = 0;
|
||||
private boolean mPlayInBackground = false;
|
||||
private boolean mBackgroundPaused = false;
|
||||
private boolean mIsFullscreen = false;
|
||||
@ -213,6 +215,7 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
mMediaPlayer.setOnErrorListener(this);
|
||||
mMediaPlayer.setOnPreparedListener(this);
|
||||
mMediaPlayer.setOnBufferingUpdateListener(this);
|
||||
mMediaPlayer.setOnSeekCompleteListener(this);
|
||||
mMediaPlayer.setOnCompletionListener(this);
|
||||
mMediaPlayer.setOnInfoListener(this);
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
@ -232,12 +235,19 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
mediaController.hide();
|
||||
}
|
||||
if ( mMediaPlayer != null ) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mMediaPlayer.setOnTimedMetaDataAvailableListener(null);
|
||||
}
|
||||
mMediaPlayerValid = false;
|
||||
release();
|
||||
}
|
||||
if (mIsFullscreen) {
|
||||
setFullscreen(false);
|
||||
}
|
||||
if (mThemedReactContext != null) {
|
||||
mThemedReactContext.removeLifecycleEventListener(this);
|
||||
mThemedReactContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final ReadableMap requestHeaders) {
|
||||
@ -564,8 +574,7 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
});
|
||||
}
|
||||
|
||||
// Select track (so we can use it to listen to timed meta data updates)
|
||||
mp.selectTrack(0);
|
||||
selectTimedMetadataTrack(mp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -600,21 +609,22 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
|
||||
@Override
|
||||
public void onBufferingUpdate(MediaPlayer mp, int percent) {
|
||||
// Select track (so we can use it to listen to timed meta data updates)
|
||||
mp.selectTrack(0);
|
||||
|
||||
selectTimedMetadataTrack(mp);
|
||||
mVideoBufferedDuration = (int) Math.round((double) (mVideoDuration * percent) / 100.0);
|
||||
}
|
||||
|
||||
public void onSeekComplete(MediaPlayer mp) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, getCurrentPosition() / 1000.0);
|
||||
event.putDouble(EVENT_PROP_SEEK_TIME, mSeekTime / 1000.0);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_SEEK.toString(), event);
|
||||
mSeekTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int msec) {
|
||||
|
||||
if (mMediaPlayerValid) {
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putDouble(EVENT_PROP_CURRENT_TIME, getCurrentPosition() / 1000.0);
|
||||
event.putDouble(EVENT_PROP_SEEK_TIME, msec / 1000.0);
|
||||
mEventEmitter.receiveEvent(getId(), Events.EVENT_SEEK.toString(), event);
|
||||
|
||||
mSeekTime = msec;
|
||||
super.seekTo(msec);
|
||||
if (isCompleted && mVideoDuration != 0 && msec < mVideoDuration) {
|
||||
isCompleted = false;
|
||||
@ -755,4 +765,20 @@ public class ReactVideoView extends ScalableVideoView implements
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Select track (so we can use it to listen to timed meta data updates)
|
||||
private void selectTimedMetadataTrack(MediaPlayer mp) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return;
|
||||
}
|
||||
try { // It's possible this could throw an exception if the framework doesn't support getting track info
|
||||
MediaPlayer.TrackInfo[] trackInfo = mp.getTrackInfo();
|
||||
for (int i = 0; i < trackInfo.length; ++i) {
|
||||
if (trackInfo[i].getTrackType() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
|
||||
mp.selectTrack(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,26 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import Video from 'react-native-video';
|
||||
import Video,{FilterType} from 'react-native-video';
|
||||
|
||||
const filterTypes = [
|
||||
FilterType.NONE,
|
||||
FilterType.INVERT,
|
||||
FilterType.MONOCHROME,
|
||||
FilterType.POSTERIZE,
|
||||
FilterType.FALSE,
|
||||
FilterType.MAXIMUMCOMPONENT,
|
||||
FilterType.MINIMUMCOMPONENT,
|
||||
FilterType.CHROME,
|
||||
FilterType.FADE,
|
||||
FilterType.INSTANT,
|
||||
FilterType.MONO,
|
||||
FilterType.NOIR,
|
||||
FilterType.PROCESS,
|
||||
FilterType.TONAL,
|
||||
FilterType.TRANSFER,
|
||||
FilterType.SEPIA
|
||||
];
|
||||
|
||||
class VideoPlayer extends Component {
|
||||
constructor(props) {
|
||||
@ -34,6 +53,8 @@ class VideoPlayer extends Component {
|
||||
skin: 'custom',
|
||||
ignoreSilentSwitch: null,
|
||||
isBuffering: false,
|
||||
filter: FilterType.NONE,
|
||||
filterEnabled: true
|
||||
};
|
||||
|
||||
onLoad(data) {
|
||||
@ -57,6 +78,20 @@ class VideoPlayer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(step) {
|
||||
let index = filterTypes.indexOf(this.state.filter) + step;
|
||||
|
||||
if (index === filterTypes.length) {
|
||||
index = 0;
|
||||
} else if (index === -1) {
|
||||
index = filterTypes.length - 1;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
filter: filterTypes[index]
|
||||
})
|
||||
}
|
||||
|
||||
renderSkinControl(skin) {
|
||||
const isSelected = this.state.skin == skin;
|
||||
const selectControls = skin == 'native' || skin == 'embed';
|
||||
@ -141,6 +176,8 @@ class VideoPlayer extends Component {
|
||||
onProgress={this.onProgress}
|
||||
onEnd={() => { AlertIOS.alert('Done!') }}
|
||||
repeat={true}
|
||||
filter={this.state.filter}
|
||||
filterEnabled={this.state.filterEnabled}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -151,6 +188,21 @@ class VideoPlayer extends Component {
|
||||
{this.renderSkinControl('native')}
|
||||
{this.renderSkinControl('embed')}
|
||||
</View>
|
||||
{
|
||||
(this.state.filterEnabled) ?
|
||||
<View style={styles.skinControl}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
this.setFilter(-1)
|
||||
}}>
|
||||
<Text style={styles.controlOption}>Previous Filter</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => {
|
||||
this.setFilter(1)
|
||||
}}>
|
||||
<Text style={styles.controlOption}>Next Filter</Text>
|
||||
</TouchableOpacity>
|
||||
</View> : null
|
||||
}
|
||||
</View>
|
||||
<View style={styles.generalControls}>
|
||||
<View style={styles.rateControl}>
|
||||
@ -212,6 +264,8 @@ class VideoPlayer extends Component {
|
||||
onEnd={() => { AlertIOS.alert('Done!') }}
|
||||
repeat={true}
|
||||
controls={this.state.controls}
|
||||
filter={this.state.filter}
|
||||
filterEnabled={this.state.filterEnabled}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.controls}>
|
||||
@ -221,6 +275,21 @@ class VideoPlayer extends Component {
|
||||
{this.renderSkinControl('native')}
|
||||
{this.renderSkinControl('embed')}
|
||||
</View>
|
||||
{
|
||||
(this.state.filterEnabled) ?
|
||||
<View style={styles.skinControl}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
this.setFilter(-1)
|
||||
}}>
|
||||
<Text style={styles.controlOption}>Previous Filter</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => {
|
||||
this.setFilter(1)
|
||||
}}>
|
||||
<Text style={styles.controlOption}>Next Filter</Text>
|
||||
</TouchableOpacity>
|
||||
</View> : null
|
||||
}
|
||||
</View>
|
||||
<View style={styles.generalControls}>
|
||||
<View style={styles.rateControl}>
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { StyleSheet, Text, View, Dimensions } from "react-native";
|
||||
import { StyleSheet, Text, View, Dimensions, TouchableOpacity } from "react-native";
|
||||
import Video from "react-native-video";
|
||||
|
||||
const { height, width } = Dimensions.get("screen");
|
||||
@ -28,6 +28,16 @@ export default class App extends Component<Props> {
|
||||
}}
|
||||
style={{ flex: 1, height, width }}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
let response = await this.player.save();
|
||||
let uri = response.uri;
|
||||
console.log("Download URI", uri);
|
||||
}}
|
||||
style={styles.button}
|
||||
>
|
||||
<Text style={{color: 'white'}}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@ -40,6 +50,14 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
backgroundColor: "#F5FCFF"
|
||||
},
|
||||
button: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
right: 16,
|
||||
padding: 10,
|
||||
backgroundColor: '#9B2FAE',
|
||||
borderRadius: 8
|
||||
},
|
||||
welcome: {
|
||||
fontSize: 20,
|
||||
textAlign: "center",
|
||||
|
@ -9,9 +9,9 @@ PODS:
|
||||
- glog (0.3.4)
|
||||
- React (0.56.0):
|
||||
- React/Core (= 0.56.0)
|
||||
- react-native-video/Video (3.1.0):
|
||||
- react-native-video/Video (3.2.2):
|
||||
- React
|
||||
- react-native-video/VideoCaching (3.1.0):
|
||||
- react-native-video/VideoCaching (3.2.2):
|
||||
- DVAssetLoaderDelegate (~> 0.3.1)
|
||||
- React
|
||||
- react-native-video/Video
|
||||
|
@ -24,6 +24,21 @@
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
@ -38,19 +53,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -4,6 +4,7 @@
|
||||
#import "RCTVideoPlayerViewController.h"
|
||||
#import "RCTVideoPlayerViewControllerDelegate.h"
|
||||
#import <React/RCTComponent.h>
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
#if __has_include(<react-native-video/RCTVideoCache.h>)
|
||||
#import <react-native-video/RCTVideoCache.h>
|
||||
@ -42,4 +43,6 @@
|
||||
|
||||
- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem;
|
||||
|
||||
- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
|
||||
|
||||
@end
|
||||
|
@ -26,6 +26,7 @@ static int const RCTVideoUnset = -1;
|
||||
{
|
||||
AVPlayer *_player;
|
||||
AVPlayerItem *_playerItem;
|
||||
NSDictionary *_source;
|
||||
BOOL _playerItemObserversSet;
|
||||
BOOL _playerBufferEmpty;
|
||||
AVPlayerLayer *_playerLayer;
|
||||
@ -51,6 +52,8 @@ static int const RCTVideoUnset = -1;
|
||||
/* Keep track of any modifiers, need to be applied after each play */
|
||||
float _volume;
|
||||
float _rate;
|
||||
float _maxBitRate;
|
||||
|
||||
BOOL _muted;
|
||||
BOOL _paused;
|
||||
BOOL _repeat;
|
||||
@ -64,8 +67,11 @@ static int const RCTVideoUnset = -1;
|
||||
NSString * _ignoreSilentSwitch;
|
||||
NSString * _resizeMode;
|
||||
BOOL _fullscreen;
|
||||
BOOL _fullscreenAutorotate;
|
||||
NSString * _fullscreenOrientation;
|
||||
BOOL _fullscreenPlayerPresented;
|
||||
NSString *_filterName;
|
||||
BOOL _filterEnabled;
|
||||
UIViewController * _presentingViewController;
|
||||
#if __has_include(<react-native-video/RCTVideoCache.h>)
|
||||
RCTVideoCache * _videoCache;
|
||||
@ -83,6 +89,7 @@ static int const RCTVideoUnset = -1;
|
||||
_rate = 1.0;
|
||||
_volume = 1.0;
|
||||
_resizeMode = @"AVLayerVideoGravityResizeAspectFill";
|
||||
_fullscreenAutorotate = YES;
|
||||
_fullscreenOrientation = @"all";
|
||||
_pendingSeek = false;
|
||||
_pendingSeekTime = 0.0f;
|
||||
@ -323,6 +330,7 @@ static int const RCTVideoUnset = -1;
|
||||
|
||||
- (void)setSrc:(NSDictionary *)source
|
||||
{
|
||||
_source = source;
|
||||
[self removePlayerLayer];
|
||||
[self removePlayerTimeObserver];
|
||||
[self removePlayerItemObservers];
|
||||
@ -333,6 +341,8 @@ static int const RCTVideoUnset = -1;
|
||||
[self playerItemForSource:source withCallback:^(AVPlayerItem * playerItem) {
|
||||
_playerItem = playerItem;
|
||||
[self addPlayerItemObservers];
|
||||
[self setFilter:_filterName];
|
||||
[self setMaxBitRate:_maxBitRate];
|
||||
|
||||
[_player pause];
|
||||
[_playerViewController.view removeFromSuperview];
|
||||
@ -397,10 +407,13 @@ static int const RCTVideoUnset = -1;
|
||||
|
||||
- (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nullable)assetOptions withCallback:(void(^)(AVPlayerItem *))handler
|
||||
{
|
||||
if (!_textTracks) {
|
||||
if (!_textTracks || _textTracks.count==0) {
|
||||
handler([AVPlayerItem playerItemWithAsset:asset]);
|
||||
return;
|
||||
}
|
||||
|
||||
// AVPlayer can't airplay AVMutableCompositions
|
||||
_allowsExternalPlayback = NO;
|
||||
|
||||
// sideload text tracks
|
||||
AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
|
||||
@ -864,6 +877,12 @@ static int const RCTVideoUnset = -1;
|
||||
[self applyModifiers];
|
||||
}
|
||||
|
||||
- (void)setMaxBitRate:(float) maxBitRate {
|
||||
_maxBitRate = maxBitRate;
|
||||
_playerItem.preferredPeakBitRate = maxBitRate;
|
||||
}
|
||||
|
||||
|
||||
- (void)applyModifiers
|
||||
{
|
||||
if (_muted) {
|
||||
@ -874,6 +893,7 @@ static int const RCTVideoUnset = -1;
|
||||
[_player setMuted:NO];
|
||||
}
|
||||
|
||||
[self setMaxBitRate:_maxBitRate];
|
||||
[self setSelectedAudioTrack:_selectedAudioTrack];
|
||||
[self setSelectedTextTrack:_selectedTextTrack];
|
||||
[self setResizeMode:_resizeMode];
|
||||
@ -1154,6 +1174,7 @@ static int const RCTVideoUnset = -1;
|
||||
[viewController presentViewController:_playerViewController animated:true completion:^{
|
||||
_playerViewController.showsPlaybackControls = YES;
|
||||
_fullscreenPlayerPresented = fullscreen;
|
||||
_playerViewController.autorotate = _fullscreenAutorotate;
|
||||
if(self.onVideoFullscreenPlayerDidPresent) {
|
||||
self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag});
|
||||
}
|
||||
@ -1169,6 +1190,13 @@ static int const RCTVideoUnset = -1;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFullscreenAutorotate:(BOOL)autorotate {
|
||||
_fullscreenAutorotate = autorotate;
|
||||
if (_fullscreenPlayerPresented) {
|
||||
_playerViewController.autorotate = autorotate;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFullscreenOrientation:(NSString *)orientation {
|
||||
_fullscreenOrientation = orientation;
|
||||
if (_fullscreenPlayerPresented) {
|
||||
@ -1270,6 +1298,36 @@ static int const RCTVideoUnset = -1;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFilter:(NSString *)filterName {
|
||||
_filterName = filterName;
|
||||
|
||||
if (!_filterEnabled) {
|
||||
return;
|
||||
} else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) {
|
||||
return; // filters don't work for HLS... return
|
||||
} else if (!_playerItem.asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
CIFilter *filter = [CIFilter filterWithName:filterName];
|
||||
_playerItem.videoComposition = [AVVideoComposition
|
||||
videoCompositionWithAsset:_playerItem.asset
|
||||
applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) {
|
||||
if (filter == nil) {
|
||||
[request finishWithImage:request.sourceImage context:nil];
|
||||
} else {
|
||||
CIImage *image = request.sourceImage.imageByClampingToExtent;
|
||||
[filter setValue:image forKey:kCIInputImageKey];
|
||||
CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent];
|
||||
[request finishWithImage:output context:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setFilterEnabled:(BOOL)filterEnabled {
|
||||
_filterEnabled = filterEnabled;
|
||||
}
|
||||
|
||||
#pragma mark - React View Management
|
||||
|
||||
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
||||
@ -1356,4 +1414,78 @@ static int const RCTVideoUnset = -1;
|
||||
[super removeFromSuperview];
|
||||
}
|
||||
|
||||
#pragma mark - Export
|
||||
|
||||
- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
|
||||
AVAsset *asset = _playerItem.asset;
|
||||
|
||||
if (asset != nil) {
|
||||
|
||||
AVAssetExportSession *exportSession = [AVAssetExportSession
|
||||
exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality];
|
||||
|
||||
if (exportSession != nil) {
|
||||
NSString *path = nil;
|
||||
NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"]
|
||||
withExtension:@".mp4"];
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
exportSession.outputFileType = AVFileTypeMPEG4;
|
||||
exportSession.outputURL = url;
|
||||
exportSession.videoComposition = _playerItem.videoComposition;
|
||||
exportSession.shouldOptimizeForNetworkUse = true;
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
|
||||
switch ([exportSession status]) {
|
||||
case AVAssetExportSessionStatusFailed:
|
||||
reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error);
|
||||
break;
|
||||
case AVAssetExportSessionStatusCancelled:
|
||||
reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error);
|
||||
break;
|
||||
default:
|
||||
resolve(@{@"uri": url.absoluteString});
|
||||
break;
|
||||
}
|
||||
|
||||
}];
|
||||
|
||||
} else {
|
||||
|
||||
reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil);
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)ensureDirExistsWithPath:(NSString *)path {
|
||||
BOOL isDir = NO;
|
||||
NSError *error;
|
||||
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
|
||||
if (!(exists && isDir)) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
if (error) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension {
|
||||
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
|
||||
[self ensureDirExistsWithPath:directory];
|
||||
return [directory stringByAppendingPathComponent:fileName];
|
||||
}
|
||||
|
||||
- (NSString *)cacheDirectoryPath {
|
||||
NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
return array[0];
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -1,5 +1,6 @@
|
||||
#import <React/RCTViewManager.h>
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCTVideoManager : RCTViewManager
|
||||
@interface RCTVideoManager : RCTViewManager <RCTBridgeModule>
|
||||
|
||||
@end
|
||||
|
@ -1,14 +1,13 @@
|
||||
#import "RCTVideoManager.h"
|
||||
#import "RCTVideo.h"
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTUIManager.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@implementation RCTVideoManager
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
return [[RCTVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
|
||||
@ -16,10 +15,11 @@ RCT_EXPORT_MODULE();
|
||||
|
||||
- (dispatch_queue_t)methodQueue
|
||||
{
|
||||
return dispatch_get_main_queue();
|
||||
return self.bridge.uiManager.methodQueue;
|
||||
}
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(maxBitRate, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL);
|
||||
@ -37,7 +37,10 @@ RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(currentTime, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(fullscreenAutorotate, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
||||
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
|
||||
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
|
||||
@ -59,6 +62,21 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTBubblingEventBlock);
|
||||
RCT_REMAP_METHOD(save,
|
||||
options:(NSDictionary *)options
|
||||
reactTag:(nonnull NSNumber *)reactTag
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
[self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTVideo *> *viewRegistry) {
|
||||
RCTVideo *view = viewRegistry[reactTag];
|
||||
if (![view isKindOfClass:[RCTVideo class]]) {
|
||||
RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view);
|
||||
} else {
|
||||
[view save:options resolve:resolve reject:reject];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary *)constantsToExport
|
||||
{
|
||||
|
@ -15,5 +15,6 @@
|
||||
|
||||
// Optional paramters
|
||||
@property (nonatomic, weak) NSString* preferredOrientation;
|
||||
@property (nonatomic) BOOL autorotate;
|
||||
|
||||
@end
|
||||
|
@ -7,7 +7,8 @@
|
||||
@implementation RCTVideoPlayerViewController
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
if (self.preferredOrientation.lowercaseString == nil || [self.preferredOrientation.lowercaseString isEqualToString:@"all"])
|
||||
|
||||
if (self.autorotate || self.preferredOrientation.lowercaseString == nil || [self.preferredOrientation.lowercaseString isEqualToString:@"all"])
|
||||
return YES;
|
||||
|
||||
return NO;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-video",
|
||||
"version": "3.2.1",
|
||||
"version": "4.2.1",
|
||||
"description": "A <Video /> element for react-native",
|
||||
"main": "Video.js",
|
||||
"license": "MIT",
|
||||
@ -49,10 +49,12 @@
|
||||
"files":[
|
||||
"android-exoplayer",
|
||||
"android",
|
||||
"dom",
|
||||
"ios",
|
||||
"windows",
|
||||
"FilterType.js",
|
||||
"TextTrackType.js",
|
||||
"react-native-video.podspec",
|
||||
"VideoResizeMode.js"
|
||||
"VideoResizeMode.js",
|
||||
"react-native-video.podspec"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user