Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ExoPlayer #426

Merged
merged 2 commits into from
Jan 11, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
28 changes: 27 additions & 1 deletion Video.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) || {};
Expand Down Expand Up @@ -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,
},
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions android-exoplayer/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions android-exoplayer/build.gradle
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions android-exoplayer/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.brentvatne.react">
</manifest>
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading