Merge branch 'master' into master

This commit is contained in:
Emrah 2018-06-09 21:36:09 +02:00 committed by GitHub
commit 2d89a3fd54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 461 additions and 70 deletions

19
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
### Current behavior
Describe what happens when you encounter this issue.
### Reproduction steps
A 1, 2, 3, etc. list of what's needed to see the issue happen.
### Expected behavior
Describe what you wanted to happen
### Platform
Which player are you experiencing the problem on:
* iOS
* Android ExoPlayer
* Android MediaPlayer
* Windows UWP
* Windows WPF
### Video sample
If possible, include a link to the video that has the problem that can be streamed or downloaded from.

13
CHANGELOG.md Normal file
View File

@ -0,0 +1,13 @@
## Changelog
### Version 2.2.0
* Text track selection support for iOS & ExoPlayer [#1049](https://github.com/react-native-community/react-native-video/pull/1049)
* Support outputting to a TextureView on Android ExoPlayer [#1058](https://github.com/react-native-community/react-native-video/pull/1058)
* Support changing the left/right balance on Android MediaPlayer [#1051](https://github.com/react-native-community/react-native-video/pull/1051)
* Prevent multiple onEnd notifications on iOS [#832](https://github.com/react-native-community/react-native-video/pull/832)
* Fix doing a partial swipe on iOS causing a black screen [#1048](https://github.com/react-native-community/react-native-video/pull/1048)
* Fix crash when switching to a new source on iOS [#974](https://github.com/react-native-community/react-native-video/pull/974)
* Add cookie support for ExoPlayer [#922](https://github.com/react-native-community/react-native-video/pull/922)
* Remove ExoPlayer onMetadata that wasn't being used [#1040](https://github.com/react-native-community/react-native-video/pull/1040)
* Fix bug where setting the progress interval on iOS didn't work [#800](https://github.com/react-native-community/react-native-video/pull/800)
* Support setting the poster resize mode [#595](https://github.com/react-native-community/react-native-video/pull/595)

185
README.md
View File

@ -86,7 +86,7 @@ include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
```
If you need to use the old Android media player based player, use the following instead:
If you need to use the old Android MediaPlayer based player, use the following instead:
```gradle
include ':react-native-video'
@ -181,27 +181,16 @@ using System.Collections.Generic;
// on a single screen if you like.
<Video source={{uri: "background"}} // Can be a URL or a local file.
poster="https://baconmockup.com/300/200/" // uri to an image to display until the video plays
ref={(ref) => {
this.player = ref
}} // Store reference
rate={1.0} // 0 is paused, 1 is normal.
volume={1.0} // 0 is muted, 1 is normal.
muted={true|false} // Mutes the audio entirely. Default false
paused={true|false} // Pauses playback entirely. Default false
resizeMode="cover" // Fill the whole screen at aspect ratio.*
repeat={true|false} // Repeat forever. Default false
playInBackground={true|false} // Audio continues to play when app entering background. Default false
playWhenInactive={true|false} // [iOS] Video continues to play when control or notification center are shown. Default false
ignoreSilentSwitch={"ignore"} // [iOS] ignore | obey - When 'ignore', audio will still play with the iOS hard silent switch set to silent. When 'obey', audio will toggle with the switch. When not specified, will inherit audio settings as usual.
progressUpdateInterval={250.0} // [iOS] Interval to fire onProgress (default to ~250ms)
onBuffer={this.onBuffer} // Callback when remote video is buffering
onEnd={this.onEnd} // Callback when playback finishes
onError={this.videoError} // Callback when video cannot be loaded
onFullscreenPlayerWillPresent={this.fullScreenPlayerWillPresent} // Callback before fullscreen starts
onFullscreenPlayerDidPresent={this.fullScreenPlayerDidPresent} // Callback after fullscreen started
onFullscreenPlayerWillDismiss={this.fullScreenPlayerWillDismiss} // Callback before fullscreen stops
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDissmiss} // Callback after fullscreen stopped
onFullscreenPlayerDidDismiss={this.fullScreenPlayerDidDismiss} // Callback after fullscreen stopped
onLoadStart={this.loadStart} // Callback when video starts to load
onLoad={this.setDuration} // Callback when video loads
onProgress={this.setTime} // Callback every ~250ms with currentTime
@ -228,6 +217,176 @@ var styles = StyleSheet.create({
},
});
```
### Configurable props
* [ignoreSilentSwitch](#ignoresilentswitch)
* [muted](#muted)
* [paused](#paused)
* [playInBackground](#playinbackground)
* [playWhenInactive](#playwheninactive)
* [poster](#poster)
* [posterResizeMode](#posterresizemode)
* [progressUpdateInterval](#progressupdateinterval)
* [rate](#rate)
* [repeat](#repeat)
* [resizeMode](#resizemode)
* [selectedTextTrack](#selectedtexttrack)
* [stereoPan](#stereopan)
* [useTextureView](#usetextureview)
* [volume](#volume)
#### ignoreSilentSwitch
Controls the iOS silent switch behavior
* **"inherit" (default)** - Use the default AVPlayer behavior
* **"ignore"** - Play audio even if the silent switch is set
* **"obey"** - Don't play audio if the silent switch is set
Platforms: iOS
#### muted
Controls whether the audio is muted
* **false (default)** - Don't mute audio
* **true** - Mute audio
Platforms: all
#### paused
Controls whether the media is paused
* **false (default)** - Pause the media
* **true** - Don't pause the media
Platforms: all
#### playInBackground
Determine whether the media should continue playing while the app is in the background. This allows customers to continue listening to the audio.
* **false (default)** - Don't continue playing the media
* **true** - Continue playing the media
To use this feature on iOS, you must:
* [Enable Background Audio](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionBasics/AudioSessionBasics.html#//apple_ref/doc/uid/TP40007875-CH3-SW3) in your Xcode project
* Set the ignoreSilentSwitch prop to "ignore"
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
#### playWhenInactive
Determine whether the media should continue playing when notifications or the Control Center are in front of the video.
* **false (default)** - Don't continue playing the media
* **true** - Continue playing the media
Platforms: iOS
#### poster
An image to display while the video is loading
<br>Value: string with a URL for the poster, e.g. "https://baconmockup.com/300/200/"
Platforms: all
#### posterResizeMode
Determines how to resize the poster image when the frame doesn't match the raw video dimensions.
* **"contain" (default)** - Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or less than the corresponding dimension of the view (minus padding).
* **"center"** - Center the image in the view along both dimensions. If the image is larger than the view, scale it down uniformly so that it is contained in the view.
* **"cover"** - Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding).
* **"none"** - Don't apply resize
* **"repeat"** - Repeat the image to cover the frame of the view. The image will keep its size and aspect ratio. (iOS only)
* **"stretch"** - Scale width and height independently, This may change the aspect ratio of the src.
Platforms: all
#### progressUpdateInterval
Delay in milliseconds between onProgress events in milliseconds.
Default: 250.0
Platforms: all
### rate
Speed at which the media should play.
* **0.0** - Pauses the video
* **1.0** - Play at normal speed
* **Other values** - Slow down or speed up playback
Platforms: all
Note: For Android MediaPlayer, rate is only supported on Android 6.0 and higher devices.
#### repeat
Determine whether to repeat the video when the end is reached
* **false (default)** - Don't repeat the video
* **true** - Repeat the video
Platforms: all
#### resizeMode
Determines how to resize the video when the frame doesn't match the raw video dimensions.
* **"none" (default)** - Don't apply resize
* **"contain"** - Scale the video uniformly (maintain the video's aspect ratio) so that both dimensions (width and height) of the video will be equal to or less than the corresponding dimension of the view (minus padding).
* **"cover"** - Scale the video uniformly (maintain the video's aspect ratio) so that both dimensions (width and height) of the image will be equal to or larger than the corresponding dimension of the view (minus padding).
* **"stretch"** - Scale width and height independently, This may change the aspect ratio of the src.
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP
#### selectedTextTrack
Configure which text track (caption or subtitle), if any, is shown.
```
selectedTextTrack={{
type: Type,
value: Value
}}
```
Example:
```
selectedTextTrack={{
type: "title",
value: "English Subtitles"
}}
```
Type | Value | Description
--- | --- | ---
"system" (default) | N/A | Display captions only if the system preference for captions is enabled
"disabled" | N/A | Don't display a text track
"title" | string | Display the text track with the title specified as the Value, e.g. "French 1"
"language" | string | Display the text track with the language specified as the Value, e.g. "fr"
"index" | number | Display the text track with the index specified as the value, e.g. 0
Both iOS & Android offer Settings to enable Captions for hearing impaired people. If "system" is selected and the Captions Setting is enabled, iOS/Android will look for a caption that matches that customer's language and display it.
If a track matching the specified Type (and Value if appropriate) is unavailable, no text track will be displayed. If multiple tracks match the criteria, the first match will be used.
Platforms: Android ExoPlayer, iOS
#### stereoPan
Adjust the balance of the left and right audio channels. Any value between 1.0 and 1.0 is accepted.
* **-1.0** - Full left
* **0.0 (default)** - Center
* **1.0** - Full right
Platforms: Android MediaPlayer
#### useTextureView
Output to a TextureView instead of the default SurfaceView. In general, you will want to use SurfaceView because it is more efficient and provides better performance. However, SurfaceViews has two limitations:
* It can't be animated, transformed or scaled
* You can't overlay multiple SurfaceViews
useTextureView can only be set at same time you're setting the source.
* **false (default)** - Use a SurfaceView
* **true** - Use a TextureView
Platforms: Android ExoPlayer
#### volume
Adjust the volume.
* **1.0 (default)** - Play at full volume
* **0.0** - Mute the audio
* **Other values** - Reduce volume
Platforms: all
### Additional props
To see the full list of available props, you can check the [propTypes](https://github.com/react-native-community/react-native-video/blob/master/Video.js#L246) of the Video.js component.
- By default, iOS 9+ will only load encrypted HTTPS urls. If you need to load content from a webserver that only supports HTTP, you will need to modify your Info.plist file and add the following entry:

View File

@ -252,7 +252,7 @@ export default class Video extends Component {
top: 0,
right: 0,
bottom: 0,
resizeMode: 'contain',
resizeMode: this.props.posterResizeMode || 'contain'
};
return (
@ -306,10 +306,19 @@ Video.propTypes = {
]),
resizeMode: PropTypes.string,
poster: PropTypes.string,
posterResizeMode: Image.propTypes.resizeMode,
repeat: PropTypes.bool,
selectedTextTrack: PropTypes.shape({
type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
}),
paused: PropTypes.bool,
muted: PropTypes.bool,
volume: PropTypes.number,
stereoPan: PropTypes.number,
rate: PropTypes.number,
playInBackground: PropTypes.bool,
playWhenInactive: PropTypes.bool,
@ -318,6 +327,7 @@ Video.propTypes = {
controls: PropTypes.bool,
currentTime: PropTypes.number,
progressUpdateInterval: PropTypes.number,
useTextureView: PropTypes.bool,
onLoadStart: PropTypes.func,
onLoad: PropTypes.func,
onBuffer: PropTypes.func,

View File

@ -19,6 +19,7 @@ import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import java.util.Map;
public class DataSourceUtil {
private DataSourceUtil() {
@ -32,14 +33,14 @@ public class DataSourceUtil {
DataSourceUtil.userAgent = userAgent;
}
public static String getUserAgent(Context context) {
public static String getUserAgent(ReactContext context) {
if (userAgent == null) {
userAgent = Util.getUserAgent(context.getApplicationContext(), "ReactNativeVideo");
userAgent = Util.getUserAgent(context, "ReactNativeVideo");
}
return userAgent;
}
public static DataSource.Factory getRawDataSourceFactory(Context context) {
public static DataSource.Factory getRawDataSourceFactory(ReactContext context) {
if (rawDataSourceFactory == null) {
rawDataSourceFactory = buildRawDataSourceFactory(context);
}
@ -50,6 +51,7 @@ public class DataSourceUtil {
DataSourceUtil.rawDataSourceFactory = factory;
}
public static DataSource.Factory getDefaultDataSourceFactory(ReactContext context, DefaultBandwidthMeter bandwidthMeter, Map<String, String> requestHeaders) {
if (defaultDataSourceFactory == null || (requestHeaders != null && !requestHeaders.isEmpty())) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter, requestHeaders);
@ -61,7 +63,7 @@ public class DataSourceUtil {
DataSourceUtil.defaultDataSourceFactory = factory;
}
private static DataSource.Factory buildRawDataSourceFactory(Context context) {
private static DataSource.Factory buildRawDataSourceFactory(ReactContext context) {
return new RawResourceDataSourceFactory(context.getApplicationContext());
}
@ -75,7 +77,6 @@ public class DataSourceUtil {
CookieJarContainer container = (CookieJarContainer) client.cookieJar();
ForwardingCookieHandler handler = new ForwardingCookieHandler(context);
container.setCookieJar(new JavaNetCookieJar(handler));
OkHttpDataSourceFactory okHttpDataSourceFactory = new OkHttpDataSourceFactory(client, getUserAgent(context), bandwidthMeter);
if (requestHeaders != null)

View File

@ -18,8 +18,6 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
@ -27,17 +25,20 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.SubtitleView;
import java.util.List;
import java.lang.Object;
@TargetApi(16)
public final class ExoPlayerView extends FrameLayout {
private final View surfaceView;
private View surfaceView;
private final View shutterView;
private final SubtitleView subtitleLayout;
private final AspectRatioFrameLayout layout;
private final ComponentListener componentListener;
private SimpleExoPlayer player;
private Context context;
private ViewGroup.LayoutParams layoutParams;
private boolean useTextureView = false;
public ExoPlayerView(Context context) {
this(context, null);
@ -50,9 +51,9 @@ public final class ExoPlayerView extends FrameLayout {
public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
boolean useTextureView = false;
this.context = context;
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
@ -66,25 +67,45 @@ public final class ExoPlayerView extends FrameLayout {
layout.setLayoutParams(aspectRatioParams);
shutterView = new View(getContext());
shutterView.setLayoutParams(params);
shutterView.setLayoutParams(layoutParams);
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
subtitleLayout = new SubtitleView(context);
subtitleLayout.setLayoutParams(params);
subtitleLayout.setLayoutParams(layoutParams);
subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize();
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
view.setLayoutParams(params);
surfaceView = view;
updateSurfaceView();
layout.addView(surfaceView, 0, params);
layout.addView(shutterView, 1, params);
layout.addView(subtitleLayout, 2, params);
layout.addView(shutterView, 1, layoutParams);
layout.addView(subtitleLayout, 2, layoutParams);
addViewInLayout(layout, 0, aspectRatioParams);
}
private void setVideoView() {
if (surfaceView instanceof TextureView) {
player.setVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SurfaceView) {
player.setVideoSurfaceView((SurfaceView) surfaceView);
}
}
private void updateSurfaceView() {
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
view.setLayoutParams(layoutParams);
surfaceView = view;
if (layout.getChildAt(0) != null) {
layout.removeViewAt(0);
}
layout.addView(surfaceView, 0, layoutParams);
if (this.player != null) {
setVideoView();
}
}
/**
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
@ -101,20 +122,14 @@ public final class ExoPlayerView extends FrameLayout {
this.player.setVideoListener(null);
this.player.removeListener(componentListener);
this.player.setVideoSurface(null);
this.player.setMetadataOutput(componentListener);
}
this.player = player;
shutterView.setVisibility(VISIBLE);
if (player != null) {
if (surfaceView instanceof TextureView) {
player.setVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SurfaceView) {
player.setVideoSurfaceView((SurfaceView) surfaceView);
}
setVideoView();
player.setVideoListener(componentListener);
player.addListener(componentListener);
player.setTextOutput(componentListener);
player.setMetadataOutput(componentListener);
}
}
@ -141,6 +156,11 @@ public final class ExoPlayerView extends FrameLayout {
return surfaceView;
}
public void setUseTextureView(boolean useTextureView) {
this.useTextureView = useTextureView;
updateSurfaceView();
}
private final Runnable measureAndLayout = new Runnable() {
@Override
public void run() {
@ -168,7 +188,7 @@ public final class ExoPlayerView extends FrameLayout {
}
private final class ComponentListener implements SimpleExoPlayer.VideoListener,
TextRenderer.Output, ExoPlayer.EventListener, MetadataRenderer.Output {
TextRenderer.Output, ExoPlayer.EventListener {
// TextRenderer.Output implementation
@ -232,11 +252,6 @@ public final class ExoPlayerView extends FrameLayout {
// Do nothing
}
@Override
public void onMetadata(Metadata metadata) {
Log.d("onMetadata", "onMetadata");
}
@Override
public void onSeekProcessed() {
// Do nothing.

View File

@ -16,6 +16,7 @@ import android.widget.FrameLayout;
import com.brentvatne.react.R;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.ThemedReactContext;
@ -46,6 +47,7 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
@ -101,6 +103,8 @@ class ReactExoplayerView extends FrameLayout implements
private Uri srcUri;
private String extension;
private boolean repeat;
private String textTrackType;
private Dynamic textTrackValue;
private boolean disableFocus;
private float mProgressUpdateInterval = 250.0f;
private boolean playInBackground = false;
@ -135,9 +139,9 @@ class ReactExoplayerView extends FrameLayout implements
public ReactExoplayerView(ThemedReactContext context) {
super(context);
this.themedReactContext = context;
createViews();
this.eventEmitter = new VideoEventEmitter(context);
this.themedReactContext = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
themedReactContext.addLifecycleEventListener(this);
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
@ -448,6 +452,7 @@ class ReactExoplayerView extends FrameLayout implements
private void videoLoaded() {
if (loadVideoStarted) {
loadVideoStarted = false;
setSelectedTextTrack(textTrackType, textTrackValue);
Format videoFormat = player.getVideoFormat();
int width = videoFormat != null ? videoFormat.width : 0;
int height = videoFormat != null ? videoFormat.height : 0;
@ -570,6 +575,16 @@ class ReactExoplayerView extends FrameLayout implements
return false;
}
public int getTextTrackRendererIndex() {
int rendererCount = player.getRendererCount();
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
if (player.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) {
return rendererIndex;
}
}
return C.INDEX_UNSET;
}
@Override
public void onMetadata(Metadata metadata) {
eventEmitter.timedMetadata(metadata);
@ -585,7 +600,7 @@ class ReactExoplayerView extends FrameLayout implements
this.srcUri = uri;
this.extension = extension;
this.requestHeaders = headers;
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER, requestHeaders);
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER, this.requestHeaders);
if (!isOriginalSourceNull && !isSourceEqual) {
reloadSource();
@ -604,7 +619,7 @@ class ReactExoplayerView extends FrameLayout implements
this.srcUri = uri;
this.extension = extension;
this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(getContext());
this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(this.themedReactContext);
if (!isOriginalSourceNull && !isSourceEqual) {
reloadSource();
@ -632,6 +647,60 @@ class ReactExoplayerView extends FrameLayout implements
this.repeat = repeat;
}
public void setSelectedTextTrack(String type, Dynamic value) {
textTrackType = type;
textTrackValue = value;
int index = getTextTrackRendererIndex();
if (index == C.INDEX_UNSET) {
return;
}
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
if (info == null) {
return;
}
TrackGroupArray groups = info.getTrackGroups(index);
int trackIndex = C.INDEX_UNSET;
if (TextUtils.isEmpty(type)) {
// Do nothing
} else if (type.equals("disabled")) {
trackSelector.setSelectionOverride(index, groups, null);
return;
} else if (type.equals("language")) {
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
if (format.language != null && format.language.equals(value.asString())) {
trackIndex = i;
break;
}
}
} else if (type.equals("title")) {
for (int i = 0; i < groups.length; ++i) {
Format format = groups.get(i).getFormat(0);
if (format.id != null && format.id.equals(value.asString())) {
trackIndex = i;
break;
}
}
} else if (type.equals("index")) {
trackIndex = value.asInt();
} else { // default. invalid type or "system"
trackSelector.clearSelectionOverrides(index);
return;
}
if (trackIndex == C.INDEX_UNSET) {
trackSelector.clearSelectionOverrides(trackIndex);
return;
}
MappingTrackSelector.SelectionOverride override
= new MappingTrackSelector.SelectionOverride(
new FixedTrackSelection.Factory(), trackIndex, 0);
trackSelector.setSelectionOverride(index, groups, override);
}
public void setPausedModifier(boolean paused) {
isPaused = paused;
if (player != null) {
@ -713,4 +782,8 @@ class ReactExoplayerView extends FrameLayout implements
eventEmitter.fullscreenDidDismiss();
}
}
public void setUseTextureView(boolean useTextureView) {
exoPlayerView.setUseTextureView(useTextureView);
}
}

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
@ -26,6 +27,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_SRC_HEADERS = "requestHeaders";
private static final String PROP_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat";
private static final String PROP_SELECTED_TEXT_TRACK = "selectedTextTrack";
private static final String PROP_SELECTED_TEXT_TRACK_TYPE = "type";
private static final String PROP_SELECTED_TEXT_TRACK_VALUE = "value";
private static final String PROP_PAUSED = "paused";
private static final String PROP_MUTED = "muted";
private static final String PROP_VOLUME = "volume";
@ -35,6 +39,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
private static final String PROP_DISABLE_FOCUS = "disableFocus";
private static final String PROP_FULLSCREEN = "fullscreen";
private static final String PROP_USE_TEXTURE_VIEW = "useTextureView";
@Override
public String getName() {
@ -120,6 +125,16 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
videoView.setRepeatModifier(repeat);
}
@ReactProp(name = PROP_SELECTED_TEXT_TRACK)
public void setSelectedTextTrack(final ReactExoplayerView videoView,
@Nullable ReadableMap selectedTextTrack) {
String typeString = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_TYPE)
? selectedTextTrack.getString(PROP_SELECTED_TEXT_TRACK_TYPE) : null;
Dynamic value = selectedTextTrack.hasKey(PROP_SELECTED_TEXT_TRACK_VALUE)
? selectedTextTrack.getDynamic(PROP_SELECTED_TEXT_TRACK_VALUE) : null;
videoView.setSelectedTextTrack(typeString, value);
}
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
public void setPaused(final ReactExoplayerView videoView, final boolean paused) {
videoView.setPausedModifier(paused);
@ -165,6 +180,11 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
videoView.setFullscreen(fullscreen);
}
@ReactProp(name = PROP_USE_TEXTURE_VIEW, defaultBoolean = false)
public void setUseTextureView(final ReactExoplayerView videoView, final boolean useTextureView) {
videoView.setUseTextureView(useTextureView);
}
private boolean startsWithValidScheme(String uriString) {
return uriString.startsWith("http://")
|| uriString.startsWith("https://")

View File

@ -29,6 +29,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.lang.Math;
import java.math.BigDecimal;
import javax.annotation.Nullable;
@ -99,6 +100,7 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
private boolean mPaused = false;
private boolean mMuted = false;
private float mVolume = 1.0f;
private float mStereoPan = 0.0f;
private float mProgressUpdateInterval = 250.0f;
private float mRate = 1.0f;
private float mActiveRate = 1.0f;
@ -373,6 +375,14 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
}
}
// reduces the volume based on stereoPan
private float calulateRelativeVolume() {
float relativeVolume = (mVolume * (1 - Math.abs(mStereoPan)));
// only one decimal allowed
BigDecimal roundRelativeVolume = new BigDecimal(relativeVolume).setScale(1, BigDecimal.ROUND_HALF_UP);
return roundRelativeVolume.floatValue();
}
public void setMutedModifier(final boolean muted) {
mMuted = muted;
@ -382,7 +392,14 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
if (mMuted) {
setVolume(0, 0);
} else if (mStereoPan < 0) {
// louder on the left channel
setVolume(mVolume, calulateRelativeVolume());
} else if (mStereoPan > 0) {
// louder on the right channel
setVolume(calulateRelativeVolume(), mVolume);
} else {
// same volume on both channels
setVolume(mVolume, mVolume);
}
}
@ -392,6 +409,11 @@ public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnP
setMutedModifier(mMuted);
}
public void setStereoPan(final float stereoPan) {
mStereoPan = stereoPan;
setMutedModifier(mMuted);
}
public void setProgressUpdateInterval(final float progressUpdateInterval) {
mProgressUpdateInterval = progressUpdateInterval;
}

View File

@ -31,6 +31,7 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
public static final String PROP_PAUSED = "paused";
public static final String PROP_MUTED = "muted";
public static final String PROP_VOLUME = "volume";
public static final String PROP_STEREO_PAN = "stereoPan";
public static final String PROP_PROGRESS_UPDATE_INTERVAL = "progressUpdateInterval";
public static final String PROP_SEEK = "seek";
public static final String PROP_RATE = "rate";
@ -127,6 +128,11 @@ public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {
videoView.setVolumeModifier(volume);
}
@ReactProp(name = PROP_STEREO_PAN)
public void setStereoPan(final ReactVideoView videoView, final float stereoPan) {
videoView.setStereoPan(stereoPan);
}
@ReactProp(name = PROP_PROGRESS_UPDATE_INTERVAL, defaultFloat = 250.0f)
public void setProgressUpdateInterval(final ReactVideoView videoView, final float progressUpdateInterval) {
videoView.setProgressUpdateInterval(progressUpdateInterval);

View File

@ -40,6 +40,7 @@ static NSString *const timedMetadata = @"timedMetadata";
BOOL _muted;
BOOL _paused;
BOOL _repeat;
NSDictionary * _selectedTextTrack;
BOOL _playbackStalled;
BOOL _playInBackground;
BOOL _playWhenInactive;
@ -127,7 +128,8 @@ static NSString *const timedMetadata = @"timedMetadata";
-(void)addPlayerTimeObserver
{
const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000;
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m
// of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html
__weak RCTVideo *weakSelf = self;
_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(progressUpdateIntervalMS, NSEC_PER_SEC)
queue:NULL
@ -150,8 +152,8 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self removePlayerItemObservers];
[self removePlayerLayer];
[self removePlayerItemObservers];
[_player removeObserver:self forKeyPath:playbackRate context:nil];
}
@ -262,9 +264,6 @@ static NSString *const timedMetadata = @"timedMetadata";
* observer set */
- (void)removePlayerItemObservers
{
if (_playerLayer) {
[_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath];
}
if (_playerItemObserversSet) {
[_playerItem removeObserver:self forKeyPath:statusKeyPath];
[_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath];
@ -278,13 +277,13 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)setSrc:(NSDictionary *)source
{
[self removePlayerLayer];
[self removePlayerTimeObserver];
[self removePlayerItemObservers];
_playerItem = [self playerItemForSource:source];
[self addPlayerItemObservers];
[_player pause];
[self removePlayerLayer];
[_playerViewController.view removeFromSuperview];
_playerViewController = nil;
@ -469,10 +468,17 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)attachListeners
{
// listen for end of file
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVPlayerItemDidPlayToEndTimeNotification
object:[_player currentItem]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[_player currentItem]];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVPlayerItemPlaybackStalledNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playbackStalled:)
name:AVPlayerItemPlaybackStalledNotification
@ -629,6 +635,7 @@ static NSString *const timedMetadata = @"timedMetadata";
[_player setMuted:NO];
}
[self setSelectedTextTrack:_selectedTextTrack];
[self setResizeMode:_resizeMode];
[self setRepeat:_repeat];
[self setPaused:_paused];
@ -639,6 +646,50 @@ static NSString *const timedMetadata = @"timedMetadata";
_repeat = repeat;
}
- (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack {
_selectedTextTrack = selectedTextTrack;
NSString *type = selectedTextTrack[@"type"];
AVMediaSelectionGroup *group = [_player.currentItem.asset
mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible];
AVMediaSelectionOption *option;
if ([type isEqualToString:@"disabled"]) {
// Do nothing. We want to ensure option is nil
} else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) {
NSString *value = selectedTextTrack[@"value"];
for (int i = 0; i < group.options.count; ++i) {
AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i];
NSString *optionValue;
if ([type isEqualToString:@"language"]) {
optionValue = [currentOption extendedLanguageTag];
} else {
optionValue = [[[currentOption commonMetadata]
valueForKey:@"value"]
objectAtIndex:0];
}
if ([value isEqualToString:optionValue]) {
option = currentOption;
break;
}
}
//} else if ([type isEqualToString:@"default"]) {
// option = group.defaultOption; */
} else if ([type isEqualToString:@"index"]) {
if ([selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) {
int index = [selectedTextTrack[@"value"] intValue];
if (group.options.count > index) {
option = [group.options objectAtIndex:index];
}
}
} else { // default. invalid type or "system"
[_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group];
return;
}
// If a match isn't found, option will be nil and text tracks will be disabled
[_player.currentItem selectMediaOption:option inMediaSelectionGroup:group];
}
- (BOOL)getFullscreen
{
return _fullscreenPlayerPresented;
@ -743,6 +794,11 @@ static NSString *const timedMetadata = @"timedMetadata";
- (void)setProgressUpdateInterval:(float)progressUpdateInterval
{
_progressUpdateInterval = progressUpdateInterval;
if (_timeObserver) {
[self removePlayerTimeObserver];
[self addPlayerTimeObserver];
}
}
- (void)removePlayerLayer

View File

@ -22,6 +22,7 @@ RCT_EXPORT_MODULE();
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
RCT_EXPORT_VIEW_PROPERTY(selectedTextTrack, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
RCT_EXPORT_VIEW_PROPERTY(muted, BOOL);
RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);

View File

@ -9,12 +9,8 @@
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[_rctDelegate videoPlayerViewControllerWillDismiss:self];
[_rctDelegate videoPlayerViewControllerDidDismiss:self];
}
- (void)viewWillDisappear:(BOOL)animated {
[_rctDelegate videoPlayerViewControllerWillDismiss:self];
[super viewWillDisappear:animated];
}
@end

View File

@ -1,6 +1,6 @@
{
"name": "react-native-video",
"version": "2.1.1",
"version": "2.2.0",
"description": "A <Video /> element for react-native",
"main": "Video.js",
"license": "MIT",