ExoPlayer (#426)
This commit is contained in:
committed by
Matt Apperson
parent
cd53e389a0
commit
9a936c9e8f
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>
|
Reference in New Issue
Block a user