ExoPlayer (#426)
This commit is contained in:
parent
cd53e389a0
commit
9a936c9e8f
@ -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
|
||||
|
28
Video.js
28
Video.js
@ -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,
|
||||
|
47
android-exoplayer/README.md
Normal file
47
android-exoplayer/README.md
Normal 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 Android’s MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android’s 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 }}`
|
20
android-exoplayer/build.gradle
Normal file
20
android-exoplayer/build.gradle
Normal 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'
|
||||
}
|
3
android-exoplayer/src/main/AndroidManifest.xml
Normal file
3
android-exoplayer/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.brentvatne.react">
|
||||
</manifest>
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.brentvatne.receiver;
|
||||
|
||||
public interface BecomingNoisyListener {
|
||||
|
||||
BecomingNoisyListener NO_OP = new BecomingNoisyListener() {
|
||||
@Override public void onAudioBecomingNoisy() {
|
||||
// NO_OP
|
||||
}
|
||||
};
|
||||
|
||||
void onAudioBecomingNoisy();
|
||||
|
||||
}
|
12
android-exoplayer/src/main/res/values/strings.xml
Normal file
12
android-exoplayer/src/main/res/values/strings.xml
Normal 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>
|
@ -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'
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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]) {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user