ExoPlayer (#426)

This commit is contained in:
Andrew Jack 2017-01-11 12:51:45 +00:00 committed by Matt Apperson
parent cd53e389a0
commit 9a936c9e8f
32 changed files with 1744 additions and 71 deletions

View File

@ -150,6 +150,7 @@ using System.Collections.Generic;
onProgress={this.setTime} // Callback every ~250ms with currentTime
onEnd={this.onEnd} // Callback when playback finishes
onError={this.videoError} // Callback when video cannot be loaded
onBuffer={this.onBuffer} // Callback when remote video is buffering
style={styles.backgroundVideo} />
// Later to trigger fullscreen
@ -250,7 +251,7 @@ Toggles a fullscreen player. Access using a ref to the component.
- [ ] Add support for captions
- [ ] Add support for playing multiple videos in a sequence (will interfere with current `repeat` implementation)
- [ ] Callback to get buffering progress for remote videos
- [x] Callback to get buffering progress for remote videos
- [ ] Bring API closer to HTML5 `<Video>` [reference](http://devdocs.io/html/element/video)
[1]: https://github.com/brentvatne/react-native-login/blob/56c47a5d1e23781e86e19b27e10427fd6391f666/App/Screens/UserInfoScreen.js#L32-L35

View File

@ -131,6 +131,24 @@ export default class Video extends Component {
}
};
_onAudioBecomingNoisy = () => {
if (this.props.onAudioBecomingNoisy) {
this.props.onAudioBecomingNoisy();
}
};
_onAudioFocusChanged = (event) => {
if (this.props.onAudioFocusChanged) {
this.props.onAudioFocusChanged(event.nativeEvent);
}
};
_onBuffer = (event) => {
if (this.props.onBuffer) {
this.props.onBuffer(event.nativeEvent);
}
};
render() {
const resizeMode = this.props.resizeMode;
const source = resolveAssetSource(this.props.source) || {};
@ -162,7 +180,7 @@ export default class Video extends Component {
uri,
isNetwork,
isAsset,
type: source.type || 'mp4',
type: source.type,
mainVer: source.mainVer || 0,
patchVer: source.patchVer || 0,
},
@ -172,6 +190,7 @@ export default class Video extends Component {
onVideoProgress: this._onProgress,
onVideoSeek: this._onSeek,
onVideoEnd: this._onEnd,
onVideoBuffer: this._onBuffer,
onVideoFullscreenPlayerWillPresent: this._onFullscreenPlayerWillPresent,
onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent,
onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss,
@ -180,6 +199,8 @@ export default class Video extends Component {
onPlaybackStalled: this._onPlaybackStalled,
onPlaybackResume: this._onPlaybackResume,
onPlaybackRateChange: this._onPlaybackRateChange,
onAudioFocusChanged: this._onAudioFocusChanged,
onAudioBecomingNoisy: this._onAudioBecomingNoisy,
});
if (this.props.poster && this.state.showPoster) {
@ -222,6 +243,7 @@ Video.propTypes = {
fullscreen: PropTypes.bool,
onVideoLoadStart: PropTypes.func,
onVideoLoad: PropTypes.func,
onVideoBuffer: PropTypes.func,
onVideoError: PropTypes.func,
onVideoProgress: PropTypes.func,
onVideoSeek: PropTypes.func,
@ -248,11 +270,13 @@ Video.propTypes = {
rate: PropTypes.number,
playInBackground: PropTypes.bool,
playWhenInactive: PropTypes.bool,
disableFocus: PropTypes.bool,
controls: PropTypes.bool,
currentTime: PropTypes.number,
progressUpdateInterval: PropTypes.number,
onLoadStart: PropTypes.func,
onLoad: PropTypes.func,
onBuffer: PropTypes.func,
onError: PropTypes.func,
onProgress: PropTypes.func,
onSeek: PropTypes.func,
@ -265,6 +289,8 @@ Video.propTypes = {
onPlaybackStalled: PropTypes.func,
onPlaybackResume: PropTypes.func,
onPlaybackRateChange: PropTypes.func,
onAudioFocusChanged: PropTypes.func,
onAudioBecomingNoisy: PropTypes.func,
/* Required by react-native */
scaleX: PropTypes.number,

View File

@ -0,0 +1,47 @@
## react-native-video - ExoPlayer
This is an Android React Native video component based on ExoPlayer v2.
> ExoPlayer is an application level media player for Android. It provides an alternative to Androids MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Androids MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates.
https://github.com/google/ExoPlayer
## Benefits over `react-native-video@0.9.0`:
- Android Video library built by Google, with a lot of support
- Supports DASH, HlS, & SmoothStreaming adaptive streams
- Supports formats such as MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC).
- Fewer device specific issues
- Highly customisable
## ExoPlayer only props
```javascript
render() {
return (
<Video
...
disableFocus={true} // disables audio focus and wake lock (default false)
onAudioBecomingNoisy={this.onAudioBecomingNoisy} // Callback when audio is becoming noisy - should pause video
onAudioFocusChanged={this.onAudioFocusChanged} // Callback when audio focus has been lost - pause if focus has been lost
/>
)
}
onAudioBecomingNoisy = () => {
this.setState({ pause: true })
}
onAudioFocusChanged(event: { hasAudioFocus: boolean }) {
if (!this.state.paused && !event.hasAudioFocus) {
this.setState({ paused: true })
}
}
```
## Unimplemented props
- `playInBackground={true}`
- `rate={1.0}`
- Expansion file - `source={{ mainVer: 1, patchVer: 0 }}`

View File

@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
}
}
dependencies {
provided 'com.facebook.react:react-native:+'
compile 'com.google.android.exoplayer:exoplayer:r2.1.1'
compile('com.google.android.exoplayer:extension-okhttp:r2.1.1') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
compile 'com.squareup.okhttp3:okhttp:3.4.2'
}

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.brentvatne.react">
</manifest>

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.brentvatne.exoplayer;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* A {@link FrameLayout} that resizes itself to match a specified aspect ratio.
*/
public final class AspectRatioFrameLayout extends FrameLayout {
/**
* The {@link FrameLayout} will not resize itself if the fractional difference between its natural
* aspect ratio and the requested aspect ratio falls below this threshold.
* <p>
* This tolerance allows the view to occupy the whole of the screen when the requested aspect
* ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce
* the number of view layers that need to be composited by the underlying system, which can help
* to reduce power consumption.
*/
private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f;
private float videoAspectRatio;
private @ResizeMode.Mode int resizeMode = ResizeMode.RESIZE_MODE_FIT;
public AspectRatioFrameLayout(Context context) {
this(context, null);
}
public AspectRatioFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Set the aspect ratio that this view should satisfy.
*
* @param widthHeightRatio The width to height ratio.
*/
public void setAspectRatio(float widthHeightRatio) {
if (this.videoAspectRatio != widthHeightRatio) {
this.videoAspectRatio = widthHeightRatio;
requestLayout();
}
}
/**
* Get the aspect ratio that this view should satisfy.
*
* @return widthHeightRatio The width to height ratio.
*/
public float getAspectRatio() {
return videoAspectRatio;
}
/**
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
*
* @param resizeMode The resize mode.
*/
public void setResizeMode(@ResizeMode.Mode int resizeMode) {
if (this.resizeMode != resizeMode) {
this.resizeMode = resizeMode;
requestLayout();
}
}
/**
* Gets the resize mode which can be of value {@link ResizeMode.Mode}
*
* @return resizeMode The resize mode.
*/
public @ResizeMode.Mode int getResizeMode() {
return resizeMode;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (videoAspectRatio == 0) {
// Aspect ratio not set.
return;
}
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
int width = measuredWidth;
int height = measuredHeight;
float viewAspectRatio = (float) measuredWidth / measuredHeight;
float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) {
// We're within the allowed tolerance.
return;
}
switch (resizeMode) {
case ResizeMode.RESIZE_MODE_FIXED_WIDTH:
height = (int) (measuredWidth / videoAspectRatio);
break;
case ResizeMode.RESIZE_MODE_FIXED_HEIGHT:
width = (int) (measuredHeight * videoAspectRatio);
break;
case ResizeMode.RESIZE_MODE_FILL:
// Do nothing width and height is the same as the view
break;
case ResizeMode.RESIZE_MODE_CENTER_CROP:
width = (int) (measuredHeight * videoAspectRatio);
// Scale video if it doesn't fill the measuredWidth
if (width < measuredWidth) {
float scaleFactor = (float) measuredWidth / width;
width = (int) (width * scaleFactor);
height = (int) (measuredHeight * scaleFactor);
}
break;
default:
if (aspectDeformation > 0) {
height = (int) (measuredWidth / videoAspectRatio);
} else {
width = (int) (measuredHeight * videoAspectRatio);
}
break;
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
}

View File

@ -0,0 +1,70 @@
package com.brentvatne.exoplayer;
import android.content.Context;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
public class DataSourceUtil {
private DataSourceUtil() {
}
private static DataSource.Factory rawDataSourceFactory = null;
private static DataSource.Factory defaultDataSourceFactory = null;
private static String userAgent = null;
public static void setUserAgent(String userAgent) {
DataSourceUtil.userAgent = userAgent;
}
public static String getUserAgent(Context context) {
if (userAgent == null) {
userAgent = Util.getUserAgent(context.getApplicationContext(), "ReactNativeVideo");
}
return userAgent;
}
public static DataSource.Factory getRawDataSourceFactory(Context context) {
if (rawDataSourceFactory == null) {
rawDataSourceFactory = buildRawDataSourceFactory(context);
}
return rawDataSourceFactory;
}
public static void setRawDataSourceFactory(DataSource.Factory factory) {
DataSourceUtil.rawDataSourceFactory = factory;
}
public static DataSource.Factory getDefaultDataSourceFactory(Context context, DefaultBandwidthMeter bandwidthMeter) {
if (defaultDataSourceFactory == null) {
defaultDataSourceFactory = buildDataSourceFactory(context, bandwidthMeter);
}
return defaultDataSourceFactory;
}
public static void setDefaultDataSourceFactory(DataSource.Factory factory) {
DataSourceUtil.defaultDataSourceFactory = factory;
}
private static DataSource.Factory buildRawDataSourceFactory(Context context) {
return new RawResourceDataSourceFactory(context.getApplicationContext());
}
private static DataSource.Factory buildDataSourceFactory(Context context, DefaultBandwidthMeter bandwidthMeter) {
Context appContext = context.getApplicationContext();
return new DefaultDataSourceFactory(appContext, bandwidthMeter,
buildHttpDataSourceFactory(appContext, bandwidthMeter));
}
private static HttpDataSource.Factory buildHttpDataSourceFactory(Context context, DefaultBandwidthMeter bandwidthMeter) {
return new OkHttpDataSourceFactory(OkHttpClientProvider.getOkHttpClient(), getUserAgent(context), bandwidthMeter);
}
}

View File

@ -0,0 +1,226 @@
package com.brentvatne.exoplayer;
import android.annotation.TargetApi;
import android.content.Context;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.SubtitleView;
import java.util.List;
@TargetApi(16)
public final class ExoPlayerView extends FrameLayout {
private final View surfaceView;
private final View shutterView;
private final SubtitleView subtitleLayout;
private final AspectRatioFrameLayout layout;
private final ComponentListener componentListener;
private SimpleExoPlayer player;
public ExoPlayerView(Context context) {
this(context, null);
}
public ExoPlayerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
boolean useTextureView = false;
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
componentListener = new ComponentListener();
FrameLayout.LayoutParams aspectRatioParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT);
aspectRatioParams.gravity = Gravity.CENTER;
layout = new AspectRatioFrameLayout(context);
layout.setLayoutParams(aspectRatioParams);
shutterView = new View(getContext());
shutterView.setLayoutParams(params);
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
subtitleLayout = new SubtitleView(context);
subtitleLayout.setLayoutParams(params);
subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize();
View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
view.setLayoutParams(params);
surfaceView = view;
layout.addView(surfaceView, 0, params);
layout.addView(shutterView, 1, params);
layout.addView(subtitleLayout, 2, params);
addViewInLayout(layout, 0, aspectRatioParams);
}
/**
* Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and
* {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous
* assignments are overridden.
*
* @param player The {@link SimpleExoPlayer} to use.
*/
public void setPlayer(SimpleExoPlayer player) {
if (this.player == player) {
return;
}
if (this.player != null) {
this.player.setTextOutput(null);
this.player.setVideoListener(null);
this.player.removeListener(componentListener);
this.player.setVideoSurface(null);
}
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);
}
player.setVideoListener(componentListener);
player.addListener(componentListener);
player.setTextOutput(componentListener);
}
}
/**
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
*
* @param resizeMode The resize mode.
*/
public void setResizeMode(@ResizeMode.Mode int resizeMode) {
if (layout.getResizeMode() != resizeMode) {
layout.setResizeMode(resizeMode);
post(measureAndLayout);
}
}
/**
* Get the view onto which video is rendered. This is either a {@link SurfaceView} (default)
* or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
*
* @return either a {@link SurfaceView} or a {@link TextureView}.
*/
public View getVideoSurfaceView() {
return surfaceView;
}
private final Runnable measureAndLayout = new Runnable() {
@Override
public void run() {
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
private void updateForCurrentTrackSelections() {
if (player == null) {
return;
}
TrackSelectionArray selections = player.getCurrentTrackSelections();
for (int i = 0; i < selections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
// onRenderedFirstFrame().
return;
}
}
// Video disabled so the shutter must be closed.
shutterView.setVisibility(VISIBLE);
}
private final class ComponentListener implements SimpleExoPlayer.VideoListener,
TextRenderer.Output, ExoPlayer.EventListener {
// TextRenderer.Output implementation
@Override
public void onCues(List<Cue> cues) {
subtitleLayout.onCues(cues);
}
// SimpleExoPlayer.VideoListener implementation
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
boolean isInitialRatio = layout.getAspectRatio() == 0;
layout.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height);
// React native workaround for measuring and layout on initial load.
if (isInitialRatio) {
post(measureAndLayout);
}
}
@Override
public void onRenderedFirstFrame() {
shutterView.setVisibility(INVISIBLE);
}
// ExoPlayer.EventListener implementation
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
// Do nothing.
}
@Override
public void onPlayerError(ExoPlaybackException e) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() {
// Do nothing.
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
// Do nothing.
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
updateForCurrentTrackSelections();
}
}
}

