package com.brentvatne.exoplayer; import android.annotation.SuppressLint; import android.app.Activity; 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.view.View; import android.view.Window; import android.widget.FrameLayout; import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.BecomingNoisyListener; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; 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.PlaybackParameters; import com.google.android.exoplayer2.Player; 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.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; 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.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; 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.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.lang.Math; import java.lang.Object; import java.util.ArrayList; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements LifecycleEventListener, ExoPlayer.EventListener, BecomingNoisyListener, AudioManager.OnAudioFocusChangeListener, MetadataRenderer.Output { 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 ExoPlayerView exoPlayerView; private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; private MappingTrackSelector trackSelector; private boolean playerNeedsSource; private int resumeWindow; private long resumePosition; private boolean loadVideoStarted; private boolean isFullscreen; private boolean isInBackground; private boolean isPaused = true; private boolean isBuffering; private float rate = 1f; // Props from React private Uri srcUri; private String extension; private boolean repeat; private String textTrackType; private Dynamic textTrackValue; private ReadableArray textTracks; private boolean disableFocus; private float mProgressUpdateInterval = 250.0f; private boolean playInBackground = false; private boolean useTextureView = false; // \ 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(); long bufferedDuration = player.getBufferedPercentage() * player.getDuration(); eventEmitter.progressChanged(pos, bufferedDuration, player.getDuration()); msg = obtainMessage(SHOW_PROGRESS); sendMessageDelayed(msg, Math.round(mProgressUpdateInterval)); } break; } } }; public ReactExoplayerView(ThemedReactContext context) { super(context); this.themedReactContext = context; createViews(); this.eventEmitter = new VideoEventEmitter(context); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); initializePlayer(); } @Override public void setId(int id) { super.setId(id); eventEmitter.setViewId(id); } private void createViews() { clearResumePosition(); mediaDataSourceFactory = buildDataSourceFactory(true); mainHandler = new Handler(); 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() { if (!playInBackground || !isInBackground) { setPlayWhenReady(!isPaused); } isInBackground = false; } @Override public void onHostPause() { isInBackground = true; if (playInBackground) { return; } setPlayWhenReady(false); } @Override public void onHostDestroy() { stopPlayback(); } public void cleanUpResources() { stopPlayback(); } // Internal methods private void initializePlayer() { if (player == null) { TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, new DefaultLoadControl()); player.addListener(this); player.setMetadataOutput(this); exoPlayerView.setPlayer(player); audioBecomingNoisyReceiver.setListener(this); setPlayWhenReady(!isPaused); playerNeedsSource = true; PlaybackParameters params = new PlaybackParameters(rate, 1f); player.setPlaybackParameters(params); } if (playerNeedsSource && srcUri != null) { ArrayList mediaSourceList = buildTextSources(); MediaSource videoSource = buildMediaSource(srcUri, extension); MediaSource mediaSource; if (mediaSourceList.size() == 0) { mediaSource = videoSource; } else { mediaSourceList.add(0, videoSource); MediaSource[] textSourceArray = mediaSourceList.toArray( new MediaSource[mediaSourceList.size()] ); mediaSource = new MergingMediaSource(textSourceArray); } boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; if (haveResumePosition) { player.seekTo(resumeWindow, resumePosition); } player.prepare(mediaSource, !haveResumePosition, false); 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 ArrayList buildTextSources() { ArrayList textSources = new ArrayList<>(); if (textTracks == null) { return textSources; } for (int i = 0; i < textTracks.size(); ++i) { ReadableMap textTrack = textTracks.getMap(i); String language = textTrack.getString("language"); String title = textTrack.hasKey("title") ? textTrack.getString("title") : language + " " + i; Uri uri = Uri.parse(textTrack.getString("uri")); MediaSource textSource = buildTextSource(title, uri, textTrack.getString("type"), language); if (textSource != null) { textSources.add(textSource); } } return textSources; } private MediaSource buildTextSource(String title, Uri uri, String mimeType, String language) { Format textFormat = Format.createTextSampleFormat(title, mimeType, Format.NO_VALUE, language); return new SingleSampleMediaSource(uri, mediaDataSourceFactory, textFormat, C.TIME_UNSET); } private void releasePlayer() { if (player != null) { isPaused = player.getPlayWhenReady(); updateResumePosition(); player.release(); player.setMetadataOutput(null); 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() { if (isFullscreen) { setFullscreen(false); } setKeepScreenOn(false); audioManager.abandonAudioFocus(this); } private void updateResumePosition() { resumeWindow = player.getCurrentWindowIndex(); resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition()) : C.TIME_UNSET; } private void clearResumePosition() { resumeWindow = C.INDEX_UNSET; resumePosition = C.TIME_UNSET; } /** * 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(this.themedReactContext, 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; setSelectedTextTrack(textTrackType, textTrackValue); 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, getTextTrackInfo()); } } private WritableArray getTextTrackInfo() { WritableArray textTracks = Arguments.createArray(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); int index = getTextTrackRendererIndex(); if (info == null || index == C.INDEX_UNSET) { return textTracks; } TrackGroupArray groups = info.getTrackGroups(index); for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); WritableMap textTrack = Arguments.createMap(); textTrack.putInt("index", i); textTrack.putString("title", format.id != null ? format.id : ""); textTrack.putString("type", format.sampleMimeType); textTrack.putString("language", format.language != null ? format.language : ""); textTracks.pushMap(textTrack); } return textTracks; } private void onBuffering(boolean buffering) { if (isBuffering == buffering) { return; } isBuffering = buffering; if (buffering) { eventEmitter.buffering(true); } else { eventEmitter.buffering(false); } } @Override public void onPositionDiscontinuity(int reason) { if (playerNeedsSource) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to // which they seeked. updateResumePosition(); } // When repeat is turned on, reaching the end of the video will not cause a state change // so we need to explicitly detect it. if (reason == ExoPlayer.DISCONTINUITY_REASON_PERIOD_TRANSITION && player.getRepeatMode() == Player.REPEAT_MODE_ONE) { eventEmitter.end(); } } @Override public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { // Do nothing. } @Override public void onSeekProcessed() { // Do nothing. } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // Do nothing. } @Override public void onRepeatModeChanged(int repeatMode) { // Do nothing. } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { // Do Nothing. } @Override public void onPlaybackParametersChanged(PlaybackParameters params) { eventEmitter.playbackRateChange(params.speed); } @Override public void onPlayerError(ExoPlaybackException e) { String errorString = null; Exception ex = e; 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); } } } else if (e.type == ExoPlaybackException.TYPE_SOURCE) { ex = e.getSourceException(); errorString = getResources().getString(R.string.unrecognized_media_format); } if (errorString != null) { eventEmitter.error(errorString, ex); } playerNeedsSource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); initializePlayer(); } else { updateResumePosition(); } } private static boolean isBehindLiveWindow(ExoPlaybackException e) { if (e.type != ExoPlaybackException.TYPE_SOURCE) { return false; } Throwable cause = e.getSourceException(); while (cause != null) { if (cause instanceof BehindLiveWindowException) { return true; } cause = cause.getCause(); } return false; } public int getTextTrackRendererIndex() { int rendererCount = player.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { if (player.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT) { return rendererIndex; } } return C.INDEX_UNSET; } @Override public void onMetadata(Metadata metadata) { eventEmitter.timedMetadata(metadata); } // ReactExoplayerViewManager public api public void setSrc(final Uri uri, final String extension) { if (uri != null) { boolean isOriginalSourceNull = srcUri == null; boolean isSourceEqual = uri.equals(srcUri); this.srcUri = uri; this.extension = extension; this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, BANDWIDTH_METER); if (!isOriginalSourceNull && !isSourceEqual) { reloadSource(); } } } public void setProgressUpdateInterval(final float progressUpdateInterval) { mProgressUpdateInterval = progressUpdateInterval; } public void setRawSrc(final Uri uri, final String extension) { if (uri != null) { boolean isOriginalSourceNull = srcUri == null; boolean isSourceEqual = uri.equals(srcUri); this.srcUri = uri; this.extension = extension; this.mediaDataSourceFactory = DataSourceUtil.getRawDataSourceFactory(this.themedReactContext); if (!isOriginalSourceNull && !isSourceEqual) { reloadSource(); } } } public void setTextTracks(ReadableArray textTracks) { this.textTracks = textTracks; reloadSource(); } private void reloadSource() { playerNeedsSource = true; initializePlayer(); } public void setResizeModeModifier(@ResizeMode.Mode int resizeMode) { exoPlayerView.setResizeMode(resizeMode); } public void setRepeatModifier(boolean repeat) { if (player != null) { if (repeat) { player.setRepeatMode(Player.REPEAT_MODE_ONE); } else { player.setRepeatMode(Player.REPEAT_MODE_OFF); } } this.repeat = repeat; } public void setSelectedTextTrack(String type, Dynamic value) { textTrackType = type; textTrackValue = value; int index = getTextTrackRendererIndex(); if (index == C.INDEX_UNSET) { return; } MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); if (info == null) { return; } TrackGroupArray groups = info.getTrackGroups(index); int trackIndex = C.INDEX_UNSET; if (TextUtils.isEmpty(type)) { // Do nothing } else if (type.equals("disabled")) { trackSelector.setSelectionOverride(index, groups, null); return; } else if (type.equals("language")) { for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); if (format.language != null && format.language.equals(value.asString())) { trackIndex = i; break; } } } else if (type.equals("title")) { for (int i = 0; i < groups.length; ++i) { Format format = groups.get(i).getFormat(0); if (format.id != null && format.id.equals(value.asString())) { trackIndex = i; break; } } } else if (type.equals("index")) { trackIndex = value.asInt(); } else { // default. invalid type or "system" trackSelector.clearSelectionOverrides(index); return; } if (trackIndex == C.INDEX_UNSET) { trackSelector.clearSelectionOverrides(trackIndex); return; } MappingTrackSelector.SelectionOverride override = new MappingTrackSelector.SelectionOverride( new FixedTrackSelection.Factory(), trackIndex, 0); trackSelector.setSelectionOverride(index, groups, override); } 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 newRate) { rate = newRate; if (player != null) { PlaybackParameters params = new PlaybackParameters(rate, 1f); player.setPlaybackParameters(params); } } public void setPlayInBackground(boolean playInBackground) { this.playInBackground = playInBackground; } public void setDisableFocus(boolean disableFocus) { this.disableFocus = disableFocus; } public void setFullscreen(boolean fullscreen) { if (fullscreen == isFullscreen) { return; // Avoid generating events when nothing is changing } isFullscreen = fullscreen; Activity activity = themedReactContext.getCurrentActivity(); if (activity == null) { return; } Window window = activity.getWindow(); View decorView = window.getDecorView(); int uiOptions; if (isFullscreen) { if (Util.SDK_INT >= 19) { // 4.4+ uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION | SYSTEM_UI_FLAG_IMMERSIVE_STICKY | SYSTEM_UI_FLAG_FULLSCREEN; } else { uiOptions = SYSTEM_UI_FLAG_HIDE_NAVIGATION | SYSTEM_UI_FLAG_FULLSCREEN; } eventEmitter.fullscreenWillPresent(); decorView.setSystemUiVisibility(uiOptions); eventEmitter.fullscreenDidPresent(); } else { uiOptions = View.SYSTEM_UI_FLAG_VISIBLE; eventEmitter.fullscreenWillDismiss(); decorView.setSystemUiVisibility(uiOptions); eventEmitter.fullscreenDidDismiss(); } } public void setUseTextureView(boolean useTextureView) { exoPlayerView.setUseTextureView(useTextureView); } }