Merge branch 'master' into master

This commit is contained in:
Hampton Maxwell 2018-12-31 21:36:49 -08:00 committed by GitHub
commit 45a851e79a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 575 additions and 60 deletions

View File

@ -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
View 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
View File

@ -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:

View File

@ -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,

View File

@ -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'
}

View File

@ -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() {

View File

@ -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;

View File

@ -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;

View File

@ -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) {}
}
}

View File

@ -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}>

View File

@ -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",

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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,11 +407,14 @@ 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

View File

@ -1,5 +1,6 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
@interface RCTVideoManager : RCTViewManager
@interface RCTVideoManager : RCTViewManager <RCTBridgeModule>
@end

View File

@ -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
{

View File

@ -15,5 +15,6 @@
// Optional paramters
@property (nonatomic, weak) NSString* preferredOrientation;
@property (nonatomic) BOOL autorotate;
@end

View File

@ -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;

View File

@ -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"
]
}