View File

@ -0,0 +1,20 @@
package com.brentvatne.exoplayer;
import android.content.Context;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
class RawResourceDataSourceFactory implements DataSource.Factory {
private final Context context;
RawResourceDataSourceFactory(Context context) {
this.context = context;
}
@Override
public DataSource createDataSource() {
return new RawResourceDataSource(context, null);
}
}

View File

@ -0,0 +1,553 @@
package com.brentvatne.exoplayer;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.widget.FrameLayout;
import com.brentvatne.react.R;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.uimanager.ThemedReactContext;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
@SuppressLint("ViewConstructor")
class ReactExoplayerView extends FrameLayout implements
LifecycleEventListener,
ExoPlayer.EventListener,
BecomingNoisyListener,
AudioManager.OnAudioFocusChangeListener {
private static final String TAG = "ReactExoplayerView";
private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private static final CookieManager DEFAULT_COOKIE_MANAGER;
private static final int SHOW_PROGRESS = 1;
static {
DEFAULT_COOKIE_MANAGER = new CookieManager();
DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
}
private final VideoEventEmitter eventEmitter;
private Handler mainHandler;
private Timeline.Window window;
private ExoPlayerView exoPlayerView;
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private MappingTrackSelector trackSelector;
private boolean playerNeedsSource;
private boolean shouldRestorePosition;
private int playerWindow;
private long playerPosition;
private boolean loadVideoStarted;
private boolean isPaused = true;
private boolean isBuffering;
private boolean isTimelineStatic;
// Props from React
private Uri srcUri;
private String extension;
private boolean repeat;
private boolean disableFocus;
// \ End props
// React
private final ThemedReactContext themedReactContext;
private final AudioManager audioManager;
private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver;
private final Handler progressHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PROGRESS:
if (player != null
&& player.getPlaybackState() == ExoPlayer.STATE_READY
&& player.getPlayWhenReady()
) {
long pos = player.getCurrentPosition();
eventEmitter.progressChanged(pos, player.getBufferedPercentage());
msg = obtainMessage(SHOW_PROGRESS);
sendMessageDelayed(msg, 1000 - (pos % 1000));
}
break;
}
}
};
public ReactExoplayerView(ThemedReactContext context) {
super(context);
createViews();
this.eventEmitter = new VideoEventEmitter(context);
this.themedReactContext = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
themedReactContext.addLifecycleEventListener(this);
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
}
@Override
public void setId(int id) {
super.setId(id);
eventEmitter.setViewId(id);
}
private void createViews() {
mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler();
window = new Timeline.Window();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
LayoutParams layoutParams = new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
exoPlayerView = new ExoPlayerView(getContext());
exoPlayerView.setLayoutParams(layoutParams);
addView(exoPlayerView, 0, layoutParams);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
initializePlayer();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopPlayback();
}
// LifecycleEventListener implementation
@Override
public void onHostResume() {
startPlayback();
}
@Override
public void onHostPause() {
setPlayWhenReady(false);
}
@Override
public void onHostDestroy() {
stopPlayback();
}
public void cleanUpResources() {
stopPlayback();
}
// Internal methods
private void initializePlayer() {
if (player == null) {
TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, new DefaultLoadControl());
player.addListener(this);
exoPlayerView.setPlayer(player);
if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow);
} else {
player.seekTo(playerWindow, playerPosition);
}
}
audioBecomingNoisyReceiver.setListener(this);
setPlayWhenReady(!isPaused);
playerNeedsSource = true;
}
if (playerNeedsSource && srcUri != null) {
MediaSource mediaSource = buildMediaSource(srcUri, extension);
mediaSource = repeat ? new LoopingMediaSource(mediaSource) : mediaSource;
player.prepare(mediaSource, !shouldRestorePosition, true);
playerNeedsSource = false;
eventEmitter.loadStart();
loadVideoStarted = true;
}
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, null);
case C.TYPE_DASH:
return new DashMediaSource(uri, buildDataSourceFactory(false),
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, null);
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, null);
case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
mainHandler, null);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
private void releasePlayer() {
if (player != null) {
isPaused = player.getPlayWhenReady();
shouldRestorePosition = false;
playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition();
}
player.release();
player = null;
trackSelector = null;
}
progressHandler.removeMessages(SHOW_PROGRESS);
themedReactContext.removeLifecycleEventListener(this);
audioBecomingNoisyReceiver.removeListener();
}
private boolean requestAudioFocus() {
if (disableFocus) {
return true;
}
int result = audioManager.requestAudioFocus(this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
private void setPlayWhenReady(boolean playWhenReady) {
if (player == null) {
return;
}
if (playWhenReady) {
boolean hasAudioFocus = requestAudioFocus();
if (hasAudioFocus) {
player.setPlayWhenReady(true);
}
} else {
player.setPlayWhenReady(false);
}
}
private void startPlayback() {
if (player != null) {
switch (player.getPlaybackState()) {
case ExoPlayer.STATE_IDLE:
case ExoPlayer.STATE_ENDED:
initializePlayer();
break;
case ExoPlayer.STATE_BUFFERING:
case ExoPlayer.STATE_READY:
if (!player.getPlayWhenReady()) {
setPlayWhenReady(true);
}
break;
default:
break;
}
} else {
initializePlayer();
}
if (!disableFocus) {
setKeepScreenOn(true);
}
}
private void pausePlayback() {
if (player != null) {
if (player.getPlayWhenReady()) {
setPlayWhenReady(false);
}
}
setKeepScreenOn(false);
}
private void stopPlayback() {
onStopPlayback();
releasePlayer();
}
private void onStopPlayback() {
setKeepScreenOn(false);
audioManager.abandonAudioFocus(this);
}
/**
* Returns a new DataSource factory.
*
* @param useBandwidthMeter Whether to set {@link #BANDWIDTH_METER} as a listener to the new
* DataSource factory.
* @return A new DataSource factory.
*/
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return DataSourceUtil.getDefaultDataSourceFactory(getContext(), useBandwidthMeter ? BANDWIDTH_METER : null);
}
// AudioManager.OnAudioFocusChangeListener implementation
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
eventEmitter.audioFocusChanged(false);
break;
case AudioManager.AUDIOFOCUS_GAIN:
eventEmitter.audioFocusChanged(true);
break;
default:
break;
}
if (player != null) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume
player.setVolume(0.8f);
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Raise it back to normal
player.setVolume(1);
}
}
}
// AudioBecomingNoisyListener implementation
@Override
public void onAudioBecomingNoisy() {
eventEmitter.audioBecomingNoisy();
}
// ExoPlayer.EventListener implementation
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
switch (playbackState) {
case ExoPlayer.STATE_IDLE:
text += "idle";
eventEmitter.idle();
break;
case ExoPlayer.STATE_BUFFERING:
text += "buffering";
onBuffering(true);
break;
case ExoPlayer.STATE_READY:
text += "ready";
eventEmitter.ready();
onBuffering(false);
startProgressHandler();
videoLoaded();
break;
case ExoPlayer.STATE_ENDED:
text += "ended";
eventEmitter.end();
onStopPlayback();
break;
default:
text += "unknown";
break;
}
Log.d(TAG, text);
}
private void startProgressHandler() {
progressHandler.sendEmptyMessage(SHOW_PROGRESS);
}
private void videoLoaded() {
if (loadVideoStarted) {
loadVideoStarted = false;
Format videoFormat = player.getVideoFormat();
int width = videoFormat != null ? videoFormat.width : 0;
int height = videoFormat != null ? videoFormat.height : 0;
eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height);
}
}
private void onBuffering(boolean buffering) {
if (isBuffering == buffering) {
return;
}
isBuffering = buffering;
if (buffering) {
eventEmitter.buffering(true);
} else {
eventEmitter.buffering(false);
}
}
@Override
public void onPositionDiscontinuity() {
// Do nothing.
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
isTimelineStatic = !timeline.isEmpty()
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do Nothing.
}
@Override
public void onPlayerError(ExoPlaybackException e) {
String errorString = null;
if (e.type == ExoPlaybackException.TYPE_RENDERER) {
Exception cause = e.getRendererException();
if (cause instanceof MediaCodecRenderer.DecoderInitializationException) {
// Special case for decoder initialization failures.
MediaCodecRenderer.DecoderInitializationException decoderInitializationException =
(MediaCodecRenderer.DecoderInitializationException) cause;
if (decoderInitializationException.decoderName == null) {
if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) {
errorString = getResources().getString(R.string.error_querying_decoders);
} else if (decoderInitializationException.secureDecoderRequired) {
errorString = getResources().getString(R.string.error_no_secure_decoder,
decoderInitializationException.mimeType);
} else {
errorString = getResources().getString(R.string.error_no_decoder,
decoderInitializationException.mimeType);
}
} else {
errorString = getResources().getString(R.string.error_instantiating_decoder,
decoderInitializationException.decoderName);
}
}
}
if (errorString != null) {
eventEmitter.error(errorString, e);
}
playerNeedsSource = true;
}
// ReactExoplayerViewManager public api
public void setSrc(final Uri uri, final String extension) {
if (uri != null) {
this.srcUri = uri;
this.extension = extension;
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(getContext(), BANDWIDTH_METER);
}
}
public void setRawSrc(final Uri uri, final String extension) {
if (uri != null) {
this.srcUri = uri;
this.extension = extension;
this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(getContext());
}
}
public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) {
exoPlayerView.setResizeMode(resizeMode);
}
public void setRepeatModifier(boolean repeat) {
this.repeat = repeat;
}
public void setPausedModifier(boolean paused) {
isPaused = paused;
if (player != null) {
if (!paused) {
startPlayback();
} else {
pausePlayback();
}
}
}
public void setMutedModifier(boolean muted) {
if (player != null) {
player.setVolume(muted ? 0 : 1);
}
}
public void setVolumeModifier(float volume) {
if (player != null) {
player.setVolume(volume);
}
}
public void seekTo(long positionMs) {
if (player != null) {
eventEmitter.seek(player.getCurrentPosition(), positionMs);
player.seekTo(positionMs);
}
}
public void setRateModifier(float rate) {
// TODO: waiting on ExoPlayer implementation
// https://github.com/google/ExoPlayer/issues/26
}
public void setPlayInBackground(boolean playInBackground) {
// TODO: implement
}
public void setDisableFocus(boolean disableFocus) {
this.disableFocus = disableFocus;
}
}

View File

@ -0,0 +1,160 @@
package com.brentvatne.exoplayer;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.exoplayer2.upstream.RawResourceDataSource;
import java.util.Map;
import javax.annotation.Nullable;
public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerView> {
private static final String REACT_CLASS = "RCTVideo";
private static final String PROP_SRC = "src";
private static final String PROP_SRC_URI = "uri";
private static final String PROP_SRC_TYPE = "type";
private static final String PROP_RESIZE_MODE = "resizeMode";
private static final String PROP_REPEAT = "repeat";
private static final String PROP_PAUSED = "paused";
private static final String PROP_MUTED = "muted";
private static final String PROP_VOLUME = "volume";
private static final String PROP_SEEK = "seek";
private static final String PROP_RATE = "rate";
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
private static final String PROP_DISABLE_FOCUS = "disableFocus";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected ReactExoplayerView createViewInstance(ThemedReactContext themedReactContext) {
return new ReactExoplayerView(themedReactContext);
}
@Override
public void onDropViewInstance(ReactExoplayerView view) {
view.cleanUpResources();
}
@Override
public @Nullable Map<String, Object> getExportedCustomDirectEventTypeConstants() {
MapBuilder.Builder<String, Object> builder = MapBuilder.builder();
for (String event : VideoEventEmitter.Events) {
builder.put(event, MapBuilder.of("registrationName", event));
}
return builder.build();
}
@Override
public @Nullable Map<String, Object> getExportedViewConstants() {
return MapBuilder.<String, Object>of(
"ScaleNone", Integer.toString(ResizeMode.RESIZE_MODE_FIT),
"ScaleAspectFit", Integer.toString(ResizeMode.RESIZE_MODE_FIT),
"ScaleToFill", Integer.toString(ResizeMode.RESIZE_MODE_FILL),
"ScaleAspectFill", Integer.toString(ResizeMode.RESIZE_MODE_CENTER_CROP)
);
}
@ReactProp(name = PROP_SRC)
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext();
String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null;
String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
if (TextUtils.isEmpty(uriString)) {
return;
}
if (startsWithValidScheme(uriString)) {
Uri srcUri = Uri.parse(uriString);
if (srcUri != null) {
videoView.setSrc(srcUri, extension);
}
} else {
int identifier = context.getResources().getIdentifier(
uriString,
"raw",
context.getPackageName()
);
if (identifier > 0) {
Uri srcUri = RawResourceDataSource.buildRawResourceUri(identifier);
if (srcUri != null) {
videoView.setRawSrc(srcUri, extension);
}
}
}
}
@ReactProp(name = PROP_RESIZE_MODE)
public void setResizeMode(final ReactExoplayerView videoView, final String resizeModeOrdinalString) {
videoView.setResizeModeModifier(convertToIntDef(resizeModeOrdinalString));
}
@ReactProp(name = PROP_REPEAT, defaultBoolean = false)
public void setRepeat(final ReactExoplayerView videoView, final boolean repeat) {
videoView.setRepeatModifier(repeat);
}
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
public void setPaused(final ReactExoplayerView videoView, final boolean paused) {
videoView.setPausedModifier(paused);
}
@ReactProp(name = PROP_MUTED, defaultBoolean = false)
public void setMuted(final ReactExoplayerView videoView, final boolean muted) {
videoView.setMutedModifier(muted);
}
@ReactProp(name = PROP_VOLUME, defaultFloat = 1.0f)
public void setVolume(final ReactExoplayerView videoView, final float volume) {
videoView.setVolumeModifier(volume);
}
@ReactProp(name = PROP_SEEK)
public void setSeek(final ReactExoplayerView videoView, final float seek) {
videoView.seekTo(Math.round(seek * 1000f));
}
@ReactProp(name = PROP_RATE)
public void setRate(final ReactExoplayerView videoView, final float rate) {
videoView.setRateModifier(rate);
}
@ReactProp(name = PROP_PLAY_IN_BACKGROUND, defaultBoolean = false)
public void setPlayInBackground(final ReactExoplayerView videoView, final boolean playInBackground) {
videoView.setPlayInBackground(playInBackground);
}
@ReactProp(name = PROP_DISABLE_FOCUS, defaultBoolean = false)
public void setDisableFocus(final ReactExoplayerView videoView, final boolean disableFocus) {
videoView.setDisableFocus(disableFocus);
}
private boolean startsWithValidScheme(String uriString) {
return uriString.startsWith("http://")
|| uriString.startsWith("https://")
|| uriString.startsWith("content://")
|| uriString.startsWith("file://")
|| uriString.startsWith("asset://");
}
private @ResizeMode.Mode int convertToIntDef(String resizeModeOrdinalString) {
if (!TextUtils.isEmpty(resizeModeOrdinalString)) {
int resizeModeOrdinal = Integer.parseInt(resizeModeOrdinalString);
return ResizeMode.toResizeMode(resizeModeOrdinal);
}
return ResizeMode.RESIZE_MODE_FIT;
}
}

