ExoPlayer (#426)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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