View File

@ -0,0 +1,63 @@
package com.brentvatne.exoplayer;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.SOURCE;
class ResizeMode {
/**
* Either the width or height is decreased to obtain the desired aspect ratio.
*/
static final int RESIZE_MODE_FIT = 0;
/**
* The width is fixed and the height is increased or decreased to obtain the desired aspect ratio.
*/
static final int RESIZE_MODE_FIXED_WIDTH = 1;
/**
* The height is fixed and the width is increased or decreased to obtain the desired aspect ratio.
*/
static final int RESIZE_MODE_FIXED_HEIGHT = 2;
/**
* The height and the width is increased or decreased to fit the size of the view.
*/
static final int RESIZE_MODE_FILL = 3;
/**
* Keeps the aspect ratio but takes up the view's size.
*/
static final int RESIZE_MODE_CENTER_CROP = 4;
@Retention(SOURCE)
@IntDef({
RESIZE_MODE_FIT,
RESIZE_MODE_FIXED_WIDTH,
RESIZE_MODE_FIXED_HEIGHT,
RESIZE_MODE_FILL,
RESIZE_MODE_CENTER_CROP
})
public @interface Mode {
}
@ResizeMode.Mode static int toResizeMode(int ordinal) {
switch (ordinal) {
case ResizeMode.RESIZE_MODE_FIXED_WIDTH:
return ResizeMode.RESIZE_MODE_FIXED_WIDTH;
case ResizeMode.RESIZE_MODE_FIXED_HEIGHT:
return ResizeMode.RESIZE_MODE_FIXED_HEIGHT;
case ResizeMode.RESIZE_MODE_FILL:
return ResizeMode.RESIZE_MODE_FILL;
case ResizeMode.RESIZE_MODE_CENTER_CROP:
return ResizeMode.RESIZE_MODE_CENTER_CROP;
case ResizeMode.RESIZE_MODE_FIT:
default:
return ResizeMode.RESIZE_MODE_FIT;
}
}
}

View File

@ -0,0 +1,184 @@
package com.brentvatne.exoplayer;
import android.support.annotation.StringDef;
import android.view.View;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
class VideoEventEmitter {
private final RCTEventEmitter eventEmitter;
private int viewId = View.NO_ID;
VideoEventEmitter(ReactContext reactContext) {
this.eventEmitter = reactContext.getJSModule(RCTEventEmitter.class);
}
private static final String EVENT_LOAD_START = "onVideoLoadStart";
private static final String EVENT_LOAD = "onVideoLoad";
private static final String EVENT_ERROR = "onVideoError";
private static final String EVENT_PROGRESS = "onVideoProgress";
private static final String EVENT_SEEK = "onVideoSeek";
private static final String EVENT_END = "onVideoEnd";
private static final String EVENT_STALLED = "onPlaybackStalled";
private static final String EVENT_RESUME = "onPlaybackResume";
private static final String EVENT_READY = "onReadyForDisplay";
private static final String EVENT_BUFFER = "onVideoBuffer";
private static final String EVENT_IDLE = "onVideoIdle";
private static final String EVENT_AUDIO_BECOMING_NOISY = "onAudioBecomingNoisy";
private static final String EVENT_AUDIO_FOCUS_CHANGE = "onAudioFocusChanged";
static final String[] Events = {
EVENT_LOAD_START,
EVENT_LOAD,
EVENT_ERROR,
EVENT_PROGRESS,
EVENT_SEEK,
EVENT_END,
EVENT_STALLED,
EVENT_RESUME,
EVENT_READY,
EVENT_BUFFER,
EVENT_IDLE,
EVENT_AUDIO_BECOMING_NOISY,
EVENT_AUDIO_FOCUS_CHANGE,
};
@Retention(RetentionPolicy.SOURCE)
@StringDef({
EVENT_LOAD_START,
EVENT_LOAD,
EVENT_ERROR,
EVENT_PROGRESS,
EVENT_SEEK,
EVENT_END,
EVENT_STALLED,
EVENT_RESUME,
EVENT_READY,
EVENT_BUFFER,
EVENT_IDLE,
EVENT_AUDIO_BECOMING_NOISY,
EVENT_AUDIO_FOCUS_CHANGE,
})
@interface VideoEvents {
}
private static final String EVENT_PROP_FAST_FORWARD = "canPlayFastForward";
private static final String EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward";
private static final String EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse";
private static final String EVENT_PROP_REVERSE = "canPlayReverse";
private static final String EVENT_PROP_STEP_FORWARD = "canStepForward";
private static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward";
private static final String EVENT_PROP_DURATION = "duration";
private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration";
private static final String EVENT_PROP_CURRENT_TIME = "currentTime";
private static final String EVENT_PROP_SEEK_TIME = "seekTime";
private static final String EVENT_PROP_NATURAL_SIZE = "naturalSize";
private static final String EVENT_PROP_WIDTH = "width";
private static final String EVENT_PROP_HEIGHT = "height";
private static final String EVENT_PROP_ORIENTATION = "orientation";
private static final String EVENT_PROP_HAS_AUDIO_FOCUS = "hasAudioFocus";
private static final String EVENT_PROP_IS_BUFFERING = "isBuffering";
private static final String EVENT_PROP_ERROR = "error";
private static final String EVENT_PROP_ERROR_STRING = "errorString";
private static final String EVENT_PROP_ERROR_EXCEPTION = "";
void setViewId(int viewId) {
this.viewId = viewId;
}
void loadStart() {
receiveEvent(EVENT_LOAD_START, null);
}
void load(double duration, double currentPosition, int videoWidth, int videoHeight) {
WritableMap event = Arguments.createMap();
event.putDouble(EVENT_PROP_DURATION, duration / 1000D);
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
WritableMap naturalSize = Arguments.createMap();
naturalSize.putInt(EVENT_PROP_WIDTH, videoWidth);
naturalSize.putInt(EVENT_PROP_HEIGHT, videoHeight);
if (videoWidth > videoHeight) {
naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape");
} else {
naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait");
}
event.putMap(EVENT_PROP_NATURAL_SIZE, naturalSize);
// TODO: Actually check if you can.
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
event.putBoolean(EVENT_PROP_SLOW_FORWARD, true);
event.putBoolean(EVENT_PROP_SLOW_REVERSE, true);
event.putBoolean(EVENT_PROP_REVERSE, true);
event.putBoolean(EVENT_PROP_FAST_FORWARD, true);
event.putBoolean(EVENT_PROP_STEP_BACKWARD, true);
event.putBoolean(EVENT_PROP_STEP_FORWARD, true);
receiveEvent(EVENT_LOAD, event);
}
void progressChanged(double currentPosition, double bufferedDuration) {
WritableMap event = Arguments.createMap();
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
event.putDouble(EVENT_PROP_PLAYABLE_DURATION, bufferedDuration / 1000D);
receiveEvent(EVENT_PROGRESS, event);
}
void seek(long currentPosition, long seekTime) {
WritableMap event = Arguments.createMap();
event.putDouble(EVENT_PROP_CURRENT_TIME, currentPosition / 1000D);
event.putDouble(EVENT_PROP_SEEK_TIME, seekTime / 1000D);
receiveEvent(EVENT_SEEK, event);
}
void ready() {
receiveEvent(EVENT_READY, null);
}
void buffering(boolean isBuffering) {
WritableMap map = Arguments.createMap();
map.putBoolean(EVENT_PROP_IS_BUFFERING, isBuffering);
receiveEvent(EVENT_BUFFER, map);
}
void idle() {
receiveEvent(EVENT_IDLE, null);
}
void end() {
receiveEvent(EVENT_END, null);
}
void error(String errorString, Exception exception) {
WritableMap error = Arguments.createMap();
error.putString(EVENT_PROP_ERROR_STRING, errorString);
error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.getMessage());
WritableMap event = Arguments.createMap();
event.putMap(EVENT_PROP_ERROR, error);
receiveEvent(EVENT_ERROR, event);
}
void audioFocusChanged(boolean hasFocus) {
WritableMap map = Arguments.createMap();
map.putBoolean(EVENT_PROP_HAS_AUDIO_FOCUS, hasFocus);
receiveEvent(EVENT_AUDIO_FOCUS_CHANGE, map);
}
void audioBecomingNoisy() {
receiveEvent(EVENT_AUDIO_BECOMING_NOISY, null);
}
private void receiveEvent(@VideoEvents String type, WritableMap event) {
eventEmitter.receiveEvent(viewId, type, event);
}
}

View File

@ -0,0 +1,29 @@
package com.brentvatne.react;
import com.brentvatne.exoplayer.ReactExoplayerViewManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Collections;
import java.util.List;
public class ReactVideoPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.<ViewManager>singletonList(new ReactExoplayerViewManager());
}
}

View File

@ -0,0 +1,39 @@
package com.brentvatne.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
public class AudioBecomingNoisyReceiver extends BroadcastReceiver {
private final Context context;
private BecomingNoisyListener listener = BecomingNoisyListener.NO_OP;
public AudioBecomingNoisyReceiver(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
listener.onAudioBecomingNoisy();
}
}
public void setListener(BecomingNoisyListener listener) {
this.listener = listener;
IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
context.registerReceiver(this, intentFilter);
}
public void removeListener() {
this.listener = BecomingNoisyListener.NO_OP;
try {
context.unregisterReceiver(this);
} catch (Exception ignore) {
// ignore if already unregistered
}
}
}

View File

@ -0,0 +1,13 @@
package com.brentvatne.receiver;
public interface BecomingNoisyListener {
BecomingNoisyListener NO_OP = new BecomingNoisyListener() {
@Override public void onAudioBecomingNoisy() {
// NO_OP
}
};
void onAudioBecomingNoisy();
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
<string name="error_querying_decoders">Unable to query device decoders</string>
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
</resources>

View File

@ -16,6 +16,6 @@ android {
}
dependencies {
compile 'com.facebook.react:react-native:0.19.+'
provided 'com.facebook.react:react-native:+'
compile 'com.yqritc:android-scalablevideoview:1.0.1'
}

View File

@ -1,8 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>

View File

@ -2,7 +2,20 @@
; We fork some components by platform
.*/*[.]android.js
; Ignore "BUCK" generated dirs
# We fork some components by platform.
.*/*[.]android.js
# Ignore templates with `@flow` in header
.*/local-cli/generator.*
# Ignore malformed json
.*/node_modules/y18n/test/.*\.json
# Ignore the website subdir
<PROJECT_ROOT>/website/.*
# Ignore BUCK generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
@ -41,4 +54,5 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
unsafe.enable_getters_and_setters=true
[version]
^0.36.0

View File

@ -106,6 +106,7 @@ android {
}
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
@ -126,10 +127,10 @@ android {
}
dependencies {
compile project(':react-native-video')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-video')
// compile project(':react-native-video-exoplayer') // uncomment to use exoplayer
}
// Run this once to be able to run the application with BUCK

View File

@ -1,11 +1,9 @@
package com.videoplayer;
import android.app.Application;
import android.util.Log;
import com.facebook.react.ReactApplication;
import com.brentvatne.react.ReactVideoPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
@ -16,13 +14,23 @@ import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MainReactPackage(),
new ReactVideoPackage()
);
}
};
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),

View File

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
classpath 'com.android.tools.build:gradle:2.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -1,5 +1,6 @@
#Sat Oct 08 18:53:26 BST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

View File

@ -1,5 +1,12 @@
rootProject.name = 'VideoPlayer'
include ':app'
include ':react-native-video'
include ':app',
':react-native-video',
':react-native-video-exoplayer'
// Quick Local Development
//project(':react-native-video').projectDir = new File(rootProject.projectDir, '../../android')
//project(':react-native-video-exoplayer').projectDir = new File(rootProject.projectDir, '../../android-exoplayer')
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
project(':react-native-video-exoplayer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')

View File

@ -15,11 +15,6 @@ import {
import Video from 'react-native-video';
class VideoPlayer extends Component {
constructor(props) {
super(props);
this.onLoad = this.onLoad.bind(this);
this.onProgress = this.onProgress.bind(this);
}
state = {
rate: 1,
@ -28,42 +23,57 @@ class VideoPlayer extends Component {
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: true,
};
onLoad(data) {
this.setState({duration: data.duration});
}
video: Video;
onProgress(data) {
this.setState({currentTime: data.currentTime});
}
onLoad = (data) => {
this.setState({ duration: data.duration });
};
onProgress = (data) => {
this.setState({ currentTime: data.currentTime });
};
onEnd = () => {
this.setState({ paused: true })
this.video.seek(0)
};
onAudioBecomingNoisy = () => {
this.setState({ paused: true })
};
onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => {
this.setState({ paused: !event.hasAudioFocus })
};
getCurrentTimePercentage() {
if (this.state.currentTime > 0) {
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
} else {
return 0;
}
}
return 0;
};
renderRateControl(rate) {
const isSelected = (this.state.rate == rate);
const isSelected = (this.state.rate === rate);
return (
<TouchableOpacity onPress={() => { this.setState({rate: rate}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
<TouchableOpacity onPress={() => { this.setState({ rate }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{rate}x
</Text>
</TouchableOpacity>
)
);
}
renderResizeModeControl(resizeMode) {
const isSelected = (this.state.resizeMode == resizeMode);
const isSelected = (this.state.resizeMode === resizeMode);
return (
<TouchableOpacity onPress={() => { this.setState({resizeMode: resizeMode}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
<TouchableOpacity onPress={() => { this.setState({ resizeMode }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{resizeMode}
</Text>
</TouchableOpacity>
@ -71,11 +81,11 @@ class VideoPlayer extends Component {
}
renderVolumeControl(volume) {
const isSelected = (this.state.volume == volume);
const isSelected = (this.state.volume === volume);
return (
<TouchableOpacity onPress={() => { this.setState({volume: volume}) }}>
<Text style={[styles.controlOption, {fontWeight: isSelected ? "bold" : "normal"}]}>
<TouchableOpacity onPress={() => { this.setState({ volume }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{volume * 100}%
</Text>
</TouchableOpacity>
@ -88,18 +98,29 @@ class VideoPlayer extends Component {
return (
<View style={styles.container}>
<TouchableOpacity style={styles.fullScreen} onPress={() => {this.setState({paused: !this.state.paused})}}>
<Video source={{uri: "broadchurch"}}
style={styles.fullScreen}
rate={this.state.rate}
paused={this.state.paused}
volume={this.state.volume}
muted={this.state.muted}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={() => { console.log('Done!') }}
repeat={true} />
<TouchableOpacity
style={styles.fullScreen}
onPress={() => this.setState({ paused: !this.state.paused })}
>
<Video
ref={(ref: Video) => { this.video = ref }}
/* For ExoPlayer */
/* source={require('./broadchurch.mp4')} */
/* source={{ uri: 'http://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0', type: 'mpd' }} */
source={{ uri: 'broadchurch', type: 'mp4' }}
style={styles.fullScreen}
rate={this.state.rate}
paused={this.state.paused}
volume={this.state.volume}
muted={this.state.muted}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
onAudioBecomingNoisy={this.onAudioBecomingNoisy}
onAudioFocusChanged={this.onAudioFocusChanged}
repeat={false}
/>
</TouchableOpacity>
<View style={styles.controls}>
@ -127,8 +148,8 @@ class VideoPlayer extends Component {
<View style={styles.trackingControls}>
<View style={styles.progress}>
<View style={[styles.innerProgressCompleted, {flex: flexCompleted}]} />
<View style={[styles.innerProgressRemaining, {flex: flexRemaining}]} />
<View style={[styles.innerProgressCompleted, { flex: flexCompleted }]} />
<View style={[styles.innerProgressRemaining, { flex: flexRemaining }]} />
</View>
</View>
</View>
@ -153,7 +174,7 @@ const styles = StyleSheet.create({
right: 0,
},
controls: {
backgroundColor: "transparent",
backgroundColor: 'transparent',
borderRadius: 5,
position: 'absolute',
bottom: 20,
@ -200,7 +221,7 @@ const styles = StyleSheet.create({
controlOption: {
alignSelf: 'center',
fontSize: 11,
color: "white",
color: 'white',
paddingLeft: 2,
paddingRight: 2,
lineHeight: 12,

View File

@ -19,6 +19,7 @@ class VideoPlayer extends Component {
super(props);
this.onLoad = this.onLoad.bind(this);
this.onProgress = this.onProgress.bind(this);
this.onBuffer = this.onBuffer.bind(this);
}
state = {
rate: 1,
@ -29,7 +30,8 @@ class VideoPlayer extends Component {
currentTime: 0.0,
controls: false,
paused: true,
skin: 'custom'
skin: 'custom',
isBuffering: false,
};
onLoad(data) {
@ -41,6 +43,10 @@ class VideoPlayer extends Component {
this.setState({currentTime: data.currentTime});
}
onBuffer({ isBuffering }: { isBuffering: boolean }) {
this.setState({ isBuffering });
}
getCurrentTimePercentage() {
if (this.state.currentTime > 0) {
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
@ -116,6 +122,7 @@ class VideoPlayer extends Component {
muted={this.state.muted}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={() => { AlertIOS.alert('Done!') }}
repeat={true}
@ -175,6 +182,7 @@ class VideoPlayer extends Component {
muted={this.state.muted}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onBuffer={this.onBuffer}
onProgress={this.onProgress}
onEnd={() => { AlertIOS.alert('Done!') }}
repeat={true}

View File

@ -802,6 +802,7 @@
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export NODE_BINARY=node\n../node_modules/react-native/packager/react-native-xcode.sh";
showEnvVarsInLog = 1;
};
/* End PBXShellScriptBuildPhase section */
@ -881,8 +882,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = NO;
INFOPLIST_FILE = VideoPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -898,8 +901,10 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = VideoPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -979,6 +984,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;

View File

@ -6,12 +6,14 @@
"start": "node_modules/react-native/packager/packager.sh"
},
"dependencies": {
"react": "15.4.2",
"react-native": "^0.40.0",
"react-native-video": "file:../",
"react-native-windows": "^0.33.4"
"react-native-windows": "~0.38.0"
},
"devDependencies": {
"rnpm-plugin-windows": "^0.2.3"
"rnpm-plugin-windows": "~0.2.3"
}
}

View File

@ -11,6 +11,7 @@
@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoad;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoBuffer;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoError;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoProgress;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoSeek;

View File

@ -366,12 +366,14 @@ static NSString *const playbackRate = @"rate";
}
} else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) {
_playerBufferEmpty = YES;
self.onVideoBuffer(@{@"isBuffering": @(YES), @"target": self.reactTag});
} else if ([keyPath isEqualToString:playbackLikelyToKeepUpKeyPath]) {
// Continue playing (or not if paused) after being paused due to hitting an unbuffered zone.
if ((!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && _playerItem.playbackLikelyToKeepUp) {
[self setPaused:_paused];
}
_playerBufferEmpty = NO;
self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag});
}
} else if (object == _playerLayer) {
if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) {

View File

@ -36,6 +36,7 @@ RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoBuffer, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTBubblingEventBlock);