From 11735bed886724b761d9726931e2e5f93995db50 Mon Sep 17 00:00:00 2001 From: chinloong Date: Tue, 11 Jul 2017 16:18:26 +0800 Subject: [PATCH 001/194] Fix nil string parameter error --- Video.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Video.js b/Video.js index 4d69b333..b7c6b89d 100644 --- a/Video.js +++ b/Video.js @@ -162,6 +162,8 @@ export default class Video extends Component { let uri = source.uri || ''; if (uri && uri.match(/^\//)) { uri = `file://${uri}`; + } else if (uri === '') { + return null; } const isNetwork = !!(uri && uri.match(/^https?:/)); From 94bceb472bce35b08e04b2e93bb0c11096ce30fe Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Sat, 4 Jul 2020 17:41:15 +0700 Subject: [PATCH 002/194] add fullscreen activity --- .../ExoPlayerFullscreenVideoActivity.java | 107 ++++++++++++++++++ .../brentvatne/exoplayer/ExoPlayerView.java | 3 - .../exoplayer/ReactExoplayerView.java | 91 ++++++++++----- .../res/layout/exo_player_control_view.xml | 19 +++- .../layout/exo_player_fullscreen_video.xml | 18 +++ android/src/main/AndroidManifest.xml | 8 +- 6 files changed, 211 insertions(+), 35 deletions(-) create mode 100644 android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java create mode 100644 android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java new file mode 100644 index 00000000..1f2ee1d5 --- /dev/null +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -0,0 +1,107 @@ +package com.brentvatne.exoplayer; + +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.brentvatne.react.R; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ui.PlayerControlView; + +public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implements ReactExoplayerView.FullScreenDelegate { + public static final String EXTRA_ID = "extra_id"; + public static final String EXTRA_IS_PLAYING = "extra_is_playing"; + + private int id; + private PlayerControlView playerControlView; + private SimpleExoPlayer player; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.exo_player_fullscreen_video); + id = getIntent().getIntExtra(EXTRA_ID, -1); + player = ReactExoplayerView.getViewInstance(id).getPlayer(); + + ExoPlayerView playerView = findViewById(R.id.player_view); + playerView.setPlayer(player); + playerView.setOnClickListener(v -> togglePlayerControlVisibility()); + + playerControlView = findViewById(R.id.player_controls); + playerControlView.setPlayer(player); + // Set the fullscreen button to "close fullscreen" icon + ImageView fullscreenIcon = playerControlView.findViewById(R.id.exo_fullscreen_icon); + fullscreenIcon.setImageResource(R.drawable.exo_controls_fullscreen_exit); + playerControlView.findViewById(R.id.exo_fullscreen_button) + .setOnClickListener(v -> finish()); + } + + @Override + public void onResume() { + super.onResume(); + boolean isPlaying = getIntent().getBooleanExtra(EXTRA_IS_PLAYING, false); + player.setPlayWhenReady(isPlaying); + ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(this); + } + + @Override + public void onPause() { + super.onPause(); + boolean isPlaying = player.getPlayWhenReady() && player.getPlaybackState() == Player.STATE_READY; + ReactExoplayerView.getViewInstance(id).setPausedModifier(!isPlaying); + player.setPlayWhenReady(false); + ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(null); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + hideSystemUI(); + } + } + + private void togglePlayerControlVisibility() { + if (playerControlView.isVisible()) { + playerControlView.hide(); + } else { + playerControlView.show(); + } + } + + /** + * Enables regular immersive mode. + */ + private void hideSystemUI() { + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE + // Set the content to appear under the system bars so that the + // content doesn't resize when the system bars hide and show. + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + // Hide the nav bar and status bar + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN); + } + + /** + * Shows the system bars by removing all the flags + * except for the ones that make the content appear under the system bars. + */ + private void showSystemUI() { + View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + @Override + public void closeFullScreen() { + finish(); + } +} diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index afa5106c..1a244f29 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -120,9 +120,6 @@ public final class ExoPlayerView extends FrameLayout { * @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); diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 4e6fea58..6cd6b844 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1,8 +1,8 @@ package com.brentvatne.exoplayer; import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.media.AudioManager; import android.net.Uri; import android.os.Handler; @@ -10,10 +10,12 @@ import android.os.Message; import android.text.TextUtils; import android.util.Log; import android.view.View; -import android.view.Window; import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; -import android.widget.ImageButton; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; @@ -25,28 +27,33 @@ 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.modules.timepicker.TimePickerDialogFragment; +import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIBlock; +import com.facebook.react.uimanager.UIManagerModule; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; -import com.google.android.exoplayer2.DefaultRenderersFactory; 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.MetadataOutput; +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.ProgressiveMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; -import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; @@ -57,17 +64,20 @@ 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.ui.PlayerControlView; -import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.ui.PlayerControlView; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.lang.Math; +import java.util.HashMap; +import java.util.Map; +import java.lang.Object; import java.util.ArrayList; import java.util.Locale; import java.util.Map; @@ -91,6 +101,11 @@ class ReactExoplayerView extends FrameLayout implements DEFAULT_COOKIE_MANAGER.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } + private static Map instances = new HashMap<>(); + private static int UNIQUE_ID = 0; + private int uid = ++UNIQUE_ID; + private FullScreenDelegate fullScreenDelegate; + private final VideoEventEmitter eventEmitter; private final ReactExoplayerConfig config; private final DefaultBandwidthMeter bandwidthMeter; @@ -187,7 +202,6 @@ class ReactExoplayerView extends FrameLayout implements createViews(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); initializePlayer(); @@ -235,6 +249,7 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onHostResume() { + exoPlayerView.setPlayer(player); if (!playInBackground || !isInBackground) { setPlayWhenReady(!isPaused); } @@ -275,6 +290,22 @@ class ReactExoplayerView extends FrameLayout implements } } + public static ReactExoplayerView getViewInstance(Integer uid) { + return instances.get(uid); + } + + public SimpleExoPlayer getPlayer() { + return player; + } + + public boolean isPaused() { + return isPaused; + } + + public void registerFullScreenDelegate(FullScreenDelegate delegate) { + this.fullScreenDelegate = delegate; + } + // Internal methods /** @@ -290,6 +321,15 @@ class ReactExoplayerView extends FrameLayout implements } } + private void showFullscreen() { + instances.put(uid, this); + Intent intent = new Intent(getContext(), ExoPlayerFullscreenVideoActivity.class); + intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ID, this.uid); + boolean isPlaying = player.getPlayWhenReady() && player.getPlaybackState() == Player.STATE_READY; + intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_IS_PLAYING, isPlaying); + getContext().startActivity(intent); + } + /** * Initializing Player control */ @@ -302,6 +342,7 @@ class ReactExoplayerView extends FrameLayout implements playerControlView.setPlayer(player); playerControlView.show(); playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container); + playerControlView.findViewById(R.id.exo_fullscreen_button).setOnClickListener(v -> showFullscreen()); // Invoking onClick event for exoplayerView exoPlayerView.setOnClickListener(new OnClickListener() { @@ -374,6 +415,7 @@ class ReactExoplayerView extends FrameLayout implements } private void initializePlayer() { + themedReactContext.addLifecycleEventListener(this); ReactExoplayerView self = this; // This ensures all props have been settled, to avoid async racing conditions. new Handler().postDelayed(new Runnable() { @@ -1217,30 +1259,15 @@ class ReactExoplayerView extends FrameLayout implements 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); + showFullscreen(); eventEmitter.fullscreenDidPresent(); } else { - uiOptions = View.SYSTEM_UI_FLAG_VISIBLE; eventEmitter.fullscreenWillDismiss(); - decorView.setSystemUiVisibility(uiOptions); + if (fullScreenDelegate != null) { + fullScreenDelegate.closeFullScreen(); + } eventEmitter.fullscreenDidDismiss(); } } @@ -1279,4 +1306,8 @@ class ReactExoplayerView extends FrameLayout implements } } } + + public interface FullScreenDelegate { + void closeFullScreen(); + } } diff --git a/android-exoplayer/src/main/res/layout/exo_player_control_view.xml b/android-exoplayer/src/main/res/layout/exo_player_control_view.xml index becee6a9..51cbf3a8 100644 --- a/android-exoplayer/src/main/res/layout/exo_player_control_view.xml +++ b/android-exoplayer/src/main/res/layout/exo_player_control_view.xml @@ -1,5 +1,6 @@ - + + + + + + diff --git a/android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml b/android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml new file mode 100644 index 00000000..671a00ee --- /dev/null +++ b/android-exoplayer/src/main/res/layout/exo_player_fullscreen_video.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 3535ad44..2d632b3d 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,9 @@ + package="com.brentvatne.react"> + + + + From 32880544e5aa7424067feefa6f947b3e0e5b6688 Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Sat, 4 Jul 2020 22:08:57 +0700 Subject: [PATCH 003/194] update manifest and import --- .../src/main/AndroidManifest.xml | 5 ++++ .../exoplayer/ReactExoplayerView.java | 28 ++++++------------- android/src/main/AndroidManifest.xml | 6 ---- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/android-exoplayer/src/main/AndroidManifest.xml b/android-exoplayer/src/main/AndroidManifest.xml index 3535ad44..39515895 100644 --- a/android-exoplayer/src/main/AndroidManifest.xml +++ b/android-exoplayer/src/main/AndroidManifest.xml @@ -1,3 +1,8 @@ + + + diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 6cd6b844..5d546877 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -12,10 +12,7 @@ import android.util.Log; import android.view.View; import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; - -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentTransaction; +import android.widget.ImageButton; import com.brentvatne.react.R; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; @@ -27,33 +24,28 @@ 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.modules.timepicker.TimePickerDialogFragment; -import com.facebook.react.uimanager.NativeViewHierarchyManager; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIBlock; -import com.facebook.react.uimanager.UIManagerModule; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.DefaultRenderersFactory; 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.metadata.MetadataOutput; 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.ProgressiveMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroup; +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; @@ -64,21 +56,19 @@ 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.ui.PlayerControlView; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; -import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.ui.PlayerControlView; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.lang.Math; -import java.util.HashMap; -import java.util.Map; -import java.lang.Object; import java.util.ArrayList; +import java.util.HashMap; import java.util.Locale; import java.util.Map; diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 2d632b3d..42a828cc 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,9 +1,3 @@ - - - - From 0b7ea71d7707516c8c108f021f70477e962c7bcb Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Sun, 5 Jul 2020 11:00:25 +0700 Subject: [PATCH 004/194] update fullscreen activity --- .../src/main/AndroidManifest.xml | 3 +- .../ExoPlayerFullscreenVideoActivity.java | 35 +++++++++++++++---- .../exoplayer/ReactExoplayerView.java | 16 ++++++--- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/android-exoplayer/src/main/AndroidManifest.xml b/android-exoplayer/src/main/AndroidManifest.xml index 39515895..53e507e0 100644 --- a/android-exoplayer/src/main/AndroidManifest.xml +++ b/android-exoplayer/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + android:configChanges="orientation|keyboardHidden|screenSize" + android:launchMode="singleTop" /> diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java index 1f2ee1d5..5492145f 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -1,6 +1,7 @@ package com.brentvatne.exoplayer; import android.os.Bundle; +import android.view.KeyEvent; import android.view.View; import android.widget.ImageView; @@ -13,7 +14,6 @@ import com.google.android.exoplayer2.ui.PlayerControlView; public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implements ReactExoplayerView.FullScreenDelegate { public static final String EXTRA_ID = "extra_id"; - public static final String EXTRA_IS_PLAYING = "extra_is_playing"; private int id; private PlayerControlView playerControlView; @@ -36,26 +36,40 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen ImageView fullscreenIcon = playerControlView.findViewById(R.id.exo_fullscreen_icon); fullscreenIcon.setImageResource(R.drawable.exo_controls_fullscreen_exit); playerControlView.findViewById(R.id.exo_fullscreen_button) - .setOnClickListener(v -> finish()); + .setOnClickListener(v -> ReactExoplayerView.getViewInstance(id).setFullscreen(false)); + //Handling the playButton click event + playerControlView.findViewById(R.id.exo_play).setOnClickListener(v -> { + if (player != null && player.getPlaybackState() == Player.STATE_ENDED) { + player.seekTo(0); + } + ReactExoplayerView.getViewInstance(id).setPausedModifier(false); + }); + + //Handling the pauseButton click event + playerControlView.findViewById(R.id.exo_pause).setOnClickListener(v -> ReactExoplayerView.getViewInstance(id).setPausedModifier(true)); } @Override public void onResume() { super.onResume(); - boolean isPlaying = getIntent().getBooleanExtra(EXTRA_IS_PLAYING, false); - player.setPlayWhenReady(isPlaying); + boolean isPaused = ReactExoplayerView.getViewInstance(id).isPaused(); + player.setPlayWhenReady(!isPaused); ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(this); } @Override public void onPause() { super.onPause(); - boolean isPlaying = player.getPlayWhenReady() && player.getPlaybackState() == Player.STATE_READY; - ReactExoplayerView.getViewInstance(id).setPausedModifier(!isPlaying); player.setPlayWhenReady(false); ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(null); } + @Override + protected void onDestroy() { + super.onDestroy(); + ReactExoplayerView.getViewInstance(id).removeViewInstance(); + } + @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); @@ -64,6 +78,15 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen } } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_BACK)) { + ReactExoplayerView.getViewInstance(id).setFullscreen(false); + return false; + } + return super.onKeyDown(keyCode, event); + } + private void togglePlayerControlVisibility() { if (playerControlView.isVisible()) { playerControlView.hide(); diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 5d546877..38bcd827 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -239,9 +239,13 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onHostResume() { - exoPlayerView.setPlayer(player); if (!playInBackground || !isInBackground) { - setPlayWhenReady(!isPaused); + if (player != null) { + exoPlayerView.setPlayer(player); + boolean temp = this.disableFocus; + player.setPlayWhenReady(!isPaused); + this.disableFocus = temp; + } } isInBackground = false; } @@ -296,6 +300,10 @@ class ReactExoplayerView extends FrameLayout implements this.fullScreenDelegate = delegate; } + public void removeViewInstance() { + instances.remove(uid); + } + // Internal methods /** @@ -315,8 +323,6 @@ class ReactExoplayerView extends FrameLayout implements instances.put(uid, this); Intent intent = new Intent(getContext(), ExoPlayerFullscreenVideoActivity.class); intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ID, this.uid); - boolean isPlaying = player.getPlayWhenReady() && player.getPlaybackState() == Player.STATE_READY; - intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_IS_PLAYING, isPlaying); getContext().startActivity(intent); } @@ -332,7 +338,7 @@ class ReactExoplayerView extends FrameLayout implements playerControlView.setPlayer(player); playerControlView.show(); playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container); - playerControlView.findViewById(R.id.exo_fullscreen_button).setOnClickListener(v -> showFullscreen()); + playerControlView.findViewById(R.id.exo_fullscreen_button).setOnClickListener(v -> setFullscreen(true)); // Invoking onClick event for exoplayerView exoPlayerView.setOnClickListener(new OnClickListener() { From 5fe76574bbf30b7ea95fa4308760c12b26377f5e Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Fri, 10 Jul 2020 10:45:41 +0700 Subject: [PATCH 005/194] add fullscreenOrientation --- README.md | 2 +- .../ExoPlayerFullscreenVideoActivity.java | 33 ++++++++++++------- .../exoplayer/ReactExoplayerView.java | 11 ++++--- .../exoplayer/ReactExoplayerViewManager.java | 6 ++++ 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 00fc7932..3e701aa7 100644 --- a/README.md +++ b/README.md @@ -469,7 +469,7 @@ Platforms: iOS * **landscape** * **portrait** -Platforms: iOS +Platforms: Android ExoPlayer, iOS #### headers Pass headers to the HTTP client. Can be used for authorization. Headers must be a part of the source object. diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java index 5492145f..6996eb6c 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -1,5 +1,6 @@ package com.brentvatne.exoplayer; +import android.content.pm.ActivityInfo; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; @@ -14,7 +15,8 @@ import com.google.android.exoplayer2.ui.PlayerControlView; public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implements ReactExoplayerView.FullScreenDelegate { public static final String EXTRA_ID = "extra_id"; - + public static final String EXTRA_ORIENTATION = "extra_orientation"; + private int id; private PlayerControlView playerControlView; private SimpleExoPlayer player; @@ -22,8 +24,14 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.exo_player_fullscreen_video); id = getIntent().getIntExtra(EXTRA_ID, -1); + String orientation = getIntent().getStringExtra(EXTRA_ORIENTATION); + if ("landscape".equals(orientation)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } else if ("portrait".equals(orientation)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); + } + setContentView(R.layout.exo_player_fullscreen_video); player = ReactExoplayerView.getViewInstance(id).getPlayer(); ExoPlayerView playerView = findViewById(R.id.player_view); @@ -54,20 +62,18 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen super.onResume(); boolean isPaused = ReactExoplayerView.getViewInstance(id).isPaused(); player.setPlayWhenReady(!isPaused); - ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(this); + if (ReactExoplayerView.getViewInstance(id) != null) { + ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(this); + } } @Override public void onPause() { super.onPause(); player.setPlayWhenReady(false); - ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(null); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - ReactExoplayerView.getViewInstance(id).removeViewInstance(); + if (ReactExoplayerView.getViewInstance(id) != null) { + ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(null); + } } @Override @@ -81,8 +87,11 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK)) { - ReactExoplayerView.getViewInstance(id).setFullscreen(false); - return false; + if (ReactExoplayerView.getViewInstance(id) != null) { + ReactExoplayerView.getViewInstance(id).setFullscreen(false); + return false; + } + return true; } return super.onKeyDown(keyCode, event); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 38bcd827..875d3402 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -114,6 +114,7 @@ class ReactExoplayerView extends FrameLayout implements private long resumePosition; private boolean loadVideoStarted; private boolean isFullscreen; + private String fullScreenOrientation; private boolean isInBackground; private boolean isPaused; private boolean isBuffering; @@ -266,6 +267,7 @@ class ReactExoplayerView extends FrameLayout implements public void cleanUpResources() { stopPlayback(); + instances.remove(uid); } //BandwidthMeter.EventListener implementation @@ -300,10 +302,6 @@ class ReactExoplayerView extends FrameLayout implements this.fullScreenDelegate = delegate; } - public void removeViewInstance() { - instances.remove(uid); - } - // Internal methods /** @@ -323,6 +321,7 @@ class ReactExoplayerView extends FrameLayout implements instances.put(uid, this); Intent intent = new Intent(getContext(), ExoPlayerFullscreenVideoActivity.class); intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ID, this.uid); + intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ORIENTATION, this.fullScreenOrientation); getContext().startActivity(intent); } @@ -1268,6 +1267,10 @@ class ReactExoplayerView extends FrameLayout implements } } + public void setFullscreenOrientation(String orientation) { + this.fullScreenOrientation = orientation; + } + public void setUseTextureView(boolean useTextureView) { exoPlayerView.setUseTextureView(useTextureView); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index cf50fdae..d520970d 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -54,6 +54,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Mon, 13 Jul 2020 14:41:38 +0700 Subject: [PATCH 006/194] do not hide fullscreen in stopPlayback --- .../ExoPlayerFullscreenVideoActivity.java | 3 +- .../exoplayer/ReactExoplayerView.java | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java index 6996eb6c..329a8fda 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -60,9 +60,8 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen @Override public void onResume() { super.onResume(); - boolean isPaused = ReactExoplayerView.getViewInstance(id).isPaused(); - player.setPlayWhenReady(!isPaused); if (ReactExoplayerView.getViewInstance(id) != null) { + ReactExoplayerView.getViewInstance(id).syncPlayerState(); ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(this); } } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 875d3402..83d70361 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -116,6 +116,7 @@ class ReactExoplayerView extends FrameLayout implements private boolean isFullscreen; private String fullScreenOrientation; private boolean isInBackground; + private boolean isInFullscreen; private boolean isPaused; private boolean isBuffering; private boolean muted = false; @@ -241,11 +242,14 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onHostResume() { if (!playInBackground || !isInBackground) { - if (player != null) { - exoPlayerView.setPlayer(player); - boolean temp = this.disableFocus; - player.setPlayWhenReady(!isPaused); - this.disableFocus = temp; + if (isInFullscreen) { + if (player != null) { + exoPlayerView.setPlayer(player); + syncPlayerState(); + } + isInFullscreen = false; + } else { + setPlayWhenReady(!isPaused); } } isInBackground = false; @@ -294,8 +298,15 @@ class ReactExoplayerView extends FrameLayout implements return player; } - public boolean isPaused() { - return isPaused; + public void syncPlayerState() { + if (player == null) return; + if (player.getPlaybackState() == Player.STATE_ENDED) { + // Try to get last frame displayed + player.seekTo(player.getDuration() - 200); + player.setPlayWhenReady(true); + } else { + player.setPlayWhenReady(!isPaused); + } } public void registerFullScreenDelegate(FullScreenDelegate delegate) { @@ -323,6 +334,7 @@ class ReactExoplayerView extends FrameLayout implements intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ID, this.uid); intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ORIENTATION, this.fullScreenOrientation); getContext().startActivity(intent); + isInFullscreen = true; } /** @@ -623,9 +635,6 @@ class ReactExoplayerView extends FrameLayout implements } private void onStopPlayback() { - if (isFullscreen) { - setFullscreen(false); - } audioManager.abandonAudioFocus(this); } From 4d35511fb98f5494fc915502f5fe7799237ab724 Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Mon, 13 Jul 2020 14:50:09 +0700 Subject: [PATCH 007/194] add delay when hideSystemUI --- .../brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java index 329a8fda..d3b87208 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -79,7 +79,7 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { - hideSystemUI(); + playerControlView.postDelayed(this::hideSystemUI, 200); } } From 5f1ec04cef478545507ab149411ee7cb331860ba Mon Sep 17 00:00:00 2001 From: Alec Winograd Date: Thu, 15 Oct 2020 11:17:49 -0500 Subject: [PATCH 008/194] Fix default behavior for captions in ExoPlayer --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index b41b2768..652178b1 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1134,7 +1134,7 @@ class ReactExoplayerView extends FrameLayout implements } } } - } else if (rendererIndex == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default + } else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default // Use system settings if possible CaptioningManager captioningManager = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE); From 42deedc8d1056ffc6a8d454e28ce449a400add3e Mon Sep 17 00:00:00 2001 From: Alec Winograd Date: Thu, 15 Oct 2020 11:24:46 -0500 Subject: [PATCH 009/194] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e2518c..9ca56b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Version 5.1.0-alpha9 - Add ARM64 support for windows [#2137](https://github.com/react-native-community/react-native-video/pull/2137) +- Fix default closed captioning behavior for Android ExoPlayer [#2181](https://github.com/react-native-video/react-native-video/pull/2181) ### Version 5.1.0-alpha8 From 991f94b22842c842d7e522904b23a0dfd995002a Mon Sep 17 00:00:00 2001 From: Mudaser Ali Date: Fri, 16 Oct 2020 18:54:50 +0500 Subject: [PATCH 010/194] Update README.md --- README.md | 91 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 1b6a913e..0a24a868 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,11 @@ First select your project in Xcode. -After that, select the tvOS target of your application and select « General » tab +After that, select the tvOS target of your application and select « General » tab -Scroll to « Linked Frameworks and Libraries » and tap on the + button +Scroll to « Linked Frameworks and Libraries » and tap on the + button @@ -273,48 +273,51 @@ var styles = StyleSheet.create({ ``` ### Configurable props -* [allowsExternalPlayback](#allowsexternalplayback) -* [audioOnly](#audioonly) -* [automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) -* [bufferConfig](#bufferconfig) -* [controls](#controls) -* [currentPlaybackTime](#currentPlaybackTime) -* [disableFocus](#disableFocus) -* [filter](#filter) -* [filterEnabled](#filterEnabled) -* [fullscreen](#fullscreen) -* [fullscreenAutorotate](#fullscreenautorotate) -* [fullscreenOrientation](#fullscreenorientation) -* [headers](#headers) -* [hideShutterView](#hideshutterview) -* [id](#id) -* [ignoreSilentSwitch](#ignoresilentswitch) -* [maxBitRate](#maxbitrate) -* [minLoadRetryCount](#minLoadRetryCount) -* [mixWithOthers](#mixWithOthers) -* [muted](#muted) -* [paused](#paused) -* [pictureInPicture](#pictureinpicture) -* [playInBackground](#playinbackground) -* [playWhenInactive](#playwheninactive) -* [poster](#poster) -* [posterResizeMode](#posterresizemode) -* [preferredForwardBufferDuration](#preferredForwardBufferDuration) -* [preventsDisplaySleepDuringVideoPlayback](#preventsDisplaySleepDuringVideoPlayback) -* [progressUpdateInterval](#progressupdateinterval) -* [rate](#rate) -* [repeat](#repeat) -* [reportBandwidth](#reportbandwidth) -* [resizeMode](#resizemode) -* [selectedAudioTrack](#selectedaudiotrack) -* [selectedTextTrack](#selectedtexttrack) -* [selectedVideoTrack](#selectedvideotrack) -* [source](#source) -* [stereoPan](#stereopan) -* [textTracks](#texttracks) -* [trackId](#trackId) -* [useTextureView](#usetextureview) -* [volume](#volume) +| Prop Name |Plateforms Support | +|--|--| +|[allowsExternalPlayback](#allowsexternalplayback) |iOS | +|[audioOnly](#audioonly)|All | +|[automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) | iOS| +|[bufferConfig](#bufferconfig)|Android ExoPlayer| +|[controls](#controls)|Android ExoPlayer, iOS, react-native-dom| +|[currentPlaybackTime](#currentPlaybackTime)|Android Exoplayer| +|[disableFocus](#disableFocus)|Android Exoplayer, iOS| +|[filter](#filter)|iOS| +|[filterEnabled](#filterEnabled)|iOS| +|[fullscreen](#fullscreen)|iOS| +|[fullscreenAutorotate](#fullscreenautorotate)|iOS| +|[fullscreenOrientation](#fullscreenorientation)|iOS| +|[headers](#headers)|Android ExoPlayer| +|[hideShutterView](#hideshutterview)|Android ExoPlayer| +|[id](#id)|react-native-dom| +|[ignoreSilentSwitch](#ignoresilentswitch)|iOS| +|[maxBitRate](#maxbitrate)|Android ExoPlayer, iOS| +|[minLoadRetryCount](#minLoadRetryCount)|Android ExoPlayer| +|[mixWithOthers](#mixWithOthers)|iOS| +|[muted](#muted)|All| +|[paused](#paused)|All| +|[pictureInPicture](#pictureinpicture)|iOS| +|[playInBackground](#playinbackground)|Android ExoPlayer, Android MediaPlayer, iOS| +|[playWhenInactive](#playwheninactive)|iOS| +|[poster](#poster)|All| +|[posterResizeMode](#posterresizemode)|All| +|[preferredForwardBufferDuration](#preferredForwardBufferDuration)|iOS| +| [preventsDisplaySleepDuringVideoPlayback](#preventsDisplaySleepDuringVideoPlayback)|iOS, Android| +|[progressUpdateInterval](#progressupdateinterval)|All| +|[rate](#rate)|All| +|[repeat](#repeat)|All| +|[reportBandwidth](#reportbandwidth)|Android ExoPlayer| +|[resizeMode](#resizemode)|Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP| +|[selectedAudioTrack](#selectedaudiotrack)|Android ExoPlayer, iOS| +|[selectedTextTrack](#selectedtexttrack)|Android ExoPlayer, iOS| +|[selectedVideoTrack](#selectedvideotrack)|Android ExoPlayer| +|[source](#source)|All| +|[stereoPan](#stereopan)|Android MediaPlayer| +|[textTracks](#texttracks)|Android ExoPlayer, iOS| +|[trackId](#trackId)|Android ExoPlayer| +|[useTextureView](#usetextureview)|Android ExoPlayer| +|[volume](#volume)|All| + ### Event props * [onAudioBecomingNoisy](#onaudiobecomingnoisy) From 2dea87c2207d0bb90e7a2ac859a8191778ab7e3f Mon Sep 17 00:00:00 2001 From: Mudaser Ali Date: Sat, 17 Oct 2020 08:54:44 +0500 Subject: [PATCH 011/194] Update Event props in README.md --- README.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0a24a868..bc845dee 100644 --- a/README.md +++ b/README.md @@ -320,23 +320,26 @@ var styles = StyleSheet.create({ ### Event props -* [onAudioBecomingNoisy](#onaudiobecomingnoisy) -* [onBandwidthUpdate](#onbandwidthupdate) -* [onEnd](#onend) -* [onExternalPlaybackChange](#onexternalplaybackchange) -* [onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent) -* [onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent) -* [onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss) -* [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) -* [onLoad](#onload) -* [onLoadStart](#onloadstart) -* [onReadyForDisplay](#onreadyfordisplay) -* [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) -* [onPlaybackRateChange](#onplaybackratechange) -* [onProgress](#onprogress) -* [onSeek](#onseek) -* [onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop) -* [onTimedMetadata](#ontimedmetadata) +| Prop Name |Plateforms Support | +|--|--| +|[onAudioBecomingNoisy](#onaudiobecomingnoisy)|Android ExoPlayer, iOS| +|[onBandwidthUpdate](#onbandwidthupdate)|Android ExoPlayer| +|[onEnd](#onend)|All| +|[onExternalPlaybackChange](#onexternalplaybackchange)|iOS| +|[onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent)|Android ExoPlayer, Android MediaPlayer, iOS| +|[onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent)|Android ExoPlayer, Android MediaPlayer, iOS| +|[onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss)|Android ExoPlayer, Android MediaPlayer, iOS| +|[onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)|Android ExoPlayer, Android MediaPlayer, iOS| +|[onLoad](#onload)|All| +|[onLoadStart](#onloadstart)|All| +|[onReadyForDisplay](#onreadyfordisplay)|Android ExoPlayer, Android MediaPlayer, iOS, Web| +|[onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged)|iOS| +|[onPlaybackRateChange](#onplaybackratechange)|All| +|[onProgress](#onprogress)|All| +|[onSeek](#onseek)|Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP| +|[onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop)|iOS| +|[onTimedMetadata](#ontimedmetadata)|Android ExoPlayer, Android MediaPlayer, iOS| + ### Methods * [dismissFullscreenPlayer](#dismissfullscreenplayer) From 38511bbf1c213a2defd8b1f3e5a4991183d572e3 Mon Sep 17 00:00:00 2001 From: Mudaser Ali Date: Sat, 17 Oct 2020 08:58:05 +0500 Subject: [PATCH 012/194] Update Method List --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bc845dee..ab509e1b 100644 --- a/README.md +++ b/README.md @@ -342,11 +342,14 @@ var styles = StyleSheet.create({ ### Methods -* [dismissFullscreenPlayer](#dismissfullscreenplayer) -* [presentFullscreenPlayer](#presentfullscreenplayer) -* [save](#save) -* [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop) -* [seek](#seek) +| Method Name |Plateforms Support | +|--|--| +|[dismissFullscreenPlayer](#dismissfullscreenplayer)|Android ExoPlayer, Android MediaPlayer, iOS| +|[presentFullscreenPlayer](#presentfullscreenplayer)|Android ExoPlayer, Android MediaPlayer, iOS| +|[save](#save)|iOS| +|[restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)|iOS| +|[seek](#seek)|All| + ### Configurable props From e17ee74f79ca28f0180c2bfdac8136b7045657e2 Mon Sep 17 00:00:00 2001 From: Mudaser Ali Date: Sat, 17 Oct 2020 08:59:12 +0500 Subject: [PATCH 013/194] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ab509e1b..5966b37e 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ var styles = StyleSheet.create({ ``` ### Configurable props -| Prop Name |Plateforms Support | +| Name |Plateforms Support | |--|--| |[allowsExternalPlayback](#allowsexternalplayback) |iOS | |[audioOnly](#audioonly)|All | @@ -320,7 +320,7 @@ var styles = StyleSheet.create({ ### Event props -| Prop Name |Plateforms Support | +| Name |Plateforms Support | |--|--| |[onAudioBecomingNoisy](#onaudiobecomingnoisy)|Android ExoPlayer, iOS| |[onBandwidthUpdate](#onbandwidthupdate)|Android ExoPlayer| @@ -342,7 +342,7 @@ var styles = StyleSheet.create({ ### Methods -| Method Name |Plateforms Support | +| Name |Plateforms Support | |--|--| |[dismissFullscreenPlayer](#dismissfullscreenplayer)|Android ExoPlayer, Android MediaPlayer, iOS| |[presentFullscreenPlayer](#presentfullscreenplayer)|Android ExoPlayer, Android MediaPlayer, iOS| From 568971be7735ce5022c5c2691d5efd399aa4012b Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Mon, 15 Mar 2021 16:51:16 +0200 Subject: [PATCH 014/194] Fix AudoFocus pausing video when attempting to play (#1) Fix AudioFocus bug that could cause the player to stop responding to play/pause in some instances. Fixes issue react-native-video#1945 This was caused by the player requesting audio focus on each play (un-pause) and that resulted in a small window of Audio focus loss and then gain. The focus loss results in the player being paused while the player was supposed to play at the time. The solution is to keep track of Audio focus and not request new focus if we already have it. --- CHANGELOG.md | 2 ++ .../com/brentvatne/exoplayer/ReactExoplayerView.java | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e2518c..731db2e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Changelog +- Fix Android AudioFocus bug that could cause player to not respond to play/pause in some instances [#2311](https://github.com/react-native-video/react-native-video/pull/2311) + ### Version 5.1.0-alpha9 - Add ARM64 support for windows [#2137](https://github.com/react-native-community/react-native-video/pull/2137) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index b41b2768..20ce983a 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -122,6 +122,7 @@ class ReactExoplayerView extends FrameLayout implements private boolean isPaused; private boolean isBuffering; private boolean muted = false; + private boolean hasAudioFocus = false; private float rate = 1f; private float audioVolume = 1f; private int minLoadRetryCount = 3; @@ -567,7 +568,7 @@ class ReactExoplayerView extends FrameLayout implements } private boolean requestAudioFocus() { - if (disableFocus || srcUri == null) { + if (disableFocus || srcUri == null || this.hasAudioFocus) { return true; } int result = audioManager.requestAudioFocus(this, @@ -582,8 +583,8 @@ class ReactExoplayerView extends FrameLayout implements } if (playWhenReady) { - boolean hasAudioFocus = requestAudioFocus(); - if (hasAudioFocus) { + this.hasAudioFocus = requestAudioFocus(); + if (this.hasAudioFocus) { player.setPlayWhenReady(true); } } else { @@ -678,6 +679,7 @@ class ReactExoplayerView extends FrameLayout implements public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_LOSS: + this.hasAudioFocus = false; eventEmitter.audioFocusChanged(false); pausePlayback(); audioManager.abandonAudioFocus(this); @@ -686,6 +688,7 @@ class ReactExoplayerView extends FrameLayout implements eventEmitter.audioFocusChanged(false); break; case AudioManager.AUDIOFOCUS_GAIN: + this.hasAudioFocus = true; eventEmitter.audioFocusChanged(true); break; default: From 61009817603b3440cb0d00dda47633d26f11f239 Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Wed, 17 Mar 2021 17:49:10 +0200 Subject: [PATCH 015/194] Upgrade exoplayer to 2.13.2 (#2) Upgrade ExoPlayer from 2.11.4 to 2.13.2 and fix any related issues related to the upgrade and deprecated method use. --- android-exoplayer/build.gradle | 4 +- .../brentvatne/exoplayer/ExoPlayerView.java | 15 ++-- .../exoplayer/ReactExoplayerView.java | 74 ++++++++++--------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index 296e21cb..cebc9df3 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -28,7 +28,7 @@ android { dependencies { implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - implementation('com.google.android.exoplayer:exoplayer:2.11.4') { + implementation('com.google.android.exoplayer:exoplayer:2.13.2') { exclude group: 'com.android.support' } @@ -37,7 +37,7 @@ dependencies { implementation "androidx.core:core:1.1.0" implementation "androidx.media:media:1.1.0" - implementation('com.google.android.exoplayer:extension-okhttp:2.11.4') { + implementation('com.google.android.exoplayer:extension-okhttp:2.13.2') { exclude group: 'com.squareup.okhttp3', module: 'okhttp' } implementation 'com.squareup.okhttp3:okhttp:3.14.3' diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index afa5106c..99446bde 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -17,6 +17,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; @@ -113,8 +114,8 @@ public final class ExoPlayerView extends FrameLayout { } /** - * Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#setTextOutput} and - * {@link SimpleExoPlayer#setVideoListener} method of the player will be called and previous + * Set the {@link SimpleExoPlayer} to use. The {@link SimpleExoPlayer#addTextOutput} and + * {@link SimpleExoPlayer#addVideoListener} method of the player will be called and previous * assignments are overridden. * * @param player The {@link SimpleExoPlayer} to use. @@ -124,8 +125,8 @@ public final class ExoPlayerView extends FrameLayout { return; } if (this.player != null) { - this.player.setTextOutput(null); - this.player.setVideoListener(null); + this.player.addTextOutput(null); + this.player.removeVideoListener(componentListener); this.player.removeListener(componentListener); this.player.setVideoSurface(null); } @@ -133,9 +134,9 @@ public final class ExoPlayerView extends FrameLayout { shutterView.setVisibility(VISIBLE); if (player != null) { setVideoView(); - player.setVideoListener(componentListener); + player.addVideoListener(componentListener); player.addListener(componentListener); - player.setTextOutput(componentListener); + player.addTextOutput(componentListener); } } @@ -205,7 +206,7 @@ public final class ExoPlayerView extends FrameLayout { layout.invalidateAspectRatio(); } - private final class ComponentListener implements SimpleExoPlayer.VideoListener, + private final class ComponentListener implements VideoListener, TextOutput, ExoPlayer.EventListener { // TextRenderer.Output implementation diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 20ce983a..d7c33214 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -37,7 +37,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; -import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; @@ -62,7 +62,7 @@ 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.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -88,7 +88,7 @@ class ReactExoplayerView extends FrameLayout implements BecomingNoisyListener, AudioManager.OnAudioFocusChangeListener, MetadataOutput, - DefaultDrmSessionEventListener { + DrmSessionEventListener { private static final String TAG = "ReactExoplayerView"; @@ -395,7 +395,7 @@ class ReactExoplayerView extends FrameLayout implements @Override public void run() { if (player == null) { - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); + ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelector.setParameters(trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); @@ -410,23 +410,11 @@ class ReactExoplayerView extends FrameLayout implements DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - // DRM - DrmSessionManager drmSessionManager = null; - if (self.drmUUID != null) { - try { - drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, - self.drmLicenseHeader); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); - eventEmitter.error(getResources().getString(errorStringId), e); - return; - } - } - // End DRM - player = ExoPlayerFactory.newSimpleInstance(getContext(), renderersFactory, - trackSelector, defaultLoadControl, drmSessionManager, bandwidthMeter); + player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) + .setTrackSelector​(trackSelector) + .setBandwidthMeter(bandwidthMeter) + .setLoadControl(defaultLoadControl) + .build(); player.addListener(self); player.addMetadataOutput(self); exoPlayerView.setPlayer(player); @@ -441,8 +429,24 @@ class ReactExoplayerView extends FrameLayout implements if (playerNeedsSource && srcUri != null) { exoPlayerView.invalidateAspectRatio(); + // DRM + DrmSessionManager drmSessionManager = null; + if (self.drmUUID != null) { + try { + drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, + self.drmLicenseHeader); + } catch (UnsupportedDrmException e) { + int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + eventEmitter.error(getResources().getString(errorStringId), e); + return; + } + } + // End DRM + ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(srcUri, extension); + MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); MediaSource mediaSource; if (mediaSourceList.size() == 0) { mediaSource = videoSource; @@ -473,7 +477,7 @@ class ReactExoplayerView extends FrameLayout implements }, 1); } - private DrmSessionManager buildDrmSessionManager(UUID uuid, + private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; @@ -486,11 +490,11 @@ class ReactExoplayerView extends FrameLayout implements keyRequestPropertiesArray[i + 1]); } } - return new DefaultDrmSessionManager<>(uuid, + return new DefaultDrmSessionManager(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, false, 3); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension : uri.getLastPathSegment()); switch (type) { @@ -498,26 +502,30 @@ class ReactExoplayerView extends FrameLayout implements return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false) - ).setLoadErrorHandlingPolicy( + ).setDrmSessionManager(drmSessionManager) + .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(uri); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false) - ).setLoadErrorHandlingPolicy( + ).setDrmSessionManager(drmSessionManager) + .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(uri); case C.TYPE_HLS: return new HlsMediaSource.Factory( mediaDataSourceFactory - ).setLoadErrorHandlingPolicy( + ).setDrmSessionManager(drmSessionManager) + .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(uri); case C.TYPE_OTHER: return new ProgressiveMediaSource.Factory( mediaDataSourceFactory - ).setLoadErrorHandlingPolicy( + ).setDrmSessionManager(drmSessionManager) + .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(uri); default: { @@ -1339,23 +1347,23 @@ class ReactExoplayerView extends FrameLayout implements @Override - public void onDrmKeysLoaded() { + public void onDrmKeysLoaded(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { Log.d("DRM Info", "onDrmKeysLoaded"); } @Override - public void onDrmSessionManagerError(Exception e) { + public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, Exception e) { Log.d("DRM Info", "onDrmSessionManagerError"); eventEmitter.error("onDrmSessionManagerError", e); } @Override - public void onDrmKeysRestored() { + public void onDrmKeysRestored(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { Log.d("DRM Info", "onDrmKeysRestored"); } @Override - public void onDrmKeysRemoved() { + public void onDrmKeysRemoved(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) { Log.d("DRM Info", "onDrmKeysRemoved"); } From 61e6535b2f9b31eccae7b1b653515a1668df05b1 Mon Sep 17 00:00:00 2001 From: Adrian Mui Date: Thu, 18 Mar 2021 03:58:04 -0700 Subject: [PATCH 016/194] Feature/toggle buffering (#3) This PR adds the property disableBuffering: boolean for android. The PR was initally created as a personal fork and referenced in crunchyroll/vilos#1227 also updated RNVLoadControl constructor and method to reflect new ExoPlayer.LoadControl api related ticket: https://jira.tenkasu.net/browse/VEX-3776 --- Video.js | 1 + .../exoplayer/ReactExoplayerView.java | 50 ++++++++++++++++--- .../exoplayer/ReactExoplayerViewManager.java | 6 +++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/Video.js b/Video.js index b492d48b..f1119150 100644 --- a/Video.js +++ b/Video.js @@ -470,6 +470,7 @@ Video.propTypes = { ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), reportBandwidth: PropTypes.bool, disableFocus: PropTypes.bool, + disableBuffering: PropTypes.bool, controls: PropTypes.bool, audioOnly: PropTypes.bool, currentTime: PropTypes.number, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index d7c33214..7881c914 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -26,6 +26,7 @@ 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.facebook.react.util.RNLog; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; @@ -70,6 +71,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; @@ -148,6 +150,7 @@ class ReactExoplayerView extends FrameLayout implements private Dynamic textTrackValue; private ReadableArray textTracks; private boolean disableFocus; + private boolean disableBuffering; private boolean preventsDisplaySleepDuringVideoPlayback = true; private float mProgressUpdateInterval = 250.0f; private boolean playInBackground = false; @@ -183,7 +186,7 @@ class ReactExoplayerView extends FrameLayout implements } } }; - + public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { Timeline.Window window = new Timeline.Window(); if(!player.getCurrentTimeline().isEmpty()) { @@ -388,6 +391,28 @@ class ReactExoplayerView extends FrameLayout implements view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight()); } + private class RNVLoadControl extends DefaultLoadControl { + public RNVLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { + super(allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, + backBufferDurationMs, + retainBackBufferFromKeyframe); + } + + @Override + public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { + if (ReactExoplayerView.this.disableBuffering) { + return false; + } + return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed); + } + } + private void initializePlayer() { ReactExoplayerView self = this; // This ensures all props have been settled, to avoid async racing conditions. @@ -401,19 +426,24 @@ class ReactExoplayerView extends FrameLayout implements .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - DefaultLoadControl.Builder defaultLoadControlBuilder = new DefaultLoadControl.Builder(); - defaultLoadControlBuilder.setAllocator(allocator); - defaultLoadControlBuilder.setBufferDurationsMs(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs); - defaultLoadControlBuilder.setTargetBufferBytes(-1); - defaultLoadControlBuilder.setPrioritizeTimeOverSizeThresholds(true); - DefaultLoadControl defaultLoadControl = defaultLoadControlBuilder.createDefaultLoadControl(); + RNVLoadControl loadControl = new RNVLoadControl( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + -1, + true, + DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, + DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME + ); DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) .setTrackSelector​(trackSelector) .setBandwidthMeter(bandwidthMeter) - .setLoadControl(defaultLoadControl) + .setLoadControl(loadControl) .build(); player.addListener(self); player.addMetadataOutput(self); @@ -1282,6 +1312,10 @@ class ReactExoplayerView extends FrameLayout implements this.disableFocus = disableFocus; } + public void setDisableBuffering(boolean disableBuffering) { + this.disableBuffering = disableBuffering; + } + public void setFullscreen(boolean fullscreen) { if (fullscreen == isFullscreen) { return; // Avoid generating events when nothing is changing diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 0d81e0b2..2fc12e42 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -63,6 +63,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Fri, 7 May 2021 11:37:57 +0300 Subject: [PATCH 017/194] VEX-4556: The app crashes when the premium episode is loaded (#4) Fix player crashing when it is being cleared. --- .../java/com/brentvatne/exoplayer/ExoPlayerView.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 99446bde..61cb0dd6 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -86,6 +86,14 @@ public final class ExoPlayerView extends FrameLayout { addViewInLayout(layout, 0, aspectRatioParams); } + private void clearVideoView() { + if (surfaceView instanceof TextureView) { + player.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + player.clearVideoSurfaceView((SurfaceView) surfaceView); + } + } + private void setVideoView() { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); @@ -125,10 +133,10 @@ public final class ExoPlayerView extends FrameLayout { return; } if (this.player != null) { - this.player.addTextOutput(null); + this.player.removeTextOutput(componentListener); this.player.removeVideoListener(componentListener); this.player.removeListener(componentListener); - this.player.setVideoSurface(null); + clearVideoView(); } this.player = player; shutterView.setVisibility(VISIBLE); From 80873102a4e739cf6b08343a91c21f760c6deaf5 Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Mon, 17 May 2021 13:09:09 +0300 Subject: [PATCH 018/194] VEX-4579: Network loss handling (#5) Add support for customizing back buffer duration and handle network errors gracefully to prevent releasing the player when network is lost. --- README.md | 14 ++++++++ .../DefaultReactExoplayerConfig.java | 14 +++++++- .../exoplayer/ReactExoplayerConfig.java | 3 ++ ...ReactExoplayerLoadErrorHandlingPolicy.java | 33 +++++++++++++++++++ .../exoplayer/ReactExoplayerView.java | 13 +++++++- .../exoplayer/ReactExoplayerViewManager.java | 12 +++++++ 6 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java diff --git a/README.md b/README.md index 1b6a913e..fc6681f0 100644 --- a/README.md +++ b/README.md @@ -276,10 +276,12 @@ var styles = StyleSheet.create({ * [allowsExternalPlayback](#allowsexternalplayback) * [audioOnly](#audioonly) * [automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) +* [backBufferDurationMs](#backBufferDurationMs) * [bufferConfig](#bufferconfig) * [controls](#controls) * [currentPlaybackTime](#currentPlaybackTime) * [disableFocus](#disableFocus) +* [disableDisconnectError](#disableDisconnectError) * [filter](#filter) * [filterEnabled](#filterEnabled) * [fullscreen](#fullscreen) @@ -367,6 +369,11 @@ A Boolean value that indicates whether the player should automatically delay pla Platforms: iOS +#### backBufferDurationMs +The number of milliseconds of buffer to keep before the current position. This allows rewinding without rebuffering within that duration. + +Platforms: Android ExoPlayer + #### bufferConfig Adjust the buffer settings. This prop takes an object with one or more of the properties listed below. @@ -416,6 +423,13 @@ Determines whether video audio should override background music/audio in Android Platforms: Android Exoplayer +#### disableDisconnectError +Determines if the player needs to throw an error when connection is lost or not +* **false (default)** - Player will throw an error when connection is lost +* **true** - Player will keep trying to buffer when network connect is lost + +Platforms: Android Exoplayer + ### DRM To setup DRM please follow [this guide](./DRM.md) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DefaultReactExoplayerConfig.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DefaultReactExoplayerConfig.java index d68274b7..3475273c 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DefaultReactExoplayerConfig.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/DefaultReactExoplayerConfig.java @@ -9,16 +9,28 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; public class DefaultReactExoplayerConfig implements ReactExoplayerConfig { private final DefaultBandwidthMeter bandwidthMeter; + private boolean disableDisconnectError = false; public DefaultReactExoplayerConfig(Context context) { this.bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); } - @Override public LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount) { + if (this.disableDisconnectError) { + // Use custom error handling policy to prevent throwing an error when losing network connection + return new ReactExoplayerLoadErrorHandlingPolicy(minLoadRetryCount); + } return new DefaultLoadErrorHandlingPolicy(minLoadRetryCount); } + public void setDisableDisconnectError(boolean disableDisconnectError) { + this.disableDisconnectError = disableDisconnectError; + } + + public boolean getDisableDisconnectError() { + return this.disableDisconnectError; + } + @Override public DefaultBandwidthMeter getBandwidthMeter() { return bandwidthMeter; diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerConfig.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerConfig.java index 522578b7..2cc56f9a 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerConfig.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerConfig.java @@ -9,5 +9,8 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; public interface ReactExoplayerConfig { LoadErrorHandlingPolicy buildLoadErrorHandlingPolicy(int minLoadRetryCount); + void setDisableDisconnectError(boolean disableDisconnectError); + boolean getDisableDisconnectError(); + DefaultBandwidthMeter getBandwidthMeter(); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java new file mode 100644 index 00000000..c701310f --- /dev/null +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java @@ -0,0 +1,33 @@ +package com.brentvatne.exoplayer; + +import java.io.IOException; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; +import com.google.android.exoplayer2.C; + +public final class ReactExoplayerLoadErrorHandlingPolicy extends DefaultLoadErrorHandlingPolicy { + private int minLoadRetryCount = Integer.MAX_VALUE; + + public ReactExoplayerLoadErrorHandlingPolicy(int minLoadRetryCount) { + super(minLoadRetryCount); + this.minLoadRetryCount = minLoadRetryCount; + } + + @Override + public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) { + if (loadErrorInfo.exception instanceof HttpDataSourceException) { + // Capture the error we get when there is no network connectivity and keep retrying it + return 1000; // Retry every second + } else if(loadErrorInfo.errorCount < this.minLoadRetryCount) { + return Math.min((loadErrorInfo.errorCount - 1) * 1000, 5000); // Default timeout handling + } else { + return C.TIME_UNSET; // Done retrying and will return the error immediately + } + } + + @Override + public int getMinimumLoadableRetryCount(int dataType) { + return Integer.MAX_VALUE; + } +} \ No newline at end of file diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 7881c914..488f2d3b 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -139,6 +139,7 @@ class ReactExoplayerView extends FrameLayout implements private Handler mainHandler; // Props from React + private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS; private Uri srcUri; private String extension; private boolean repeat; @@ -151,6 +152,7 @@ class ReactExoplayerView extends FrameLayout implements private ReadableArray textTracks; private boolean disableFocus; private boolean disableBuffering; + private boolean disableDisconnectError; private boolean preventsDisplaySleepDuringVideoPlayback = true; private float mProgressUpdateInterval = 250.0f; private boolean playInBackground = false; @@ -434,7 +436,7 @@ class ReactExoplayerView extends FrameLayout implements bufferForPlaybackAfterRebufferMs, -1, true, - DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, + backBufferDurationMs, DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME ); DefaultRenderersFactory renderersFactory = @@ -527,6 +529,7 @@ class ReactExoplayerView extends FrameLayout implements private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension : uri.getLastPathSegment()); + config.setDisableDisconnectError(this.disableDisconnectError); switch (type) { case C.TYPE_SS: return new SsMediaSource.Factory( @@ -1312,10 +1315,18 @@ class ReactExoplayerView extends FrameLayout implements this.disableFocus = disableFocus; } + public void setBackBufferDurationMs(int backBufferDurationMs) { + this.backBufferDurationMs = backBufferDurationMs; + } + public void setDisableBuffering(boolean disableBuffering) { this.disableBuffering = disableBuffering; } + public void setDisableDisconnectError(boolean disableDisconnectError) { + this.disableDisconnectError = disableDisconnectError; + } + public void setFullscreen(boolean fullscreen) { if (fullscreen == isFullscreen) { return; // Avoid generating events when nothing is changing diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 2fc12e42..c460f176 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -49,6 +49,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Tue, 18 May 2021 09:20:32 -0600 Subject: [PATCH 019/194] Fix for issue #2153 --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index b41b2768..56be2eb5 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1219,13 +1219,11 @@ class ReactExoplayerView extends FrameLayout implements public void setMutedModifier(boolean muted) { this.muted = muted; - audioVolume = muted ? 0.f : 1.f; if (player != null) { - player.setVolume(audioVolume); + player.setVolume(muted ? 0.f : audioVolume); } } - public void setVolumeModifier(float volume) { audioVolume = volume; if (player != null) { From dcea67eb963c24a5876c75621dd15ecef8dc197c Mon Sep 17 00:00:00 2001 From: anderslemke Date: Thu, 3 Jun 2021 15:11:57 +0200 Subject: [PATCH 020/194] Make sure modifiers are applied before playing --- ios/Video/RCTVideo.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 989167a6..8811b65c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -383,6 +383,7 @@ static int const RCTVideoUnset = -1; } self->_player = [AVPlayer playerWithPlayerItem:self->_playerItem]; + [self applyModifiers]; self->_player.actionAtItemEnd = AVPlayerActionAtItemEndNone; [self->_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; From d094886087c046bc79888c47f48833463bf7350a Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Sun, 13 Jun 2021 12:06:37 +0700 Subject: [PATCH 021/194] keep screen on when in fullscreen mode --- .../brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java index d3b87208..09b2cdfd 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -4,6 +4,7 @@ import android.content.pm.ActivityInfo; import android.os.Bundle; import android.view.KeyEvent; import android.view.View; +import android.view.WindowManager; import android.widget.ImageView; import androidx.appcompat.app.AppCompatActivity; @@ -24,6 +25,8 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + id = getIntent().getIntExtra(EXTRA_ID, -1); String orientation = getIntent().getStringExtra(EXTRA_ORIENTATION); if ("landscape".equals(orientation)) { From cba88fa9d8d9b7ef86ec310bc81c5038581c4f43 Mon Sep 17 00:00:00 2001 From: Nick Fujita Date: Wed, 30 Jun 2021 10:24:21 +0900 Subject: [PATCH 022/194] VEX-5044: Allow exoplayer to preinit with empty source (#6) - Allow player to be init before source is provided, and later update once a source is provided. - Adds handling for providing a empty source in order to stop playback and clear out any existing content --- README.md | 6 ++++++ .../exoplayer/ReactExoplayerView.java | 18 ++++++++++++++---- .../exoplayer/ReactExoplayerViewManager.java | 1 + 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fc6681f0..fdb8231b 100644 --- a/README.md +++ b/README.md @@ -770,6 +770,12 @@ Platforms: Android ExoPlayer #### source Sets the media source. You can pass an asset loaded via require or an object with a uri. +Setting the source will trigger the player to attempt to load the provided media with all other given props. Please be sure that all props are provided before/at the same time as setting the source. + +Rendering the player component with a null source will init the player, and start playing once a source value is provided. + +Providing a null source value after loading a previous source will stop playback, and clear out the previous source content. + The docs for this prop are incomplete and will be updated as each option is investigated and tested. diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 488f2d3b..209cc949 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -497,6 +497,7 @@ class ReactExoplayerView extends FrameLayout implements player.prepare(mediaSource, !haveResumePosition, false); playerNeedsSource = false; + reLayout(exoPlayerView); eventEmitter.loadStart(); loadVideoStarted = true; } @@ -1045,7 +1046,6 @@ class ReactExoplayerView extends FrameLayout implements public void setSrc(final Uri uri, final String extension, Map headers) { if (uri != null) { - boolean isOriginalSourceNull = srcUri == null; boolean isSourceEqual = uri.equals(srcUri); this.srcUri = uri; @@ -1055,12 +1055,23 @@ class ReactExoplayerView extends FrameLayout implements DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter, this.requestHeaders); - if (!isOriginalSourceNull && !isSourceEqual) { + if (!isSourceEqual) { reloadSource(); } } } + public void clearSrc() { + if (srcUri != null) { + player.stop(true); + this.srcUri = null; + this.extension = null; + this.requestHeaders = null; + this.mediaDataSourceFactory = null; + clearResumePosition(); + } + } + public void setProgressUpdateInterval(final float progressUpdateInterval) { mProgressUpdateInterval = progressUpdateInterval; } @@ -1071,14 +1082,13 @@ class ReactExoplayerView extends FrameLayout implements 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 = buildDataSourceFactory(true); - if (!isOriginalSourceNull && !isSourceEqual) { + if (!isSourceEqual) { reloadSource(); } } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index c460f176..fc9a1354 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -147,6 +147,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; if (TextUtils.isEmpty(uriString)) { + videoView.clearSrc(); return; } From 93604b2c25f09405fd1765a02405ed5da00e5bcf Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Wed, 7 Jul 2021 18:59:55 +0300 Subject: [PATCH 023/194] VEX-3245: Buffer Progress UI While Paused (#7) Add support for onBufferProgress prop on Android to get buffer data even when the player is paused. --- CHANGELOG.md | 2 + README.md | 13 +++++++ Video.js | 7 ++++ .../exoplayer/ReactExoplayerView.java | 39 +++++++++++++++++++ .../exoplayer/VideoEventEmitter.java | 12 ++++++ 5 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 731db2e8..3fdf4ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Changelog +- Add support for `onBufferProgress` on Android for getting buffer data even when the player is paused + - Fix Android AudioFocus bug that could cause player to not respond to play/pause in some instances [#2311](https://github.com/react-native-video/react-native-video/pull/2311) ### Version 5.1.0-alpha9 diff --git a/README.md b/README.md index fdb8231b..d46e8d39 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,7 @@ var styles = StyleSheet.create({ ### Event props * [onAudioBecomingNoisy](#onaudiobecomingnoisy) * [onBandwidthUpdate](#onbandwidthupdate) +* [onBufferProgress](#onbufferprogress) * [onEnd](#onend) * [onExternalPlaybackChange](#onexternalplaybackchange) * [onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent) @@ -944,6 +945,18 @@ Note: On Android ExoPlayer, you must set the [reportBandwidth](#reportbandwidth) Platforms: Android ExoPlayer + +#### onBufferProgress +Callback function that is called on a set interval which contains the buffer start and end position in ms. + +Payload: +Property | Type | Description +--- | --- | --- +start | number | The buffer start (ms) +end | number | The buffer end (ms) + +Platforms: Android ExoPlayer + #### onEnd Callback function that is called when the player reaches the end of the media. diff --git a/Video.js b/Video.js index f1119150..88d90324 100644 --- a/Video.js +++ b/Video.js @@ -233,6 +233,12 @@ export default class Video extends Component { } }; + _onBufferProgress = (event) => { + if (this.props.onBufferProgress) { + this.props.onBufferProgress(event.nativeEvent); + } + }; + _onGetLicense = (event) => { if (this.props.drm && this.props.drm.getLicense instanceof Function) { const data = event.nativeEvent; @@ -311,6 +317,7 @@ export default class Video extends Component { onVideoSeek: this._onSeek, onVideoEnd: this._onEnd, onVideoBuffer: this._onBuffer, + onVideoBufferProgress: this._onBufferProgress, onVideoBandwidthUpdate: this._onBandwidthUpdate, onTimedMetadata: this._onTimedMetadata, onVideoAudioBecomingNoisy: this._onAudioBecomingNoisy, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 209cc949..b928ef07 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -81,6 +81,8 @@ import java.util.ArrayList; import java.util.Locale; import java.util.UUID; import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -137,6 +139,7 @@ class ReactExoplayerView extends FrameLayout implements private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; private Handler mainHandler; + private Timer bufferCheckTimer; // Props from React private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS; @@ -415,6 +418,40 @@ class ReactExoplayerView extends FrameLayout implements } } + private void startBufferCheckTimer() { + SimpleExoPlayer player = this.player; + VideoEventEmitter eventEmitter = this.eventEmitter; + Handler mainHandler = this.mainHandler; + + if (this.bufferCheckTimer != null) { + this.stopBufferCheckTimer(); + } + + this.bufferCheckTimer = new Timer(); + TimerTask bufferCheckTimerTask = new TimerTask() { + @Override + public void run() { + if (mainHandler != null) { + mainHandler.post(new Runnable() { + public void run() { + if (player != null) { + double bufferedDuration = (double) (player.getBufferedPercentage() * player.getDuration() / 100); + eventEmitter.bufferProgress(0d, bufferedDuration); + } + } + }); + } + }; + }; + + this.bufferCheckTimer.scheduleAtFixedRate(bufferCheckTimerTask, 500, 1000); + } + + private void stopBufferCheckTimer() { + this.bufferCheckTimer.cancel(); + this.bufferCheckTimer = null; + } + private void initializePlayer() { ReactExoplayerView self = this; // This ensures all props have been settled, to avoid async racing conditions. @@ -506,6 +543,7 @@ class ReactExoplayerView extends FrameLayout implements initializePlayerControl(); setControls(controls); applyModifiers(); + startBufferCheckTimer(); } }, 1); } @@ -597,6 +635,7 @@ class ReactExoplayerView extends FrameLayout implements private void releasePlayer() { if (player != null) { + stopBufferCheckTimer(); updateResumePosition(); player.release(); player.removeMetadataOutput(this); diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index ea0cc5ac..a6d3b6c8 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -42,6 +42,7 @@ class VideoEventEmitter { 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_BUFFER_PROGRESS = "onVideoBufferProgress"; private static final String EVENT_IDLE = "onVideoIdle"; private static final String EVENT_TIMED_METADATA = "onTimedMetadata"; private static final String EVENT_AUDIO_BECOMING_NOISY = "onVideoAudioBecomingNoisy"; @@ -63,6 +64,7 @@ class VideoEventEmitter { EVENT_RESUME, EVENT_READY, EVENT_BUFFER, + EVENT_BUFFER_PROGRESS, EVENT_IDLE, EVENT_TIMED_METADATA, EVENT_AUDIO_BECOMING_NOISY, @@ -87,6 +89,7 @@ class VideoEventEmitter { EVENT_RESUME, EVENT_READY, EVENT_BUFFER, + EVENT_BUFFER_PROGRESS, EVENT_IDLE, EVENT_TIMED_METADATA, EVENT_AUDIO_BECOMING_NOISY, @@ -104,6 +107,8 @@ class VideoEventEmitter { private static final String EVENT_PROP_STEP_FORWARD = "canStepForward"; private static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward"; + private static final String EVENT_PROP_BUFFER_START = "bufferStart"; + private static final String EVENT_PROP_BUFFER_END = "bufferEnd"; private static final String EVENT_PROP_DURATION = "duration"; private static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration"; private static final String EVENT_PROP_SEEKABLE_DURATION = "seekableDuration"; @@ -206,6 +211,13 @@ class VideoEventEmitter { receiveEvent(EVENT_BUFFER, map); } + void bufferProgress(double start, double end) { + WritableMap map = Arguments.createMap(); + map.putDouble(EVENT_PROP_BUFFER_START, start); + map.putDouble(EVENT_PROP_BUFFER_END, end); + receiveEvent(EVENT_BUFFER_PROGRESS, map); + } + void idle() { receiveEvent(EVENT_IDLE, null); } From 7c17f5ec96e7913ffe5d97db952c061882ab4681 Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Fri, 6 Aug 2021 10:56:07 +0700 Subject: [PATCH 024/194] Update ReactExoplayerView.java --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 9ada2571..f2469032 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -460,7 +460,7 @@ class ReactExoplayerView extends FrameLayout implements new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) - .setTrackSelector​(trackSelector) + .setTrackSelector(trackSelector) .setBandwidthMeter(bandwidthMeter) .setLoadControl(defaultLoadControl) .build(); From a0b679cc076c2fb669df40834742f1d882aeadac Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Sun, 26 Sep 2021 16:38:10 +0300 Subject: [PATCH 025/194] VEX-5545: Some Android codecs don't support specific resolutions (#8) Add support for detecting if format is supported and exclude unsupported resolutions from auto quality selection and video track info in RN. --- .../exoplayer/ReactExoplayerView.java | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index b928ef07..9fe85c61 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; +import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.metadata.Metadata; @@ -921,7 +922,9 @@ class ReactExoplayerView extends FrameLayout implements videoTrack.putString("codecs", format.codecs != null ? format.codecs : ""); videoTrack.putString("trackId", format.id == null ? String.valueOf(trackIndex) : format.id); - videoTracks.pushMap(videoTrack); + if (isFormatSupported(format)) { + videoTracks.pushMap(videoTrack); + } } } return videoTracks; @@ -1241,10 +1244,31 @@ class ReactExoplayerView extends FrameLayout implements if (groupIndex == C.INDEX_UNSET && trackType == C.TRACK_TYPE_VIDEO && groups.length != 0) { // Video auto // Add all tracks as valid options for ABR to choose from TrackGroup group = groups.get(0); - tracks = new int[group.length]; + int[] allTracks = new int[group.length]; groupIndex = 0; + for (int j = 0; j < group.length; j++) { - tracks[j] = j; + allTracks[j] = j; + } + + // Valiate list of all tracks and add only supported formats + int supportedFormatLength = 0; + ArrayList supportedTrackList = new ArrayList(); + for (int g = 0; g < allTracks.length; g++) { + Format format = group.getFormat(g); + if (isFormatSupported(format)) { + supportedFormatLength++; + } + } + tracks = new int[supportedFormatLength + 1]; + int o = 0; + for (int k = 0; k < allTracks.length; k++) { + Format format = group.getFormat(k); + if (isFormatSupported(format)) { + tracks[o] = allTracks[k]; + supportedTrackList.add(allTracks[k]); + o++; + } } } @@ -1262,6 +1286,25 @@ class ReactExoplayerView extends FrameLayout implements trackSelector.setParameters(selectionParameters); } + private boolean isFormatSupported(Format format) { + int width = format.width == Format.NO_VALUE ? 0 : format.width; + int height = format.height == Format.NO_VALUE ? 0 : format.height; + float frameRate = format.frameRate == Format.NO_VALUE ? 0 : format.frameRate; + String mimeType = format.sampleMimeType; + if (mimeType == null) { + return true; + } + boolean isSupported = false; + try { + MediaCodecInfo codecInfo = MediaCodecUtil.getDecoderInfo(mimeType, false, false); + isSupported = codecInfo.isVideoSizeAndRateSupportedV21(width, height, frameRate); + } catch (Exception e) { + // Failed to get decoder info - assume it is supported + isSupported = true; + } + return isSupported; + } + private int getGroupIndexForDefaultLocale(TrackGroupArray groups) { if (groups.length == 0){ return C.INDEX_UNSET; From 2fcc1de6269d374c272f4cf1cdcc02e71d1188cb Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Tue, 12 Oct 2021 14:56:50 +0300 Subject: [PATCH 026/194] VEX-5825: Android player error handling improvements (#10) Improve error handling. --- .../exoplayer/ReactExoplayerView.java | 203 ++++++++++-------- .../exoplayer/VideoEventEmitter.java | 20 ++ 2 files changed, 139 insertions(+), 84 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 9fe85c61..4bb7f43b 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.MediaDrmCallbackException; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; @@ -459,94 +460,103 @@ class ReactExoplayerView extends FrameLayout implements new Handler().postDelayed(new Runnable() { @Override public void run() { - if (player == null) { - ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelector.setParameters(trackSelector.buildUponParameters() - .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); + try { + if (player == null) { + ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); + trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); - DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - RNVLoadControl loadControl = new RNVLoadControl( - allocator, - minBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs, - -1, - true, - backBufferDurationMs, - DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME - ); - DefaultRenderersFactory renderersFactory = - new DefaultRenderersFactory(getContext()) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) - .setTrackSelector​(trackSelector) - .setBandwidthMeter(bandwidthMeter) - .setLoadControl(loadControl) - .build(); - player.addListener(self); - player.addMetadataOutput(self); - exoPlayerView.setPlayer(player); - audioBecomingNoisyReceiver.setListener(self); - bandwidthMeter.addEventListener(new Handler(), self); - setPlayWhenReady(!isPaused); - playerNeedsSource = true; - - PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); - } - if (playerNeedsSource && srcUri != null) { - exoPlayerView.invalidateAspectRatio(); - - // DRM - DrmSessionManager drmSessionManager = null; - if (self.drmUUID != null) { - try { - drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, - self.drmLicenseHeader); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); - eventEmitter.error(getResources().getString(errorStringId), e); - return; - } - } - // End DRM - - ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); - MediaSource mediaSource; - if (mediaSourceList.size() == 0) { - mediaSource = videoSource; - } else { - mediaSourceList.add(0, videoSource); - MediaSource[] textSourceArray = mediaSourceList.toArray( - new MediaSource[mediaSourceList.size()] + DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + RNVLoadControl loadControl = new RNVLoadControl( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + -1, + true, + backBufferDurationMs, + DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME ); - mediaSource = new MergingMediaSource(textSourceArray); + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(getContext()) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); + player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) + .setTrackSelector​(trackSelector) + .setBandwidthMeter(bandwidthMeter) + .setLoadControl(loadControl) + .build(); + player.addListener(self); + player.addMetadataOutput(self); + exoPlayerView.setPlayer(player); + audioBecomingNoisyReceiver.setListener(self); + bandwidthMeter.addEventListener(new Handler(), self); + setPlayWhenReady(!isPaused); + playerNeedsSource = true; + + PlaybackParameters params = new PlaybackParameters(rate, 1f); + player.setPlaybackParameters(params); + } + if (playerNeedsSource && srcUri != null) { + exoPlayerView.invalidateAspectRatio(); + + // DRM + DrmSessionManager drmSessionManager = null; + if (self.drmUUID != null) { + try { + drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, + self.drmLicenseHeader); + } catch (UnsupportedDrmException e) { + int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + eventEmitter.error(getResources().getString(errorStringId), e, "3003"); + return; + } + } + // End DRM + + ArrayList mediaSourceList = buildTextSources(); + MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); + 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; + + reLayout(exoPlayerView); + eventEmitter.loadStart(); + loadVideoStarted = true; } - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - playerNeedsSource = false; - - reLayout(exoPlayerView); - eventEmitter.loadStart(); - loadVideoStarted = true; + // Initializing the playerControlView + initializePlayerControl(); + setControls(controls); + applyModifiers(); + startBufferCheckTimer(); + + } catch (Exception ex) { + self.playerNeedsSource = true; + Log.e("ExoPlayer Exception", "Failed to initialize Player!"); + Log.e("ExoPlayer Exception", ex.toString()); + eventEmitter.error(ex.toString(), ex, "1001"); } - - // Initializing the playerControlView - initializePlayerControl(); - setControls(controls); - applyModifiers(); - startBufferCheckTimer(); } }, 1); + } private DrmSessionManager buildDrmSessionManager(UUID uuid, @@ -567,6 +577,9 @@ class ReactExoplayerView extends FrameLayout implements } private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { + if (uri == null) { + throw new IllegalStateException("Invalid video uri"); + } int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension : uri.getLastPathSegment()); config.setDisableDisconnectError(this.disableDisconnectError); @@ -1015,6 +1028,7 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onPlayerError(ExoPlaybackException e) { String errorString = "ExoPlaybackException type : " + e.type; + String errorCode = "2001"; // Playback error code 2xxx (2001 - unknown playback exception) Exception ex = e; if (e.type == ExoPlaybackException.TYPE_RENDERER) { Exception cause = e.getRendererException(); @@ -1024,24 +1038,45 @@ class ReactExoplayerView extends FrameLayout implements (MediaCodecRenderer.DecoderInitializationException) cause; if (decoderInitializationException.codecInfo.name == null) { if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) { + errorCode = "2011"; errorString = getResources().getString(R.string.error_querying_decoders); } else if (decoderInitializationException.secureDecoderRequired) { + errorCode = "2012"; errorString = getResources().getString(R.string.error_no_secure_decoder, decoderInitializationException.mimeType); } else { + errorCode = "2013"; errorString = getResources().getString(R.string.error_no_decoder, decoderInitializationException.mimeType); } } else { + errorCode = "2014"; errorString = getResources().getString(R.string.error_instantiating_decoder, decoderInitializationException.codecInfo.name); } } } else if (e.type == ExoPlaybackException.TYPE_SOURCE) { - errorString = getResources().getString(R.string.unrecognized_media_format); + Exception cause = e.getSourceException(); + if (cause instanceof DefaultDrmSessionManager.MissingSchemeDataException) { + errorCode = "3004"; + errorString = getResources().getString(R.string.unrecognized_media_format); + } else if(cause instanceof MediaDrmCallbackException) { + errorCode = "3005"; + errorString = getResources().getString(R.string.unrecognized_media_format); + } else { + errorCode = "2021"; + errorString = getResources().getString(R.string.unrecognized_media_format); + } + if (cause != null) { + Throwable rootCause = cause.getCause(); + if (rootCause instanceof MediaDrmCallbackException) { + errorCode = "3005"; + errorString = getResources().getString(R.string.unrecognized_media_format); + } + } } - eventEmitter.error(errorString, ex); + eventEmitter.error(errorString, ex, errorCode); playerNeedsSource = true; if (isBehindLiveWindow(e)) { clearResumePosition(); @@ -1491,7 +1526,7 @@ class ReactExoplayerView extends FrameLayout implements @Override public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, Exception e) { Log.d("DRM Info", "onDrmSessionManagerError"); - eventEmitter.error("onDrmSessionManagerError", e); + eventEmitter.error("onDrmSessionManagerError", e, "3002"); } @Override diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index a6d3b6c8..266d2538 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -15,6 +15,8 @@ import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.io.StringWriter; +import java.io.PrintWriter; class VideoEventEmitter { @@ -130,6 +132,8 @@ class VideoEventEmitter { 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 = "errorException"; + private static final String EVENT_PROP_ERROR_TRACE = "errorStackTrace"; + private static final String EVENT_PROP_ERROR_CODE = "errorCode"; private static final String EVENT_PROP_TIMED_METADATA = "metadata"; @@ -243,9 +247,25 @@ class VideoEventEmitter { } void error(String errorString, Exception exception) { + _error(errorString, exception, "0001"); + } + + void error(String errorString, Exception exception, String errorCode) { + _error(errorString, exception, errorCode); + } + + void _error(String errorString, Exception exception, String errorCode) { + // Prepare stack trace + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + exception.printStackTrace(pw); + String stackTrace = sw.toString(); + WritableMap error = Arguments.createMap(); error.putString(EVENT_PROP_ERROR_STRING, errorString); error.putString(EVENT_PROP_ERROR_EXCEPTION, exception.toString()); + error.putString(EVENT_PROP_ERROR_CODE, errorCode); + error.putString(EVENT_PROP_ERROR_TRACE, stackTrace); WritableMap event = Arguments.createMap(); event.putMap(EVENT_PROP_ERROR, error); receiveEvent(EVENT_ERROR, event); From 3bcf2c60614c822c979dd550ae96672be291f67a Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Wed, 13 Oct 2021 18:03:29 +0300 Subject: [PATCH 027/194] VEX-5798: Implement DRM L1 to L3 fallback (#9) Add support for L1 to L3 Widevine fallback if playback fails initially. --- .../exoplayer/ReactExoplayerView.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 4bb7f43b..24dda940 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.drm.MediaDrmCallbackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; @@ -134,6 +135,7 @@ class ReactExoplayerView extends FrameLayout implements private int minLoadRetryCount = 3; private int maxBitRate = 0; private long seekTime = C.TIME_UNSET; + private boolean hasDrmFailed = false; private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; @@ -572,8 +574,13 @@ class ReactExoplayerView extends FrameLayout implements keyRequestPropertiesArray[i + 1]); } } + FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); + if (hasDrmFailed) { + // When DRM fails using L1 we want to switch to L3 + mediaDrm.setPropertyString("securityLevel", "L3"); + } return new DefaultDrmSessionManager(uuid, - FrameworkMediaDrm.newInstance(uuid), drmCallback, null, false, 3); + mediaDrm, drmCallback, null, false, 3); } private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { @@ -1029,6 +1036,7 @@ class ReactExoplayerView extends FrameLayout implements public void onPlayerError(ExoPlaybackException e) { String errorString = "ExoPlaybackException type : " + e.type; String errorCode = "2001"; // Playback error code 2xxx (2001 - unknown playback exception) + boolean needsReInitialization = false; Exception ex = e; if (e.type == ExoPlaybackException.TYPE_RENDERER) { Exception cause = e.getRendererException(); @@ -1057,6 +1065,9 @@ class ReactExoplayerView extends FrameLayout implements } } else if (e.type == ExoPlaybackException.TYPE_SOURCE) { + // Re-initialization improves recovery speed and properly resumes + needsReInitialization = true; + errorString = getResources().getString(R.string.unrecognized_media_format); Exception cause = e.getSourceException(); if (cause instanceof DefaultDrmSessionManager.MissingSchemeDataException) { errorCode = "3004"; @@ -1073,6 +1084,14 @@ class ReactExoplayerView extends FrameLayout implements if (rootCause instanceof MediaDrmCallbackException) { errorCode = "3005"; errorString = getResources().getString(R.string.unrecognized_media_format); + if (!hasDrmFailed) { + // When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time + hasDrmFailed = true; + playerNeedsSource = true; + updateResumePosition(); + initializePlayer(); + return; + } } } } @@ -1083,6 +1102,9 @@ class ReactExoplayerView extends FrameLayout implements initializePlayer(); } else { updateResumePosition(); + if (needsReInitialization) { + initializePlayer(); + } } } From 8b7543814854b12b81b007cd56d8f68f5b119055 Mon Sep 17 00:00:00 2001 From: Nick Fujita Date: Wed, 27 Oct 2021 10:35:07 +0900 Subject: [PATCH 028/194] VEX-5682: iOS Swift Conversion (#11) Converts ios implementation from objective-c to swift. --- .gitignore | 3 + examples/basic/yarn.lock | 23 +- ios/RCTVideo.xcodeproj/project.pbxproj | 91 +- ios/Video/DataStructures/DRMParams.swift | 30 + .../SelectedTrackCriteria.swift | 18 + ios/Video/DataStructures/TextTrack.swift | 25 + ios/Video/DataStructures/VideoSource.swift | 31 + ios/Video/Features/RCTPictureInPicture.swift | 75 + ios/Video/Features/RCTPlayerObserver.swift | 212 ++ ios/Video/Features/RCTPlayerOperations.swift | 160 ++ .../Features/RCTResourceLoaderDelegate.swift | 258 +++ .../Features/RCTVideoErrorHandling.swift | 83 + ios/Video/Features/RCTVideoSave.swift | 75 + ios/Video/Features/RCTVideoUtils.swift | 143 ++ ios/Video/RCTSwiftLog/RCTSwiftLog.h | 11 + ios/Video/RCTSwiftLog/RCTSwiftLog.m | 32 + ios/Video/RCTSwiftLog/RCTSwiftLog.swift | 53 + ios/Video/RCTVideo-Bridging-Header.h | 7 + ios/Video/RCTVideo.h | 67 - ios/Video/RCTVideo.m | 2013 ----------------- ios/Video/RCTVideo.swift | 1147 ++++++++++ ios/Video/RCTVideoManager.h | 6 - ios/Video/RCTVideoManager.m | 84 +- ios/Video/RCTVideoManager.swift | 63 + ios/Video/RCTVideoPlayerViewController.h | 20 - ios/Video/RCTVideoPlayerViewController.m | 43 - ios/Video/RCTVideoPlayerViewController.swift | 44 + .../RCTVideoPlayerViewControllerDelegate.h | 7 - ...RCTVideoPlayerViewControllerDelegate.swift | 7 + ios/Video/UIView+FindUIViewController.h | 15 - ios/Video/UIView+FindUIViewController.m | 21 - ios/Video/UIView+FindUIViewController.swift | 18 + ios/VideoCaching/RCTVideoCachingHandler.swift | 81 + react-native-video.podspec | 8 +- 34 files changed, 2661 insertions(+), 2313 deletions(-) create mode 100644 ios/Video/DataStructures/DRMParams.swift create mode 100644 ios/Video/DataStructures/SelectedTrackCriteria.swift create mode 100644 ios/Video/DataStructures/TextTrack.swift create mode 100644 ios/Video/DataStructures/VideoSource.swift create mode 100644 ios/Video/Features/RCTPictureInPicture.swift create mode 100644 ios/Video/Features/RCTPlayerObserver.swift create mode 100644 ios/Video/Features/RCTPlayerOperations.swift create mode 100644 ios/Video/Features/RCTResourceLoaderDelegate.swift create mode 100644 ios/Video/Features/RCTVideoErrorHandling.swift create mode 100644 ios/Video/Features/RCTVideoSave.swift create mode 100644 ios/Video/Features/RCTVideoUtils.swift create mode 100644 ios/Video/RCTSwiftLog/RCTSwiftLog.h create mode 100644 ios/Video/RCTSwiftLog/RCTSwiftLog.m create mode 100644 ios/Video/RCTSwiftLog/RCTSwiftLog.swift create mode 100644 ios/Video/RCTVideo-Bridging-Header.h delete mode 100644 ios/Video/RCTVideo.h delete mode 100644 ios/Video/RCTVideo.m create mode 100644 ios/Video/RCTVideo.swift delete mode 100644 ios/Video/RCTVideoManager.h create mode 100644 ios/Video/RCTVideoManager.swift delete mode 100644 ios/Video/RCTVideoPlayerViewController.h delete mode 100644 ios/Video/RCTVideoPlayerViewController.m create mode 100644 ios/Video/RCTVideoPlayerViewController.swift delete mode 100644 ios/Video/RCTVideoPlayerViewControllerDelegate.h create mode 100644 ios/Video/RCTVideoPlayerViewControllerDelegate.swift delete mode 100644 ios/Video/UIView+FindUIViewController.h delete mode 100644 ios/Video/UIView+FindUIViewController.m create mode 100644 ios/Video/UIView+FindUIViewController.swift create mode 100644 ios/VideoCaching/RCTVideoCachingHandler.swift diff --git a/.gitignore b/.gitignore index d477ec05..2ca6ff0b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ project.xcworkspace .gradle local.properties *.hprof +.project +.settings +.classpath # node.js # diff --git a/examples/basic/yarn.lock b/examples/basic/yarn.lock index 50d8d52e..ec0a6edd 100644 --- a/examples/basic/yarn.lock +++ b/examples/basic/yarn.lock @@ -2093,6 +2093,11 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +eme-encryption-scheme-polyfill@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.3.tgz#2ca6e06480e06cceb5e50efd27943ac46c959878" + integrity sha512-44CNFMsqzHdKHrzWxlS7xZ8KUHn5XutBqpmCuWzNIynmAyFInHrrD3ozv/RvK9ZhgV6QY6Easx8EWAmxteNodg== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -5160,7 +5165,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -5238,11 +5243,11 @@ react-is@^16.8.4, react-is@^16.8.6: integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== "react-native-video@file:../..": - version "5.0.1" + version "5.1.1" dependencies: keymirror "^0.1.1" - prop-types "^15.5.10" - shaka-player "^2.4.4" + prop-types "^15.7.2" + shaka-player "^2.5.9" react-native@0.60.5: version "0.60.5" @@ -5774,10 +5779,12 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -shaka-player@^2.4.4: - version "2.5.2" - resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-2.5.2.tgz#3e639f8f5dfdb1a0afff2ff1f87cddf481f93fe4" - integrity sha512-gpRfXVLAZi33kw9Egop18MkZ/EVRS0soeN6ocR+Btq/J5IoCC56MxwwHzAGna+PgBW9Ox458HRefJ6HB0BoADA== +shaka-player@^2.5.9: + version "2.5.23" + resolved "https://registry.yarnpkg.com/shaka-player/-/shaka-player-2.5.23.tgz#db92d1c6cf2314f0180a2cec11b0e2f2560336f5" + integrity sha512-3MC9k0OXJGw8AZ4n/ZNCZS2yDxx+3as5KgH6Tx4Q5TRboTBBCu6dYPI5vp1DxKeyU12MBN1Zcbs7AKzXv2EnCg== + dependencies: + eme-encryption-scheme-polyfill "^2.0.1" shebang-command@^1.2.0: version "1.2.0" diff --git a/ios/RCTVideo.xcodeproj/project.pbxproj b/ios/RCTVideo.xcodeproj/project.pbxproj index 4e675ac7..e601ea80 100644 --- a/ios/RCTVideo.xcodeproj/project.pbxproj +++ b/ios/RCTVideo.xcodeproj/project.pbxproj @@ -7,14 +7,12 @@ objects = { /* Begin PBXBuildFile section */ - D1107C0A2110259000073188 /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C032110259000073188 /* UIView+FindUIViewController.m */; }; - D1107C0B2110259000073188 /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C032110259000073188 /* UIView+FindUIViewController.m */; }; - D1107C0C2110259000073188 /* RCTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C052110259000073188 /* RCTVideo.m */; }; - D1107C0D2110259000073188 /* RCTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C052110259000073188 /* RCTVideo.m */; }; - D1107C0E2110259000073188 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C062110259000073188 /* RCTVideoManager.m */; }; - D1107C0F2110259000073188 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C062110259000073188 /* RCTVideoManager.m */; }; - D1107C102110259000073188 /* RCTVideoPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C082110259000073188 /* RCTVideoPlayerViewController.m */; }; - D1107C112110259000073188 /* RCTVideoPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1107C082110259000073188 /* RCTVideoPlayerViewController.m */; }; + 0177D39A27170A7A00F5BE18 /* RCTVideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */; }; + 0177D39B27170A7A00F5BE18 /* UIView+FindUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39327170A7A00F5BE18 /* UIView+FindUIViewController.swift */; }; + 0177D39C27170A7A00F5BE18 /* RCTVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */; }; + 0177D39D27170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */; }; + 0177D39E27170A7A00F5BE18 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */; }; + 0177D39F27170A7A00F5BE18 /* RCTVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39927170A7A00F5BE18 /* RCTVideo.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -39,17 +37,18 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01450CB5271D5738005D8F6B /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 01489050272001A100E69940 /* DataStructures */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DataStructures; path = Video/DataStructures; sourceTree = ""; }; + 01489051272001A100E69940 /* Features */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Features; path = Video/Features; sourceTree = ""; }; + 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideoManager.swift; path = Video/RCTVideoManager.swift; sourceTree = ""; }; + 0177D39327170A7A00F5BE18 /* UIView+FindUIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+FindUIViewController.swift"; path = "Video/UIView+FindUIViewController.swift"; sourceTree = ""; }; + 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideoPlayerViewController.swift; path = Video/RCTVideoPlayerViewController.swift; sourceTree = ""; }; + 0177D39527170A7A00F5BE18 /* RCTSwiftLog */ = {isa = PBXFileReference; lastKnownFileType = folder; name = RCTSwiftLog; path = Video/RCTSwiftLog; sourceTree = ""; }; + 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideoPlayerViewControllerDelegate.swift; path = Video/RCTVideoPlayerViewControllerDelegate.swift; sourceTree = ""; }; + 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideoManager.m; path = Video/RCTVideoManager.m; sourceTree = ""; }; + 0177D39827170A7A00F5BE18 /* RCTVideo-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RCTVideo-Bridging-Header.h"; path = "Video/RCTVideo-Bridging-Header.h"; sourceTree = ""; }; + 0177D39927170A7A00F5BE18 /* RCTVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideo.swift; path = Video/RCTVideo.swift; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 641E28441F0EEC8500443AF6 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; - D1107C012110259000073188 /* RCTVideoPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoPlayerViewController.h; path = Video/RCTVideoPlayerViewController.h; sourceTree = ""; }; - D1107C022110259000073188 /* RCTVideoPlayerViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoPlayerViewControllerDelegate.h; path = Video/RCTVideoPlayerViewControllerDelegate.h; sourceTree = ""; }; - D1107C032110259000073188 /* UIView+FindUIViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIView+FindUIViewController.m"; path = "Video/UIView+FindUIViewController.m"; sourceTree = ""; }; - D1107C042110259000073188 /* UIView+FindUIViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIView+FindUIViewController.h"; path = "Video/UIView+FindUIViewController.h"; sourceTree = ""; }; - D1107C052110259000073188 /* RCTVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideo.m; path = Video/RCTVideo.m; sourceTree = ""; }; - D1107C062110259000073188 /* RCTVideoManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideoManager.m; path = Video/RCTVideoManager.m; sourceTree = ""; }; - D1107C072110259000073188 /* RCTVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideo.h; path = Video/RCTVideo.h; sourceTree = ""; }; - D1107C082110259000073188 /* RCTVideoPlayerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTVideoPlayerViewController.m; path = Video/RCTVideoPlayerViewController.m; sourceTree = ""; }; - D1107C092110259000073188 /* RCTVideoManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTVideoManager.h; path = Video/RCTVideoManager.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,18 +87,19 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( - D1107C072110259000073188 /* RCTVideo.h */, - D1107C052110259000073188 /* RCTVideo.m */, - D1107C092110259000073188 /* RCTVideoManager.h */, - D1107C062110259000073188 /* RCTVideoManager.m */, - D1107C012110259000073188 /* RCTVideoPlayerViewController.h */, - D1107C082110259000073188 /* RCTVideoPlayerViewController.m */, - D1107C022110259000073188 /* RCTVideoPlayerViewControllerDelegate.h */, - D1107C042110259000073188 /* UIView+FindUIViewController.h */, - D1107C032110259000073188 /* UIView+FindUIViewController.m */, + 01489050272001A100E69940 /* DataStructures */, + 01489051272001A100E69940 /* Features */, + 0177D39527170A7A00F5BE18 /* RCTSwiftLog */, + 0177D39927170A7A00F5BE18 /* RCTVideo.swift */, + 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */, + 0177D39227170A7A00F5BE18 /* RCTVideoManager.swift */, + 0177D39427170A7A00F5BE18 /* RCTVideoPlayerViewController.swift */, + 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */, + 0177D39327170A7A00F5BE18 /* UIView+FindUIViewController.swift */, + 0177D39827170A7A00F5BE18 /* RCTVideo-Bridging-Header.h */, 134814211AA4EA7D00B7C361 /* Products */, - 641E28441F0EEC8500443AF6 /* libRCTVideo.a */, 49E995712048B4CE00EA7890 /* Frameworks */, + 01450CB5271D5738005D8F6B /* libRCTVideo.a */, ); sourceTree = ""; }; @@ -137,7 +137,7 @@ ); name = "RCTVideo-tvOS"; productName = "RCTVideo-tvOS"; - productReference = 641E28441F0EEC8500443AF6 /* libRCTVideo.a */; + productReference = 01450CB5271D5738005D8F6B /* libRCTVideo.a */; productType = "com.apple.product-type.library.static"; }; /* End PBXNativeTarget section */ @@ -151,9 +151,11 @@ TargetAttributes = { 58B511DA1A9E6C8500147676 = { CreatedOnToolsVersion = 6.1.1; + LastSwiftMigration = 1300; }; 641E28431F0EEC8500443AF6 = { CreatedOnToolsVersion = 8.3.3; + LastSwiftMigration = 1300; ProvisioningStyle = Automatic; }; }; @@ -163,6 +165,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 58B511D21A9E6C8500147676; @@ -181,10 +184,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D1107C0A2110259000073188 /* UIView+FindUIViewController.m in Sources */, - D1107C102110259000073188 /* RCTVideoPlayerViewController.m in Sources */, - D1107C0E2110259000073188 /* RCTVideoManager.m in Sources */, - D1107C0C2110259000073188 /* RCTVideo.m in Sources */, + 0177D39D27170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift in Sources */, + 0177D39C27170A7A00F5BE18 /* RCTVideoPlayerViewController.swift in Sources */, + 0177D39B27170A7A00F5BE18 /* UIView+FindUIViewController.swift in Sources */, + 0177D39F27170A7A00F5BE18 /* RCTVideo.swift in Sources */, + 0177D39E27170A7A00F5BE18 /* RCTVideoManager.m in Sources */, + 0177D39A27170A7A00F5BE18 /* RCTVideoManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -192,10 +197,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D1107C0B2110259000073188 /* UIView+FindUIViewController.m in Sources */, - D1107C112110259000073188 /* RCTVideoPlayerViewController.m in Sources */, - D1107C0F2110259000073188 /* RCTVideoManager.m in Sources */, - D1107C0D2110259000073188 /* RCTVideo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -280,30 +281,39 @@ 58B511F01A9E6C8500147676 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/Vendor/SPTPersistentCache/include/**", ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Video/RCTVideo-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; 58B511F11A9E6C8500147676 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/Vendor/SPTPersistentCache/include/**", ); + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Video/RCTVideo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -312,15 +322,19 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; DEBUG_INFORMATION_FORMAT = dwarf; GCC_NO_COMMON_BLOCKS = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = Debug; @@ -330,16 +344,19 @@ buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_NO_COMMON_BLOCKS = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = RCTVideo; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = Release; diff --git a/ios/Video/DataStructures/DRMParams.swift b/ios/Video/DataStructures/DRMParams.swift new file mode 100644 index 00000000..64add7e0 --- /dev/null +++ b/ios/Video/DataStructures/DRMParams.swift @@ -0,0 +1,30 @@ +struct DRMParams { + let type: String? + let licenseServer: String? + let headers: Dictionary? + let contentId: String? + let certificateUrl: String? + let base64Certificate: Bool? + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = nil + self.licenseServer = nil + self.contentId = nil + self.certificateUrl = nil + self.base64Certificate = nil + self.headers = nil + return + } + self.json = json + self.type = json["type"] as? String + self.licenseServer = json["licenseServer"] as? String + self.contentId = json["contentId"] as? String + self.certificateUrl = json["certificateUrl"] as? String + self.base64Certificate = json["base64Certificate"] as? Bool + self.headers = json["headers"] as? Dictionary + } +} diff --git a/ios/Video/DataStructures/SelectedTrackCriteria.swift b/ios/Video/DataStructures/SelectedTrackCriteria.swift new file mode 100644 index 00000000..7d97b8f2 --- /dev/null +++ b/ios/Video/DataStructures/SelectedTrackCriteria.swift @@ -0,0 +1,18 @@ +struct SelectedTrackCriteria { + let type: String + let value: Any? + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = "" + self.value = nil + return + } + self.json = json + self.type = json["type"] as? String ?? "" + self.value = json["value"] + } +} diff --git a/ios/Video/DataStructures/TextTrack.swift b/ios/Video/DataStructures/TextTrack.swift new file mode 100644 index 00000000..b0bdad80 --- /dev/null +++ b/ios/Video/DataStructures/TextTrack.swift @@ -0,0 +1,25 @@ + +struct TextTrack { + let type: String + let language: String + let title: String + let uri: String + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = "" + self.language = "" + self.title = "" + self.uri = "" + return + } + self.json = json + self.type = json["type"] as? String ?? "" + self.language = json["language"] as? String ?? "" + self.title = json["title"] as? String ?? "" + self.uri = json["uri"] as? String ?? "" + } +} diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift new file mode 100644 index 00000000..20ab7158 --- /dev/null +++ b/ios/Video/DataStructures/VideoSource.swift @@ -0,0 +1,31 @@ + +struct VideoSource { + let type: String? + let uri: String? + let isNetwork: Bool + let isAsset: Bool + let shouldCache: Bool + let requestHeaders: Dictionary? + + let json: NSDictionary? + + init(_ json: NSDictionary!) { + guard json != nil else { + self.json = nil + self.type = nil + self.uri = nil + self.isNetwork = false + self.isAsset = false + self.shouldCache = false + self.requestHeaders = nil + return + } + self.json = json + self.type = json["type"] as? String + self.uri = json["uri"] as? String + self.isNetwork = json["isNetwork"] as? Bool ?? false + self.isAsset = json["isAsset"] as? Bool ?? false + self.shouldCache = json["shouldCache"] as? Bool ?? false + self.requestHeaders = json["requestHeaders"] as? Dictionary + } +} diff --git a/ios/Video/Features/RCTPictureInPicture.swift b/ios/Video/Features/RCTPictureInPicture.swift new file mode 100644 index 00000000..be9812c4 --- /dev/null +++ b/ios/Video/Features/RCTPictureInPicture.swift @@ -0,0 +1,75 @@ +import AVFoundation +import AVKit +import MediaAccessibility +import React +import Foundation + +#if TARGET_OS_IOS +class RCTPictureInPicture: NSObject, AVPictureInPictureControllerDelegate { + private var _onPictureInPictureStatusChanged: RCTDirectEventBlock? + private var _onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock? + private var _restoreUserInterfaceForPIPStopCompletionHandler:((Bool) -> Void)? = nil + private var _pipController:AVPictureInPictureController? + private var _isActive:Bool = false + + init(_ onPictureInPictureStatusChanged: @escaping RCTDirectEventBlock, _ onRestoreUserInterfaceForPictureInPictureStop: @escaping RCTDirectEventBlock) { + _onPictureInPictureStatusChanged = onPictureInPictureStatusChanged + _onRestoreUserInterfaceForPictureInPictureStop = onRestoreUserInterfaceForPictureInPictureStop + } + + func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } + + _onPictureInPictureStatusChanged([ "isActive": NSNumber(value: true)]) + } + + func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + guard let _onPictureInPictureStatusChanged = _onPictureInPictureStatusChanged else { return } + + _onPictureInPictureStatusChanged([ "isActive": NSNumber(value: false)]) + } + + func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { + + assert(_restoreUserInterfaceForPIPStopCompletionHandler == nil, "restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited.") + + guard let _onRestoreUserInterfaceForPictureInPictureStop = _onRestoreUserInterfaceForPictureInPictureStop else { return } + + _onRestoreUserInterfaceForPictureInPictureStop([:]) + + _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler + } + + func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore:Bool) { + guard let _restoreUserInterfaceForPIPStopCompletionHandler = _restoreUserInterfaceForPIPStopCompletionHandler else { return } + _restoreUserInterfaceForPIPStopCompletionHandler(restore) + self._restoreUserInterfaceForPIPStopCompletionHandler = nil + } + + func setupPipController(_ playerLayer: AVPlayerLayer?) { + guard playerLayer != nil && AVPictureInPictureController.isPictureInPictureSupported() else { return } + // Create new controller passing reference to the AVPlayerLayer + _pipController = AVPictureInPictureController(playerLayer:playerLayer!) + _pipController?.delegate = self + } + + func setPictureInPicture(_ isActive:Bool) { + if _isActive == isActive { + return + } + _isActive = isActive + + guard let _pipController = _pipController else { return } + + if _isActive && !_pipController.isPictureInPictureActive { + DispatchQueue.main.async(execute: { + _pipController.startPictureInPicture() + }) + } else if !_isActive && _pipController.isPictureInPictureActive { + DispatchQueue.main.async(execute: { + _pipController.stopPictureInPicture() + }) + } + } +} +#endif diff --git a/ios/Video/Features/RCTPlayerObserver.swift b/ios/Video/Features/RCTPlayerObserver.swift new file mode 100644 index 00000000..d17b27da --- /dev/null +++ b/ios/Video/Features/RCTPlayerObserver.swift @@ -0,0 +1,212 @@ +import AVFoundation +import AVKit +import Foundation + +@objc +protocol RCTPlayerObserverHandlerObjc { + func handleDidFailToFinishPlaying(notification:NSNotification!) + func handlePlaybackStalled(notification:NSNotification!) + func handlePlayerItemDidReachEnd(notification:NSNotification!) + // unused +// func handleAVPlayerAccess(notification:NSNotification!) +} + +protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc { + func handleTimeUpdate(time:CMTime) + func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) + func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) + func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) + func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange) + func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange) + func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange) +} + +class RCTPlayerObserver: NSObject { + + var _handlers: RCTPlayerObserverHandler! + + var player:AVPlayer? { + didSet { + if player == nil { + removePlayerObservers() + removePlayerTimeObserver() + } else { + addPlayerObservers() + addPlayerTimeObserver() + } + } + } + var playerItem:AVPlayerItem? { + didSet { + if playerItem == nil { + removePlayerItemObservers() + } else { + addPlayerItemObservers() + } + } + } + var playerViewController:AVPlayerViewController? { + didSet { + if playerViewController == nil { + removePlayerViewControllerObservers() + } else { + addPlayerViewControllerObservers() + } + } + } + var playerLayer:AVPlayerLayer? { + didSet { + if playerLayer == nil { + removePlayerLayerObserver() + } else { + addPlayerLayerObserver() + } + } + } + + private var _progressUpdateInterval:TimeInterval = 250 + private var _timeObserver:Any? + + private var _playerRateChangeObserver:NSKeyValueObservation? + private var _playerExpernalPlaybackActiveObserver:NSKeyValueObservation? + private var _playerItemStatusObserver:NSKeyValueObservation? + private var _playerPlaybackBufferEmptyObserver:NSKeyValueObservation? + private var _playerPlaybackLikelyToKeepUpObserver:NSKeyValueObservation? + private var _playerTimedMetadataObserver:NSKeyValueObservation? + private var _playerViewControllerReadyForDisplayObserver:NSKeyValueObservation? + private var _playerLayerReadyForDisplayObserver:NSKeyValueObservation? + private var _playerViewControllerOverlayFrameObserver:NSKeyValueObservation? + + deinit { + NotificationCenter.default.removeObserver(_handlers) + } + + func addPlayerObservers() { + guard let player = player else { + return + } + + _playerRateChangeObserver = player.observe(\.rate, changeHandler: _handlers.handlePlaybackRateChange) + _playerExpernalPlaybackActiveObserver = player.observe(\.isExternalPlaybackActive, changeHandler: _handlers.handleExternalPlaybackActiveChange) + } + + func removePlayerObservers() { + _playerRateChangeObserver?.invalidate() + _playerExpernalPlaybackActiveObserver?.invalidate() + } + + func addPlayerItemObservers() { + guard let playerItem = playerItem else { return } + + _playerItemStatusObserver = playerItem.observe(\.status, options: [.new, .old], changeHandler: _handlers.handlePlayerItemStatusChange) + _playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty) + _playerPlaybackLikelyToKeepUpObserver = playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .old], changeHandler: _handlers.handlePlaybackLikelyToKeepUp) + _playerTimedMetadataObserver = playerItem.observe(\.timedMetadata, options: [.new], changeHandler: _handlers.handleTimeMetadataChange) + } + + func removePlayerItemObservers() { + _playerItemStatusObserver?.invalidate() + _playerPlaybackBufferEmptyObserver?.invalidate() + _playerPlaybackLikelyToKeepUpObserver?.invalidate() + _playerTimedMetadataObserver?.invalidate() + } + + func addPlayerViewControllerObservers() { + guard let playerViewController = playerViewController else { return } + + _playerViewControllerReadyForDisplayObserver = playerViewController.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) + + _playerViewControllerOverlayFrameObserver = playerViewController.contentOverlayView?.observe(\.frame, options: [.new, .old], changeHandler: _handlers.handleViewControllerOverlayViewFrameChange) + } + + func removePlayerViewControllerObservers() { + _playerViewControllerReadyForDisplayObserver?.invalidate() + _playerViewControllerOverlayFrameObserver?.invalidate() + } + + func addPlayerLayerObserver() { + _playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) + } + + func removePlayerLayerObserver() { + _playerLayerReadyForDisplayObserver?.invalidate() + } + + func addPlayerTimeObserver() { + removePlayerTimeObserver() + let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000 + // @see endScrubbing in AVPlayerDemoPlaybackViewController.m + // of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html + _timeObserver = player?.addPeriodicTimeObserver( + forInterval: CMTimeMakeWithSeconds(progressUpdateIntervalMS, preferredTimescale: Int32(NSEC_PER_SEC)), + queue:nil, + using:_handlers.handleTimeUpdate + ) + } + + /* Cancels the previously registered time observer. */ + func removePlayerTimeObserver() { + if let timeObserver = _timeObserver { + player?.removeTimeObserver(timeObserver) + _timeObserver = nil + } + } + + func addTimeObserverIfNotSet() { + if (_timeObserver == nil) { + addPlayerTimeObserver() + } + } + + func replaceTimeObserverIfSet(_ newUpdateInterval:Float64? = nil) { + if let newUpdateInterval = newUpdateInterval { + _progressUpdateInterval = newUpdateInterval + } + if (_timeObserver != nil) { + addPlayerTimeObserver() + } + } + + func attachPlayerEventListeners() { + + NotificationCenter.default.removeObserver(_handlers, + name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object:player?.currentItem) + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handlePlayerItemDidReachEnd(notification:)), + name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, + object:player?.currentItem) + + NotificationCenter.default.removeObserver(_handlers, + name:NSNotification.Name.AVPlayerItemPlaybackStalled, + object:nil) + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handlePlaybackStalled(notification:)), + name:NSNotification.Name.AVPlayerItemPlaybackStalled, + object:nil) + NotificationCenter.default.removeObserver(_handlers, + name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, + object:nil) + NotificationCenter.default.addObserver(_handlers, + selector:#selector(RCTPlayerObserverHandler.handleDidFailToFinishPlaying(notification:)), + name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, + object:nil) + // Unused +// NotificationCenter.default.removeObserver(_handlers, +// name:NSNotification.Name.AVPlayerItemNewAccessLogEntry, +// object:nil) +// NotificationCenter.default.addObserver(_handlers, +// selector: #selector(RCTPlayerObserverHandler.handleAVPlayerAccess(notification:)), +// name:NSNotification.Name.AVPlayerItemNewAccessLogEntry, +// object:nil) + + } + + func clearPlayer() { + player = nil + playerItem = nil + NotificationCenter.default.removeObserver(_handlers) + } +} diff --git a/ios/Video/Features/RCTPlayerOperations.swift b/ios/Video/Features/RCTPlayerOperations.swift new file mode 100644 index 00000000..8d70f46d --- /dev/null +++ b/ios/Video/Features/RCTPlayerOperations.swift @@ -0,0 +1,160 @@ +import AVFoundation +import MediaAccessibility + +let RCTVideoUnset = -1 + +/*! + * Collection of mutating functions + */ +enum RCTPlayerOperations { + + static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) { + let type = criteria?.type + let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player) + + // The first few tracks will be audio & video track + let firstTextIndex:Int = 0 + for firstTextIndex in 0..<(player?.currentItem?.tracks.count ?? 0) { + if player?.currentItem?.tracks[firstTextIndex].assetTrack?.hasMediaCharacteristic(.legible) ?? false { + break + } + } + + var selectedTrackIndex:Int = RCTVideoUnset + + if (type == "disabled") { + // Do nothing. We want to ensure option is nil + } else if (type == "language") { + let selectedValue = criteria?.value as? String + for i in 0.. index { + selectedTrackIndex = index + } + } + } + + // in the situation that a selected text track is not available (eg. specifies a textTrack not available) + if (type != "disabled") && selectedTrackIndex == RCTVideoUnset { + let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) as! CFArray + let captionSettings = captioningMediaCharacteristics as? [AnyHashable] + if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) { + selectedTrackIndex = 0 // If we can't find a match, use the first available track + let systemLanguage = NSLocale.preferredLanguages.first + for i in 0.. index { + mediaOption = group.options[index] + } + } + } else { // default. invalid type or "system" + player?.currentItem?.selectMediaOptionAutomatically(in: group) + return + } + + // If a match isn't found, option will be nil and text tracks will be disabled + player?.currentItem?.select(mediaOption, in:group) + } + + static func setMediaSelectionTrackForCharacteristic(player:AVPlayer?, characteristic:AVMediaCharacteristic, criteria:SelectedTrackCriteria?) { + let type = criteria?.type + let group:AVMediaSelectionGroup! = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: characteristic) + var mediaOption:AVMediaSelectionOption! + + if (type == "disabled") { + // Do nothing. We want to ensure option is nil + } else if (type == "language") || (type == "title") { + let value = criteria?.value as? String + for i in 0.. index { + mediaOption = group.options[index] + } + } + } else if let group = group { // default. invalid type or "system" + player?.currentItem?.selectMediaOptionAutomatically(in: group) + return + } + + if let group = group { + // If a match isn't found, option will be nil and text tracks will be disabled + player?.currentItem?.select(mediaOption, in:group) + } + + } +} diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift new file mode 100644 index 00000000..5ead7fef --- /dev/null +++ b/ios/Video/Features/RCTResourceLoaderDelegate.swift @@ -0,0 +1,258 @@ +import AVFoundation + +class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { + + private var _loadingRequest:AVAssetResourceLoadingRequest? + private var _requestingCertificate:Bool = false + private var _requestingCertificateErrored:Bool = false + private var _drm: DRMParams? + private var _reactTag: NSNumber? + private var _onVideoError: RCTDirectEventBlock? + private var _onGetLicense: RCTDirectEventBlock? + + + init( + asset: AVURLAsset, + drm: DRMParams?, + onVideoError: RCTDirectEventBlock?, + onGetLicense: RCTDirectEventBlock?, + reactTag: NSNumber + ) { + super.init() + let queue = DispatchQueue(label: "assetQueue") + asset.resourceLoader.setDelegate(self, queue: queue) + _reactTag = reactTag + _onVideoError = onVideoError + _onGetLicense = onGetLicense + _drm = drm + } + + deinit { + _loadingRequest?.finishLoading() + } + + func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForRenewalOfRequestedResource renewalRequest:AVAssetResourceRenewalRequest) -> Bool { + return loadingRequestHandling(renewalRequest) + } + + func resourceLoader(_ resourceLoader:AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest:AVAssetResourceLoadingRequest) -> Bool { + return loadingRequestHandling(loadingRequest) + } + + func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) { + NSLog("didCancelLoadingRequest") + } + + func base64DataFromBase64String(base64String:String?) -> Data? { + if let base64String = base64String { + return Data(base64Encoded:base64String) + } + return nil + } + + func setLicenseResult(_ license:String!) { + guard let respondData = self.base64DataFromBase64String(base64String: license), + let _loadingRequest = _loadingRequest else { + setLicenseResultError("No data from JS license response") + return + } + let dataRequest:AVAssetResourceLoadingDataRequest! = _loadingRequest.dataRequest + dataRequest.respond(with: respondData) + _loadingRequest.finishLoading() + } + + func setLicenseResultError(_ error:String!) { + if _loadingRequest != nil { + self.finishLoadingWithError(error: RCTVideoErrorHandler.fromJSPart(error)) + } + } + + func finishLoadingWithError(error:NSError!) -> Bool { + if let _loadingRequest = _loadingRequest, let error = error { + let licenseError:NSError! = error + _loadingRequest.finishLoading(with: licenseError) + + _onVideoError?([ + "error": [ + "code": NSNumber(value: error.code), + "localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription, + "localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "", + "localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "", + "domain": (error as NSError).domain + ], + "target": _reactTag + ]) + + } + return false + } + + func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { + if _requestingCertificate { + return true + } else if _requestingCertificateErrored { + return false + } + _loadingRequest = loadingRequest + + let url = loadingRequest.request.url + guard let _drm = _drm else { + return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) + } + + var contentId:String! + let contentIdOverride:String! = _drm.contentId + if contentIdOverride != nil { + contentId = contentIdOverride + } else if (_onGetLicense != nil) { + contentId = url?.host + } else { + contentId = url?.absoluteString.replacingOccurrences(of: "skd://", with:"") + } + + let drmType:String! = _drm.type + guard drmType == "fairplay" else { + return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) + } + + let certificateStringUrl:String! = _drm.certificateUrl + guard let certificateStringUrl = certificateStringUrl, let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { + return finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateURL) + } + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + var certificateData:Data? + if (_drm.base64Certificate != nil) { + certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters) + } else { + do { + certificateData = try Data(contentsOf: certificateURL) + } catch {} + } + + guard let certificateData = certificateData else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData) + self._requestingCertificateErrored = true + return + } + + var contentIdData:NSData! + if self._onGetLicense != nil { + contentIdData = contentId.data(using: .utf8) as NSData? + } else { + contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8)) + } + + let dataRequest:AVAssetResourceLoadingDataRequest! = loadingRequest.dataRequest + guard dataRequest != nil else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData) + self._requestingCertificateErrored = true + return + } + + var spcError:NSError! + var spcData: Data? + do { + spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil) + } catch let spcError { + print("SPC error") + } + // Request CKC to the server + var licenseServer:String! = _drm.licenseServer + if spcError != nil { + self.finishLoadingWithError(error: spcError) + self._requestingCertificateErrored = true + } + + guard spcData != nil else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noSPC) + self._requestingCertificateErrored = true + return + } + + // js client has a onGetLicense callback and will handle license fetching + if let _onGetLicense = self._onGetLicense { + let base64Encoded = spcData?.base64EncodedString(options: []) + self._requestingCertificate = true + if licenseServer == nil { + licenseServer = "" + } + _onGetLicense(["licenseUrl": licenseServer, + "contentId": contentId, + "spcBase64": base64Encoded, + "target": self._reactTag]) + + + } else if licenseServer != nil { + self.fetchLicense( + licenseServer: licenseServer, + spcData: spcData, + contentId: contentId, + dataRequest: dataRequest + ) + } + } + return true + } + + func fetchLicense( + licenseServer: String, + spcData: Data?, + contentId: String, + dataRequest: AVAssetResourceLoadingDataRequest! + ) { + var request = URLRequest(url: URL(string: licenseServer)!) + request.httpMethod = "POST" + + // HEADERS + if let headers = _drm?.headers { + for item in headers { + guard let key = item.key as? String, let value = item.value as? String else { + continue + } + request.setValue(value, forHTTPHeaderField: key) + } + } + + if (_onGetLicense != nil) { + request.httpBody = spcData + } else { + let spcEncoded = spcData?.base64EncodedString(options: []) + let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String + let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId) + let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true) + request.httpBody = postData + } + + let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ [weak self] (data:Data!,response:URLResponse!,error:Error!) in + guard let self = self else { return } + let httpResponse:HTTPURLResponse! = response as! HTTPURLResponse + guard error == nil else { + print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") + self.finishLoadingWithError(error: error as NSError?) + self._requestingCertificateErrored = true + return + } + guard httpResponse.statusCode == 200 else { + print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") + self.finishLoadingWithError(error: RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode)) + self._requestingCertificateErrored = true + return + } + + guard data != nil else { + self.finishLoadingWithError(error: RCTVideoErrorHandler.noDataFromLicenseRequest) + self._requestingCertificateErrored = true + return + } + + if (self._onGetLicense != nil) { + dataRequest.respond(with: data) + } else if let decodedData = Data(base64Encoded: data, options: []) { + dataRequest.respond(with: decodedData) + } + self._loadingRequest?.finishLoading() + }) + postDataTask.resume() + } +} diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift new file mode 100644 index 00000000..2b65a969 --- /dev/null +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -0,0 +1,83 @@ +enum RCTVideoError : Int { + case fromJSPart + case licenseRequestNotOk + case noDataFromLicenseRequest + case noSPC + case noDataRequest + case noCertificateData + case noCertificateURL + case noFairplayDRM + case noDRMData +} + +enum RCTVideoErrorHandler { + + static let noDRMData: NSError = NSError( + domain: "RCTVideo", + code: RCTVideoError.noDRMData.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No drm object found.", + NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?" + ]) + + static let noCertificateURL: NSError = NSError( + domain: "RCTVideo", + code: RCTVideoError.noCertificateURL.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM License.", + NSLocalizedFailureReasonErrorKey: "No certificate URL has been found.", + NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?" + ]) + + static let noCertificateData: NSError = NSError( + domain: "RCTVideo", + code: RCTVideoError.noCertificateData.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No certificate data obtained from the specificied url.", + NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?" + ]) + + static let noSPC:NSError! = NSError( + domain: "RCTVideo", + code: RCTVideoError.noSPC.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining license.", + NSLocalizedFailureReasonErrorKey: "No spc received.", + NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config." + ]) + + static let noDataFromLicenseRequest:NSError! = NSError( + domain: "RCTVideo", + code: RCTVideoError.noDataFromLicenseRequest.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No data received from the license server.", + NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?." + ]) + + static func licenseRequestNotOk(_ statusCode: Int) -> NSError { + return NSError( + domain: "RCTVideo", + code: RCTVideoError.licenseRequestNotOk.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining license.", + NSLocalizedFailureReasonErrorKey: String( + format:"License server responded with status code %li", + (statusCode) + ), + NSLocalizedRecoverySuggestionErrorKey: "Did you send the correct data to the license Server? Is the server ok?" + ]) + } + + static func fromJSPart(_ error: String) -> NSError { + return NSError(domain: "RCTVideo", + code: RCTVideoError.fromJSPart.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: error, + NSLocalizedFailureReasonErrorKey: error, + NSLocalizedRecoverySuggestionErrorKey: error + ]) + } +} diff --git a/ios/Video/Features/RCTVideoSave.swift b/ios/Video/Features/RCTVideoSave.swift new file mode 100644 index 00000000..ff8155ec --- /dev/null +++ b/ios/Video/Features/RCTVideoSave.swift @@ -0,0 +1,75 @@ +import AVFoundation + +enum RCTVideoSave { + + static func save( + options:NSDictionary!, + resolve: @escaping RCTPromiseResolveBlock, + reject:@escaping RCTPromiseRejectBlock, + + playerItem: AVPlayerItem? + ) { + let asset:AVAsset! = playerItem?.asset + + guard asset != nil else { + reject("ERROR_ASSET_NIL", "Asset is nil", nil) + return + } + + guard let exportSession = AVAssetExportSession(asset: asset, presetName:AVAssetExportPresetHighestQuality) else { + reject("ERROR_COULD_NOT_CREATE_EXPORT_SESSION", "Could not create export session", nil) + return + } + var path:String! = nil + path = RCTVideoSave.generatePathInDirectory( + directory: URL(fileURLWithPath: RCTVideoSave.cacheDirectoryPath() ?? "").appendingPathComponent("Videos").path, + withExtension: ".mp4") + let url:NSURL! = NSURL.fileURL(withPath: path) as NSURL + exportSession.outputFileType = AVFileType.mp4 + exportSession.outputURL = url as URL? + exportSession.videoComposition = playerItem?.videoComposition + exportSession.shouldOptimizeForNetworkUse = true + exportSession.exportAsynchronously(completionHandler: { + + switch (exportSession.status) { + case .failed: + reject("ERROR_COULD_NOT_EXPORT_VIDEO", "Could not export video", exportSession.error) + break + case .cancelled: + reject("ERROR_EXPORT_SESSION_CANCELLED", "Export session was cancelled", exportSession.error) + break + default: + resolve(["uri": url.absoluteString]) + break + } + + }) + } + + static func generatePathInDirectory(directory: String?, withExtension `extension`: String?) -> String? { + let fileName = UUID().uuidString + (`extension` ?? "") + RCTVideoSave.ensureDirExists(withPath: directory) + return URL(fileURLWithPath: directory ?? "").appendingPathComponent(fileName).path + } + + static func cacheDirectoryPath() -> String? { + let array = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).map(\.path) + return array[0] + } + + static func ensureDirExists(withPath path: String?) -> Bool { + var isDir: ObjCBool = false + var error: Error? + let exists = FileManager.default.fileExists(atPath: path ?? "", isDirectory: &isDir) + if !(exists && isDir.boolValue) { + do { + try FileManager.default.createDirectory(atPath: path ?? "", withIntermediateDirectories: true, attributes: nil) + } catch { + } + if error != nil { + return false + } + } + return true + } +} diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift new file mode 100644 index 00000000..51a6aae7 --- /dev/null +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -0,0 +1,143 @@ +import AVFoundation + +/*! + * Collection of pure functions + */ +enum RCTVideoUtils { + + /*! + * Calculates and returns the playable duration of the current player item using its loaded time ranges. + * + * \returns The playable duration of the current player item in seconds. + */ + static func calculatePlayableDuration(_ player:AVPlayer?) -> NSNumber { + guard let player = player, + let video:AVPlayerItem = player.currentItem, + video.status == AVPlayerItem.Status.readyToPlay else { + return 0 + } + + var effectiveTimeRange:CMTimeRange? + for (_, value) in video.loadedTimeRanges.enumerated() { + let timeRange:CMTimeRange = value.timeRangeValue + if CMTimeRangeContainsTime(timeRange, time: video.currentTime()) { + effectiveTimeRange = timeRange + break + } + } + + if let effectiveTimeRange = effectiveTimeRange { + let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) + if playableDuration > 0 { + return playableDuration as NSNumber + } + } + + return 0 + } + + static func urlFilePath(filepath:NSString!) -> NSURL! { + if filepath.contains("file://") { + return NSURL(string: filepath as String) + } + + // if no file found, check if the file exists in the Document directory + let paths:[String]! = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) + var relativeFilePath:String! = filepath.lastPathComponent + // the file may be multiple levels below the documents directory + let fileComponents:[String]! = filepath.components(separatedBy: "Documents/") + if fileComponents.count > 1 { + relativeFilePath = fileComponents[1] + } + + let path:String! = (paths.first! as NSString).appendingPathComponent(relativeFilePath) + if FileManager.default.fileExists(atPath: path) { + return NSURL.fileURL(withPath: path) as NSURL + } + return nil + } + + static func playerItemSeekableTimeRange(_ player:AVPlayer?) -> CMTimeRange { + if let playerItem = player?.currentItem, + playerItem.status == .readyToPlay, + let firstItem = playerItem.seekableTimeRanges.first { + return firstItem.timeRangeValue + } + + return (CMTimeRange.zero) + } + + static func playerItemDuration(_ player:AVPlayer?) -> CMTime { + if let playerItem = player?.currentItem, + playerItem.status == .readyToPlay { + return(playerItem.duration) + } + + return(CMTime.invalid) + } + + static func calculateSeekableDuration(_ player:AVPlayer?) -> NSNumber { + let timeRange:CMTimeRange = RCTVideoUtils.playerItemSeekableTimeRange(player) + if CMTIME_IS_NUMERIC(timeRange.duration) + { + return NSNumber(value: CMTimeGetSeconds(timeRange.duration)) + } + return 0 + } + + static func getAudioTrackInfo(_ player:AVPlayer?) -> [AnyObject]! { + guard let player = player else { + return [] + } + + let audioTracks:NSMutableArray! = NSMutableArray() + let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) + for i in 0..<(group?.options.count ?? 0) { + let currentOption = group?.options[i] + var title = "" + let values = currentOption?.commonMetadata.map(\.value) + if (values?.count ?? 0) > 0, let value = values?[0] { + title = value as! String + } + let language:String! = currentOption?.extendedLanguageTag ?? "" + let audioTrack = [ + "index": NSNumber(value: i), + "title": title, + "language": language + ] as [String : Any] + audioTracks.add(audioTrack) + } + return audioTracks as [AnyObject]? + } + + static func getTextTrackInfo(_ player:AVPlayer?) -> [TextTrack]! { + guard let player = player else { + return [] + } + + // if streaming video, we extract the text tracks + var textTracks:[TextTrack] = [] + let group = player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) + for i in 0..<(group?.options.count ?? 0) { + let currentOption = group?.options[i] + var title = "" + let values = currentOption?.commonMetadata.map(\.value) + if (values?.count ?? 0) > 0, let value = values?[0] { + title = value as! String + } + let language:String! = currentOption?.extendedLanguageTag ?? "" + let textTrack = TextTrack([ + "index": NSNumber(value: i), + "title": title, + "language": language + ]) + textTracks.append(textTrack) + } + return textTracks + } + + // UNUSED + static func getCurrentTime(playerItem:AVPlayerItem?) -> Float { + return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero)) + } +} diff --git a/ios/Video/RCTSwiftLog/RCTSwiftLog.h b/ios/Video/RCTSwiftLog/RCTSwiftLog.h new file mode 100644 index 00000000..8f9deab7 --- /dev/null +++ b/ios/Video/RCTSwiftLog/RCTSwiftLog.h @@ -0,0 +1,11 @@ +#import + +@interface RCTSwiftLog : NSObject + ++ (void)error:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)warn:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)info:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)log:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; ++ (void)trace:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + +@end diff --git a/ios/Video/RCTSwiftLog/RCTSwiftLog.m b/ios/Video/RCTSwiftLog/RCTSwiftLog.m new file mode 100644 index 00000000..4e153846 --- /dev/null +++ b/ios/Video/RCTSwiftLog/RCTSwiftLog.m @@ -0,0 +1,32 @@ +#import + +#import "RCTSwiftLog.h" + +@implementation RCTSwiftLog + ++ (void)info:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)warn:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelWarning, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)error:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelError, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)log:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)trace:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message); +} + +@end diff --git a/ios/Video/RCTSwiftLog/RCTSwiftLog.swift b/ios/Video/RCTSwiftLog/RCTSwiftLog.swift new file mode 100644 index 00000000..ec39d9b1 --- /dev/null +++ b/ios/Video/RCTSwiftLog/RCTSwiftLog.swift @@ -0,0 +1,53 @@ +// +// RCTLog.swift +// WebViewExample +// +// Created by Jimmy Dee on 4/5/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +/* + * Under at least some conditions, output from NSLog has been unavailable in the RNBranch module. + * Hence that module uses the RCTLog macros from . The React logger is nicer than + * NSLog anyway, since it provides log levels with runtime filtering, file and line context and + * an identifier for the thread that logged the message. + * + * This wrapper lets you use functions with the same name in Swift. For example: + * + * RCTLogInfo("application launched") + * + * generates + * + * 2017-04-06 12:31:09.611 [info][tid:main][AppDelegate.swift:18] application launched + * + * This is currently part of this sample app. There may be some issues integrating it into an + * Objective-C library, either react-native-branch or react-native itself, but it may find its + * way into one or the other eventually. Feel free to reuse it as desired. + */ + +func RCTLogError(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.error(message, file: file, line: line) +} + +func RCTLogWarn(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.warn(message, file: file, line: line) +} + +func RCTLogInfo(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.info(message, file: file, line: line) +} + +func RCTLog(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.log(message, file: file, line: line) +} + +func RCTLogTrace(_ message: String, _ file: String=#file, _ line: UInt=#line) { + RCTSwiftLog.trace(message, file: file, line: line) +} + +func DebugLog(_ message: String) { +#if DEBUG + print(message) +#endif +} + diff --git a/ios/Video/RCTVideo-Bridging-Header.h b/ios/Video/RCTVideo-Bridging-Header.h new file mode 100644 index 00000000..ad6089be --- /dev/null +++ b/ios/Video/RCTVideo-Bridging-Header.h @@ -0,0 +1,7 @@ +#import +#import "RCTSwiftLog.h" + +#if __has_include() +#import "RCTVideoCache.h" +#endif + diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h deleted file mode 100644 index 6fee2996..00000000 --- a/ios/Video/RCTVideo.h +++ /dev/null @@ -1,67 +0,0 @@ -#import -#import "AVKit/AVKit.h" -#import "UIView+FindUIViewController.h" -#import "RCTVideoPlayerViewController.h" -#import "RCTVideoPlayerViewControllerDelegate.h" -#import -#import - -#if __has_include() -#import -#import -#import -#endif - -@class RCTEventDispatcher; -#if __has_include() -@interface RCTVideo : UIView -#elif TARGET_OS_TV -@interface RCTVideo : UIView -#else -@interface RCTVideo : UIView -#endif - -@property (nonatomic, copy) RCTDirectEventBlock onVideoLoadStart; -@property (nonatomic, copy) RCTDirectEventBlock onVideoLoad; -@property (nonatomic, copy) RCTDirectEventBlock onVideoBuffer; -@property (nonatomic, copy) RCTDirectEventBlock onVideoError; -@property (nonatomic, copy) RCTDirectEventBlock onVideoProgress; -@property (nonatomic, copy) RCTDirectEventBlock onBandwidthUpdate; -@property (nonatomic, copy) RCTDirectEventBlock onVideoSeek; -@property (nonatomic, copy) RCTDirectEventBlock onVideoEnd; -@property (nonatomic, copy) RCTDirectEventBlock onTimedMetadata; -@property (nonatomic, copy) RCTDirectEventBlock onVideoAudioBecomingNoisy; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillPresent; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidPresent; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillDismiss; -@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidDismiss; -@property (nonatomic, copy) RCTDirectEventBlock onReadyForDisplay; -@property (nonatomic, copy) RCTDirectEventBlock onPlaybackStalled; -@property (nonatomic, copy) RCTDirectEventBlock onPlaybackResume; -@property (nonatomic, copy) RCTDirectEventBlock onPlaybackRateChange; -@property (nonatomic, copy) RCTDirectEventBlock onVideoExternalPlaybackChange; -@property (nonatomic, copy) RCTDirectEventBlock onPictureInPictureStatusChanged; -@property (nonatomic, copy) RCTDirectEventBlock onRestoreUserInterfaceForPictureInPictureStop; -@property (nonatomic, copy) RCTDirectEventBlock onGetLicense; - -typedef NS_ENUM(NSInteger, RCTVideoError) { - RCTVideoErrorFromJSPart, - RCTVideoErrorLicenseRequestNotOk, - RCTVideoErrorNoDataFromLicenseRequest, - RCTVideoErrorNoSPC, - RCTVideoErrorNoDataRequest, - RCTVideoErrorNoCertificateData, - RCTVideoErrorNoCertificateURL, - RCTVideoErrorNoFairplayDRM, - RCTVideoErrorNoDRMData -}; - -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - -- (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem; - -- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; -- (void)setLicenseResult:(NSString * )license; -- (BOOL)setLicenseResultError:(NSString * )error; - -@end diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m deleted file mode 100644 index 989167a6..00000000 --- a/ios/Video/RCTVideo.m +++ /dev/null @@ -1,2013 +0,0 @@ -#import -#import "RCTVideo.h" -#import -#import -#import -#include -#include - -static NSString *const statusKeyPath = @"status"; -static NSString *const playbackLikelyToKeepUpKeyPath = @"playbackLikelyToKeepUp"; -static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; -static NSString *const readyForDisplayKeyPath = @"readyForDisplay"; -static NSString *const playbackRate = @"rate"; -static NSString *const timedMetadata = @"timedMetadata"; -static NSString *const externalPlaybackActive = @"externalPlaybackActive"; - -static int const RCTVideoUnset = -1; - -#ifdef DEBUG - #define DebugLog(...) NSLog(__VA_ARGS__) -#else - #define DebugLog(...) (void)0 -#endif - -@implementation RCTVideo -{ - AVPlayer *_player; - AVPlayerItem *_playerItem; - NSDictionary *_source; - BOOL _playerItemObserversSet; - BOOL _playerBufferEmpty; - AVPlayerLayer *_playerLayer; - BOOL _playerLayerObserverSet; - RCTVideoPlayerViewController *_playerViewController; - NSURL *_videoURL; - BOOL _requestingCertificate; - BOOL _requestingCertificateErrored; - - /* DRM */ - NSDictionary *_drm; - AVAssetResourceLoadingRequest *_loadingRequest; - - /* Required to publish events */ - RCTEventDispatcher *_eventDispatcher; - BOOL _playbackRateObserverRegistered; - BOOL _isExternalPlaybackActiveObserverRegistered; - BOOL _videoLoadStarted; - - bool _pendingSeek; - float _pendingSeekTime; - float _lastSeekTime; - - /* For sending videoProgress events */ - Float64 _progressUpdateInterval; - BOOL _controls; - id _timeObserver; - - /* Keep track of any modifiers, need to be applied after each play */ - float _volume; - float _rate; - float _maxBitRate; - - BOOL _automaticallyWaitsToMinimizeStalling; - BOOL _muted; - BOOL _paused; - BOOL _repeat; - BOOL _allowsExternalPlayback; - NSArray * _textTracks; - NSDictionary * _selectedTextTrack; - NSDictionary * _selectedAudioTrack; - BOOL _playbackStalled; - BOOL _playInBackground; - BOOL _preventsDisplaySleepDuringVideoPlayback; - float _preferredForwardBufferDuration; - BOOL _playWhenInactive; - BOOL _pictureInPicture; - NSString * _ignoreSilentSwitch; - NSString * _mixWithOthers; - NSString * _resizeMode; - BOOL _fullscreen; - BOOL _fullscreenAutorotate; - NSString * _fullscreenOrientation; - BOOL _fullscreenPlayerPresented; - NSString *_filterName; - BOOL _filterEnabled; - UIViewController * _presentingViewController; -#if __has_include() - RCTVideoCache * _videoCache; -#endif -#if TARGET_OS_IOS - void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL); - AVPictureInPictureController *_pipController; -#endif -} - -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher -{ - if ((self = [super init])) { - _eventDispatcher = eventDispatcher; - _automaticallyWaitsToMinimizeStalling = YES; - _playbackRateObserverRegistered = NO; - _isExternalPlaybackActiveObserverRegistered = NO; - _playbackStalled = NO; - _rate = 1.0; - _volume = 1.0; - _resizeMode = @"AVLayerVideoGravityResizeAspectFill"; - _fullscreenAutorotate = YES; - _fullscreenOrientation = @"all"; - _pendingSeek = false; - _pendingSeekTime = 0.0f; - _lastSeekTime = 0.0f; - _progressUpdateInterval = 250; - _controls = NO; - _playerBufferEmpty = YES; - _playInBackground = false; - _preventsDisplaySleepDuringVideoPlayback = true; - _preferredForwardBufferDuration = 0.0f; - _allowsExternalPlayback = YES; - _playWhenInactive = false; - _pictureInPicture = false; - _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey - _mixWithOthers = @"inherit"; // inherit, mix, duck -#if TARGET_OS_IOS - _restoreUserInterfaceForPIPStopCompletionHandler = NULL; -#endif -#if __has_include() - _videoCache = [RCTVideoCache sharedInstance]; -#endif - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(audioRouteChanged:) - name:AVAudioSessionRouteChangeNotification - object:nil]; - } - - return self; -} - -- (RCTVideoPlayerViewController*)createPlayerViewController:(AVPlayer*)player - withPlayerItem:(AVPlayerItem*)playerItem { - RCTVideoPlayerViewController* viewController = [[RCTVideoPlayerViewController alloc] init]; - viewController.showsPlaybackControls = YES; - viewController.rctDelegate = self; - viewController.preferredOrientation = _fullscreenOrientation; - - viewController.view.frame = self.bounds; - viewController.player = player; - return viewController; -} - -/* --------------------------------------------------------- - ** Get the duration for a AVPlayerItem. - ** ------------------------------------------------------- */ - -- (CMTime)playerItemDuration -{ - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return([playerItem duration]); - } - - return(kCMTimeInvalid); -} - -- (CMTimeRange)playerItemSeekableTimeRange -{ - AVPlayerItem *playerItem = [_player currentItem]; - if (playerItem.status == AVPlayerItemStatusReadyToPlay) - { - return [playerItem seekableTimeRanges].firstObject.CMTimeRangeValue; - } - - return (kCMTimeRangeZero); -} - --(void)addPlayerTimeObserver -{ - const Float64 progressUpdateIntervalMS = _progressUpdateInterval / 1000; - // @see endScrubbing in AVPlayerDemoPlaybackViewController.m - // of https://developer.apple.com/library/ios/samplecode/AVPlayerDemo/Introduction/Intro.html - __weak RCTVideo *weakSelf = self; - _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(progressUpdateIntervalMS, NSEC_PER_SEC) - queue:NULL - usingBlock:^(CMTime time) { [weakSelf sendProgressUpdate]; } - ]; -} - -/* Cancels the previously registered time observer. */ --(void)removePlayerTimeObserver -{ - if (_timeObserver) - { - [_player removeTimeObserver:_timeObserver]; - _timeObserver = nil; - } -} - -#pragma mark - Progress - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self removePlayerLayer]; - [self removePlayerItemObservers]; - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - [_player removeObserver:self forKeyPath:externalPlaybackActive context: nil]; -} - -#pragma mark - App lifecycle handlers - -- (void)applicationWillResignActive:(NSNotification *)notification -{ - if (_playInBackground || _playWhenInactive || _paused) return; - - [_player pause]; - [_player setRate:0.0]; -} - -- (void)applicationDidEnterBackground:(NSNotification *)notification -{ - if (_playInBackground) { - // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html - [_playerLayer setPlayer:nil]; - [_playerViewController setPlayer:nil]; - } -} - -- (void)applicationWillEnterForeground:(NSNotification *)notification -{ - [self applyModifiers]; - if (_playInBackground) { - [_playerLayer setPlayer:_player]; - [_playerViewController setPlayer:_player]; - } -} - -#pragma mark - Audio events - -- (void)audioRouteChanged:(NSNotification *)notification -{ - NSNumber *reason = [[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey]; - NSNumber *previousRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey]; - if (reason.unsignedIntValue == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { - self.onVideoAudioBecomingNoisy(@{@"target": self.reactTag}); - } -} - -#pragma mark - Progress - -- (void)sendProgressUpdate -{ - AVPlayerItem *video = [_player currentItem]; - if (video == nil || video.status != AVPlayerItemStatusReadyToPlay) { - return; - } - - CMTime playerDuration = [self playerItemDuration]; - if (CMTIME_IS_INVALID(playerDuration)) { - return; - } - - CMTime currentTime = _player.currentTime; - NSDate *currentPlaybackTime = _player.currentItem.currentDate; - const Float64 duration = CMTimeGetSeconds(playerDuration); - const Float64 currentTimeSecs = CMTimeGetSeconds(currentTime); - - [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTVideo_progress" object:nil userInfo:@{@"progress": [NSNumber numberWithDouble: currentTimeSecs / duration]}]; - - if( currentTimeSecs >= 0 && self.onVideoProgress) { - self.onVideoProgress(@{ - @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(currentTime)], - @"playableDuration": [self calculatePlayableDuration], - @"atValue": [NSNumber numberWithLongLong:currentTime.value], - @"atTimescale": [NSNumber numberWithInt:currentTime.timescale], - @"currentPlaybackTime": [NSNumber numberWithLongLong:[@(floor([currentPlaybackTime timeIntervalSince1970] * 1000)) longLongValue]], - @"target": self.reactTag, - @"seekableDuration": [self calculateSeekableDuration], - }); - } -} - -/*! - * Calculates and returns the playable duration of the current player item using its loaded time ranges. - * - * \returns The playable duration of the current player item in seconds. - */ -- (NSNumber *)calculatePlayableDuration -{ - AVPlayerItem *video = _player.currentItem; - if (video.status == AVPlayerItemStatusReadyToPlay) { - __block CMTimeRange effectiveTimeRange; - [video.loadedTimeRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - CMTimeRange timeRange = [obj CMTimeRangeValue]; - if (CMTimeRangeContainsTime(timeRange, video.currentTime)) { - effectiveTimeRange = timeRange; - *stop = YES; - } - }]; - Float64 playableDuration = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)); - if (playableDuration > 0) { - return [NSNumber numberWithFloat:playableDuration]; - } - } - return [NSNumber numberWithInteger:0]; -} - -- (NSNumber *)calculateSeekableDuration -{ - CMTimeRange timeRange = [self playerItemSeekableTimeRange]; - if (CMTIME_IS_NUMERIC(timeRange.duration)) - { - return [NSNumber numberWithFloat:CMTimeGetSeconds(timeRange.duration)]; - } - return [NSNumber numberWithInteger:0]; -} - -- (void)addPlayerItemObservers -{ - [_playerItem addObserver:self forKeyPath:statusKeyPath options:0 context:nil]; - [_playerItem addObserver:self forKeyPath:playbackBufferEmptyKeyPath options:0 context:nil]; - [_playerItem addObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath options:0 context:nil]; - [_playerItem addObserver:self forKeyPath:timedMetadata options:NSKeyValueObservingOptionNew context:nil]; - _playerItemObserversSet = YES; -} - -/* Fixes https://github.com/brentvatne/react-native-video/issues/43 - * Crashes caused when trying to remove the observer when there is no - * observer set */ -- (void)removePlayerItemObservers -{ - if (_playerItemObserversSet) { - [_playerItem removeObserver:self forKeyPath:statusKeyPath]; - [_playerItem removeObserver:self forKeyPath:playbackBufferEmptyKeyPath]; - [_playerItem removeObserver:self forKeyPath:playbackLikelyToKeepUpKeyPath]; - [_playerItem removeObserver:self forKeyPath:timedMetadata]; - _playerItemObserversSet = NO; - } -} - -#pragma mark - Player and source - -- (void)setSrc:(NSDictionary *)source -{ - _source = source; - [self removePlayerLayer]; - [self removePlayerTimeObserver]; - [self removePlayerItemObservers]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) 0), dispatch_get_main_queue(), ^{ - - // perform on next run loop, otherwise other passed react-props may not be set - [self playerItemForSource:self->_source withCallback:^(AVPlayerItem * playerItem) { - self->_playerItem = playerItem; - _playerItem = playerItem; - [self setPreferredForwardBufferDuration:_preferredForwardBufferDuration]; - [self addPlayerItemObservers]; - [self setFilter:self->_filterName]; - [self setMaxBitRate:self->_maxBitRate]; - - [_player pause]; - - if (_playbackRateObserverRegistered) { - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - _playbackRateObserverRegistered = NO; - } - if (self->_isExternalPlaybackActiveObserverRegistered) { - [self->_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - self->_isExternalPlaybackActiveObserverRegistered = NO; - } - - self->_player = [AVPlayer playerWithPlayerItem:self->_playerItem]; - self->_player.actionAtItemEnd = AVPlayerActionAtItemEndNone; - - [self->_player addObserver:self forKeyPath:playbackRate options:0 context:nil]; - self->_playbackRateObserverRegistered = YES; - - [self->_player addObserver:self forKeyPath:externalPlaybackActive options:0 context:nil]; - self->_isExternalPlaybackActiveObserverRegistered = YES; - - [self addPlayerTimeObserver]; - if (@available(iOS 10.0, *)) { - [self setAutomaticallyWaitsToMinimizeStalling:_automaticallyWaitsToMinimizeStalling]; - } - - //Perform on next run loop, otherwise onVideoLoadStart is nil - if (self.onVideoLoadStart) { - id uri = [self->_source objectForKey:@"uri"]; - id type = [self->_source objectForKey:@"type"]; - self.onVideoLoadStart(@{@"src": @{ - @"uri": uri ? uri : [NSNull null], - @"type": type ? type : [NSNull null], - @"isNetwork": [NSNumber numberWithBool:(bool)[self->_source objectForKey:@"isNetwork"]]}, - @"drm": self->_drm ? self->_drm : [NSNull null], - @"target": self.reactTag - }); - } - }]; - }); - _videoLoadStarted = YES; -} - -- (void)setDrm:(NSDictionary *)drm { - _drm = drm; -} - -- (NSURL*) urlFilePath:(NSString*) filepath { - if ([filepath containsString:@"file://"]) { - return [NSURL URLWithString:filepath]; - } - - // if no file found, check if the file exists in the Document directory - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString* relativeFilePath = [filepath lastPathComponent]; - // the file may be multiple levels below the documents directory - NSArray* fileComponents = [filepath componentsSeparatedByString:@"Documents/"]; - if (fileComponents.count > 1) { - relativeFilePath = [fileComponents objectAtIndex:1]; - } - - NSString *path = [paths.firstObject stringByAppendingPathComponent:relativeFilePath]; - if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { - return [NSURL fileURLWithPath:path]; - } - return nil; -} - -- (void)playerItemPrepareText:(AVAsset *)asset assetOptions:(NSDictionary * __nullable)assetOptions withCallback:(void(^)(AVPlayerItem *))handler -{ - if (!_textTracks || _textTracks.count==0) { - handler([AVPlayerItem playerItemWithAsset:asset]); - return; - } - - // AVPlayer can't airplay AVMutableCompositions - _allowsExternalPlayback = NO; - - // sideload text tracks - AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init]; - - AVAssetTrack *videoAsset = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject; - AVMutableCompositionTrack *videoCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; - [videoCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:videoAsset - atTime:kCMTimeZero - error:nil]; - - AVAssetTrack *audioAsset = [asset tracksWithMediaType:AVMediaTypeAudio].firstObject; - AVMutableCompositionTrack *audioCompTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; - [audioCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:audioAsset - atTime:kCMTimeZero - error:nil]; - - NSMutableArray* validTextTracks = [NSMutableArray array]; - for (int i = 0; i < _textTracks.count; ++i) { - AVURLAsset *textURLAsset; - NSString *textUri = [_textTracks objectAtIndex:i][@"uri"]; - if ([[textUri lowercaseString] hasPrefix:@"http"]) { - textURLAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:textUri] options:assetOptions]; - } else { - textURLAsset = [AVURLAsset URLAssetWithURL:[self urlFilePath:textUri] options:nil]; - } - AVAssetTrack *textTrackAsset = [textURLAsset tracksWithMediaType:AVMediaTypeText].firstObject; - if (!textTrackAsset) continue; // fix when there's no textTrackAsset - [validTextTracks addObject:[_textTracks objectAtIndex:i]]; - AVMutableCompositionTrack *textCompTrack = [mixComposition - addMutableTrackWithMediaType:AVMediaTypeText - preferredTrackID:kCMPersistentTrackID_Invalid]; - [textCompTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, videoAsset.timeRange.duration) - ofTrack:textTrackAsset - atTime:kCMTimeZero - error:nil]; - } - if (validTextTracks.count != _textTracks.count) { - [self setTextTracks:validTextTracks]; - } - - handler([AVPlayerItem playerItemWithAsset:mixComposition]); -} - -- (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlayerItem *))handler -{ - bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]]; - bool isAsset = [RCTConvert BOOL:[source objectForKey:@"isAsset"]]; - bool shouldCache = [RCTConvert BOOL:[source objectForKey:@"shouldCache"]]; - NSString *uri = [source objectForKey:@"uri"]; - NSString *type = [source objectForKey:@"type"]; - AVURLAsset *asset; - if (!uri || [uri isEqualToString:@""]) { - DebugLog(@"Could not find video URL in source '%@'", source); - return; - } - - NSURL *url = isNetwork || isAsset - ? [NSURL URLWithString:uri] - : [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]]; - NSMutableDictionary *assetOptions = [[NSMutableDictionary alloc] init]; - - if (isNetwork) { - NSDictionary *headers = [source objectForKey:@"requestHeaders"]; - if ([headers count] > 0) { - [assetOptions setObject:headers forKey:@"AVURLAssetHTTPHeaderFieldsKey"]; - } - NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; - [assetOptions setObject:cookies forKey:AVURLAssetHTTPCookiesKey]; - -#if __has_include() - if (shouldCache && (!_textTracks || !_textTracks.count)) { - /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying - * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. - * Until this is fixed, we need to bypass caching when text tracks are specified. - */ - DebugLog(@"Caching is not supported for uri '%@' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - [self playerItemForSourceUsingCache:uri assetOptions:assetOptions withCallback:handler]; - return; - } -#endif - - asset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; - } else if (isAsset) { - asset = [AVURLAsset URLAssetWithURL:url options:nil]; - } else { - asset = [AVURLAsset URLAssetWithURL:[[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:uri ofType:type]] options:nil]; - } - // Reset _loadingRequest - if (_loadingRequest != nil) { - [_loadingRequest finishLoading]; - } - _requestingCertificate = NO; - _requestingCertificateErrored = NO; - // End Reset _loadingRequest - if (self->_drm != nil) { - dispatch_queue_t queue = dispatch_queue_create("assetQueue", nil); - [asset.resourceLoader setDelegate:self queue:queue]; - } - - [self playerItemPrepareText:asset assetOptions:assetOptions withCallback:handler]; -} - -#if __has_include() - -- (void)playerItemForSourceUsingCache:(NSString *)uri assetOptions:(NSDictionary *)options withCallback:(void(^)(AVPlayerItem *))handler { - NSURL *url = [NSURL URLWithString:uri]; - [_videoCache getItemForUri:uri withCallback:^(RCTVideoCacheStatus videoCacheStatus, AVAsset * _Nullable cachedAsset) { - switch (videoCacheStatus) { - case RCTVideoCacheStatusMissingFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - case RCTVideoCacheStatusUnsupportedFileExtension: { - DebugLog(@"Could not generate cache key for uri '%@'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md", uri); - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:options]; - [self playerItemPrepareText:asset assetOptions:options withCallback:handler]; - return; - } - default: - if (cachedAsset) { - DebugLog(@"Playing back uri '%@' from cache", uri); - // See note in playerItemForSource about not being able to support text tracks & caching - handler([AVPlayerItem playerItemWithAsset:cachedAsset]); - return; - } - } - - DVURLAsset *asset = [[DVURLAsset alloc] initWithURL:url options:options networkTimeout:10000]; - asset.loaderDelegate = self; - - /* More granular code to have control over the DVURLAsset - DVAssetLoaderDelegate *resourceLoaderDelegate = [[DVAssetLoaderDelegate alloc] initWithURL:url]; - resourceLoaderDelegate.delegate = self; - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO]; - components.scheme = [DVAssetLoaderDelegate scheme]; - AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[components URL] options:options]; - [asset.resourceLoader setDelegate:resourceLoaderDelegate queue:dispatch_get_main_queue()]; - */ - - handler([AVPlayerItem playerItemWithAsset:asset]); - }]; -} - -#pragma mark - DVAssetLoaderDelegate - -- (void)dvAssetLoaderDelegate:(DVAssetLoaderDelegate *)loaderDelegate - didLoadData:(NSData *)data - forURL:(NSURL *)url { - [_videoCache storeItem:data forUri:[url absoluteString] withCallback:^(BOOL success) { - DebugLog(@"Cache data stored successfully 🎉"); - }]; -} - -#endif - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - - if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) { - self.onReadyForDisplay(@{@"target": self.reactTag}); - return; - } - if (object == _playerItem) { - // When timeMetadata is read the event onTimedMetadata is triggered - if ([keyPath isEqualToString:timedMetadata]) { - NSArray *items = [change objectForKey:@"new"]; - if (items && ![items isEqual:[NSNull null]] && items.count > 0) { - NSMutableArray *array = [NSMutableArray new]; - for (AVMetadataItem *item in items) { - NSString *value = (NSString *)item.value; - NSString *identifier = item.identifier; - - if (![value isEqual: [NSNull null]]) { - NSDictionary *dictionary = [[NSDictionary alloc] initWithObjects:@[value, identifier] forKeys:@[@"value", @"identifier"]]; - - [array addObject:dictionary]; - } - } - - self.onTimedMetadata(@{ - @"target": self.reactTag, - @"metadata": array - }); - } - } - - if ([keyPath isEqualToString:statusKeyPath]) { - // Handle player item status change. - if (_playerItem.status == AVPlayerItemStatusReadyToPlay) { - float duration = CMTimeGetSeconds(_playerItem.asset.duration); - - if (isnan(duration)) { - duration = 0.0; - } - - NSObject *width = @"undefined"; - NSObject *height = @"undefined"; - NSString *orientation = @"undefined"; - - if ([_playerItem.asset tracksWithMediaType:AVMediaTypeVideo].count > 0) { - AVAssetTrack *videoTrack = [[_playerItem.asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; - width = [NSNumber numberWithFloat:videoTrack.naturalSize.width]; - height = [NSNumber numberWithFloat:videoTrack.naturalSize.height]; - CGAffineTransform preferredTransform = [videoTrack preferredTransform]; - - if ((videoTrack.naturalSize.width == preferredTransform.tx - && videoTrack.naturalSize.height == preferredTransform.ty) - || (preferredTransform.tx == 0 && preferredTransform.ty == 0)) - { - orientation = @"landscape"; - } else { - orientation = @"portrait"; - } - } else if (_playerItem.presentationSize.height) { - width = [NSNumber numberWithFloat:_playerItem.presentationSize.width]; - height = [NSNumber numberWithFloat:_playerItem.presentationSize.height]; - orientation = _playerItem.presentationSize.width > _playerItem.presentationSize.height ? @"landscape" : @"portrait"; - } - - if (_pendingSeek) { - [self setCurrentTime:_pendingSeekTime]; - _pendingSeek = false; - } - - if (self.onVideoLoad && _videoLoadStarted) { - self.onVideoLoad(@{@"duration": [NSNumber numberWithFloat:duration], - @"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(_playerItem.currentTime)], - @"canPlayReverse": [NSNumber numberWithBool:_playerItem.canPlayReverse], - @"canPlayFastForward": [NSNumber numberWithBool:_playerItem.canPlayFastForward], - @"canPlaySlowForward": [NSNumber numberWithBool:_playerItem.canPlaySlowForward], - @"canPlaySlowReverse": [NSNumber numberWithBool:_playerItem.canPlaySlowReverse], - @"canStepBackward": [NSNumber numberWithBool:_playerItem.canStepBackward], - @"canStepForward": [NSNumber numberWithBool:_playerItem.canStepForward], - @"naturalSize": @{ - @"width": width, - @"height": height, - @"orientation": orientation - }, - @"audioTracks": [self getAudioTrackInfo], - @"textTracks": [self getTextTrackInfo], - @"target": self.reactTag}); - } - _videoLoadStarted = NO; - - [self attachListeners]; - [self applyModifiers]; - } else if (_playerItem.status == AVPlayerItemStatusFailed && self.onVideoError) { - self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: _playerItem.error.code], - @"localizedDescription": [_playerItem.error localizedDescription] == nil ? @"" : [_playerItem.error localizedDescription], - @"localizedFailureReason": [_playerItem.error localizedFailureReason] == nil ? @"" : [_playerItem.error localizedFailureReason], - @"localizedRecoverySuggestion": [_playerItem.error localizedRecoverySuggestion] == nil ? @"" : [_playerItem.error localizedRecoverySuggestion], - @"domain": _playerItem != nil && _playerItem.error != nil ? _playerItem.error.domain : @"RTCVideo"}, - @"target": self.reactTag}); - } - } else if ([keyPath isEqualToString:playbackBufferEmptyKeyPath]) { - _playerBufferEmpty = YES; - self.onVideoBuffer(@{@"isBuffering": @(YES), @"target": self.reactTag}); - } else if ([keyPath isEqualToString:playbackLikelyToKeepUpKeyPath]) { - // Continue playing (or not if paused) after being paused due to hitting an unbuffered zone. - if ((!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && _playerItem.playbackLikelyToKeepUp) { - [self setPaused:_paused]; - } - _playerBufferEmpty = NO; - self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); - } - } else if (object == _player) { - if([keyPath isEqualToString:playbackRate]) { - if(self.onPlaybackRateChange) { - self.onPlaybackRateChange(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], - @"target": self.reactTag}); - } - if(_playbackStalled && _player.rate > 0) { - if(self.onPlaybackResume) { - self.onPlaybackResume(@{@"playbackRate": [NSNumber numberWithFloat:_player.rate], - @"target": self.reactTag}); - } - _playbackStalled = NO; - } - } - else if([keyPath isEqualToString:externalPlaybackActive]) { - if(self.onVideoExternalPlaybackChange) { - self.onVideoExternalPlaybackChange(@{@"isExternalPlaybackActive": [NSNumber numberWithBool:_player.isExternalPlaybackActive], - @"target": self.reactTag}); - } - } - } else if (object == _playerViewController.contentOverlayView) { - // when controls==true, this is a hack to reset the rootview when rotation happens in fullscreen - if ([keyPath isEqualToString:@"frame"]) { - - CGRect oldRect = [change[NSKeyValueChangeOldKey] CGRectValue]; - CGRect newRect = [change[NSKeyValueChangeNewKey] CGRectValue]; - - if (!CGRectEqualToRect(oldRect, newRect)) { - if (CGRectEqualToRect(newRect, [UIScreen mainScreen].bounds)) { - NSLog(@"in fullscreen"); - - [self.reactViewController.view setFrame:[UIScreen mainScreen].bounds]; - [self.reactViewController.view setNeedsLayout]; - } else NSLog(@"not fullscreen"); - } - - return; - } - } -} - -- (void)attachListeners -{ - // listen for end of file - [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVPlayerItemDidPlayToEndTimeNotification - object:[_player currentItem]]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(playerItemDidReachEnd:) - name:AVPlayerItemDidPlayToEndTimeNotification - object:[_player currentItem]]; - - [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVPlayerItemPlaybackStalledNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(playbackStalled:) - name:AVPlayerItemPlaybackStalledNotification - object:nil]; - - [[NSNotificationCenter defaultCenter] removeObserver:self - name:AVPlayerItemNewAccessLogEntryNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleAVPlayerAccess:) - name:AVPlayerItemNewAccessLogEntryNotification - object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self - name: AVPlayerItemFailedToPlayToEndTimeNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(didFailToFinishPlaying:) - name: AVPlayerItemFailedToPlayToEndTimeNotification - object:nil]; - -} - -- (void)handleAVPlayerAccess:(NSNotification *)notification { - AVPlayerItemAccessLog *accessLog = [((AVPlayerItem *)notification.object) accessLog]; - AVPlayerItemAccessLogEvent *lastEvent = accessLog.events.lastObject; - - /* TODO: get this working - if (self.onBandwidthUpdate) { - self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); - } - */ -} - -- (void)didFailToFinishPlaying:(NSNotification *)notification { - NSError *error = notification.userInfo[AVPlayerItemFailedToPlayToEndTimeErrorKey]; - self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], - @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], - @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], - @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], - @"domain": error.domain}, - @"target": self.reactTag}); -} - -- (void)playbackStalled:(NSNotification *)notification -{ - if(self.onPlaybackStalled) { - self.onPlaybackStalled(@{@"target": self.reactTag}); - } - _playbackStalled = YES; -} - -- (void)playerItemDidReachEnd:(NSNotification *)notification -{ - if(self.onVideoEnd) { - self.onVideoEnd(@{@"target": self.reactTag}); - } - - if (_repeat) { - AVPlayerItem *item = [notification object]; - [item seekToTime:kCMTimeZero]; - [self applyModifiers]; - } else { - [self removePlayerTimeObserver]; - } -} - -#pragma mark - Prop setters - -- (void)setResizeMode:(NSString*)mode -{ - if( _controls ) - { - _playerViewController.videoGravity = mode; - } - else - { - _playerLayer.videoGravity = mode; - } - _resizeMode = mode; -} - -- (void)setPlayInBackground:(BOOL)playInBackground -{ - _playInBackground = playInBackground; -} - -- (void)setPreventsDisplaySleepDuringVideoPlayback:(BOOL)preventsDisplaySleepDuringVideoPlayback -{ - _preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback; - [self applyModifiers]; -} - -- (void)setAllowsExternalPlayback:(BOOL)allowsExternalPlayback -{ - _allowsExternalPlayback = allowsExternalPlayback; - _player.allowsExternalPlayback = _allowsExternalPlayback; -} - -- (void)setPlayWhenInactive:(BOOL)playWhenInactive -{ - _playWhenInactive = playWhenInactive; -} - -- (void)setPictureInPicture:(BOOL)pictureInPicture -{ - #if TARGET_OS_IOS - if (_pictureInPicture == pictureInPicture) { - return; - } - - _pictureInPicture = pictureInPicture; - if (_pipController && _pictureInPicture && ![_pipController isPictureInPictureActive]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_pipController startPictureInPicture]; - }); - } else if (_pipController && !_pictureInPicture && [_pipController isPictureInPictureActive]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [_pipController stopPictureInPicture]; - }); - } - #endif -} - -#if TARGET_OS_IOS -- (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore -{ - if (_restoreUserInterfaceForPIPStopCompletionHandler != NULL) { - _restoreUserInterfaceForPIPStopCompletionHandler(restore); - _restoreUserInterfaceForPIPStopCompletionHandler = NULL; - } -} - -- (void)setupPipController { - if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { - // Create new controller passing reference to the AVPlayerLayer - _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; - _pipController.delegate = self; - } -} -#endif - -- (void)setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch -{ - _ignoreSilentSwitch = ignoreSilentSwitch; - [self applyModifiers]; -} - -- (void)setMixWithOthers:(NSString *)mixWithOthers -{ - _mixWithOthers = mixWithOthers; - [self applyModifiers]; -} - -- (void)setPaused:(BOOL)paused -{ - if (paused) { - [_player pause]; - [_player setRate:0.0]; - } else { - AVAudioSession *session = [AVAudioSession sharedInstance]; - AVAudioSessionCategory category = nil; - AVAudioSessionCategoryOptions options = nil; - - if([_ignoreSilentSwitch isEqualToString:@"ignore"]) { - category = AVAudioSessionCategoryPlayback; - } else if([_ignoreSilentSwitch isEqualToString:@"obey"]) { - category = AVAudioSessionCategoryAmbient; - } - - if([_mixWithOthers isEqualToString:@"mix"]) { - options = AVAudioSessionCategoryOptionMixWithOthers; - } else if([_mixWithOthers isEqualToString:@"duck"]) { - options = AVAudioSessionCategoryOptionDuckOthers; - } - - if (category != nil && options != nil) { - [session setCategory:category withOptions:options error:nil]; - } else if (category != nil && options == nil) { - [session setCategory:category error:nil]; - } else if (category == nil && options != nil) { - [session setCategory:session.category withOptions:options error:nil]; - } - - if (@available(iOS 10.0, *) && !_automaticallyWaitsToMinimizeStalling) { - [_player playImmediatelyAtRate:_rate]; - } else { - [_player play]; - [_player setRate:_rate]; - } - [_player setRate:_rate]; - } - - _paused = paused; -} - -- (float)getCurrentTime -{ - return _playerItem != NULL ? CMTimeGetSeconds(_playerItem.currentTime) : 0; -} - -- (void)setCurrentTime:(float)currentTime -{ - NSDictionary *info = @{ - @"time": [NSNumber numberWithFloat:currentTime], - @"tolerance": [NSNumber numberWithInt:100] - }; - [self setSeek:info]; -} - -- (void)setSeek:(NSDictionary *)info -{ - NSNumber *seekTime = info[@"time"]; - NSNumber *seekTolerance = info[@"tolerance"]; - - int timeScale = 1000; - - AVPlayerItem *item = _player.currentItem; - if (item && item.status == AVPlayerItemStatusReadyToPlay) { - // TODO check loadedTimeRanges - - CMTime cmSeekTime = CMTimeMakeWithSeconds([seekTime floatValue], timeScale); - CMTime current = item.currentTime; - // TODO figure out a good tolerance level - CMTime tolerance = CMTimeMake([seekTolerance floatValue], timeScale); - BOOL wasPaused = _paused; - - if (CMTimeCompare(current, cmSeekTime) != 0) { - if (!wasPaused) [_player pause]; - [_player seekToTime:cmSeekTime toleranceBefore:tolerance toleranceAfter:tolerance completionHandler:^(BOOL finished) { - if (!_timeObserver) { - [self addPlayerTimeObserver]; - } - if (!wasPaused) { - [self setPaused:false]; - } - if(self.onVideoSeek) { - self.onVideoSeek(@{@"currentTime": [NSNumber numberWithFloat:CMTimeGetSeconds(item.currentTime)], - @"seekTime": seekTime, - @"target": self.reactTag}); - } - }]; - - _pendingSeek = false; - } - - } else { - _pendingSeek = true; - _pendingSeekTime = [seekTime floatValue]; - } -} - -- (void)setRate:(float)rate -{ - _rate = rate; - [self applyModifiers]; -} - -- (void)setMuted:(BOOL)muted -{ - _muted = muted; - [self applyModifiers]; -} - -- (void)setVolume:(float)volume -{ - _volume = volume; - [self applyModifiers]; -} - -- (void)setMaxBitRate:(float) maxBitRate { - _maxBitRate = maxBitRate; - _playerItem.preferredPeakBitRate = maxBitRate; -} - -- (void)setPreferredForwardBufferDuration:(float) preferredForwardBufferDuration -{ - _preferredForwardBufferDuration = preferredForwardBufferDuration; - _playerItem.preferredForwardBufferDuration = preferredForwardBufferDuration; -} - -- (void)setAutomaticallyWaitsToMinimizeStalling:(BOOL)waits -{ - _automaticallyWaitsToMinimizeStalling = waits; - _player.automaticallyWaitsToMinimizeStalling = waits; -} - - -- (void)applyModifiers -{ - if (_muted) { - if (!_controls) { - [_player setVolume:0]; - } - [_player setMuted:YES]; - } else { - [_player setVolume:_volume]; - [_player setMuted:NO]; - } - - if (@available(iOS 12.0, *)) { - self->_player.preventsDisplaySleepDuringVideoPlayback = _preventsDisplaySleepDuringVideoPlayback; - } else { - // Fallback on earlier versions - } - - [self setMaxBitRate:_maxBitRate]; - [self setSelectedAudioTrack:_selectedAudioTrack]; - [self setSelectedTextTrack:_selectedTextTrack]; - [self setResizeMode:_resizeMode]; - [self setRepeat:_repeat]; - [self setPaused:_paused]; - [self setControls:_controls]; - [self setAllowsExternalPlayback:_allowsExternalPlayback]; -} - -- (void)setRepeat:(BOOL)repeat { - _repeat = repeat; -} - -- (void)setMediaSelectionTrackForCharacteristic:(AVMediaCharacteristic)characteristic - withCriteria:(NSDictionary *)criteria -{ - NSString *type = criteria[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:characteristic]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = criteria[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([criteria[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [criteria[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; - } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; -} - -- (void)setSelectedAudioTrack:(NSDictionary *)selectedAudioTrack { - _selectedAudioTrack = selectedAudioTrack; - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicAudible - withCriteria:_selectedAudioTrack]; -} - -- (void)setSelectedTextTrack:(NSDictionary *)selectedTextTrack { - _selectedTextTrack = selectedTextTrack; - if (_textTracks) { // sideloaded text tracks - [self setSideloadedText]; - } else { // text tracks included in the HLS playlist - [self setMediaSelectionTrackForCharacteristic:AVMediaCharacteristicLegible - withCriteria:_selectedTextTrack]; - } -} - -- (void) setSideloadedText { - NSString *type = _selectedTextTrack[@"type"]; - NSArray *textTracks = [self getTextTrackInfo]; - - // The first few tracks will be audio & video track - int firstTextIndex = 0; - for (firstTextIndex = 0; firstTextIndex < _player.currentItem.tracks.count; ++firstTextIndex) { - if ([_player.currentItem.tracks[firstTextIndex].assetTrack hasMediaCharacteristic:AVMediaCharacteristicLegible]) { - break; - } - } - - int selectedTrackIndex = RCTVideoUnset; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"]) { - NSString *selectedValue = _selectedTextTrack[@"value"]; - for (int i = 0; i < textTracks.count; ++i) { - NSDictionary *currentTextTrack = [textTracks objectAtIndex:i]; - if ([selectedValue isEqualToString:currentTextTrack[@"language"]]) { - selectedTrackIndex = i; - break; - } - } - } else if ([type isEqualToString:@"title"]) { - NSString *selectedValue = _selectedTextTrack[@"value"]; - for (int i = 0; i < textTracks.count; ++i) { - NSDictionary *currentTextTrack = [textTracks objectAtIndex:i]; - if ([selectedValue isEqualToString:currentTextTrack[@"title"]]) { - selectedTrackIndex = i; - break; - } - } - } else if ([type isEqualToString:@"index"]) { - if ([_selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [_selectedTextTrack[@"value"] intValue]; - if (textTracks.count > index) { - selectedTrackIndex = index; - } - } - } - - // in the situation that a selected text track is not available (eg. specifies a textTrack not available) - if (![type isEqualToString:@"disabled"] && selectedTrackIndex == RCTVideoUnset) { - CFArrayRef captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser); - NSArray *captionSettings = (__bridge NSArray*)captioningMediaCharacteristics; - if ([captionSettings containsObject:AVMediaCharacteristicTranscribesSpokenDialogForAccessibility]) { - selectedTrackIndex = 0; // If we can't find a match, use the first available track - NSString *systemLanguage = [[NSLocale preferredLanguages] firstObject]; - for (int i = 0; i < textTracks.count; ++i) { - NSDictionary *currentTextTrack = [textTracks objectAtIndex:i]; - if ([systemLanguage isEqualToString:currentTextTrack[@"language"]]) { - selectedTrackIndex = i; - break; - } - } - } - } - - for (int i = firstTextIndex; i < _player.currentItem.tracks.count; ++i) { - BOOL isEnabled = NO; - if (selectedTrackIndex != RCTVideoUnset) { - isEnabled = i == selectedTrackIndex + firstTextIndex; - } - [_player.currentItem.tracks[i] setEnabled:isEnabled]; - } -} - --(void) setStreamingText { - NSString *type = _selectedTextTrack[@"type"]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - AVMediaSelectionOption *mediaOption; - - if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil - } else if ([type isEqualToString:@"language"] || [type isEqualToString:@"title"]) { - NSString *value = _selectedTextTrack[@"value"]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *optionValue; - if ([type isEqualToString:@"language"]) { - optionValue = [currentOption extendedLanguageTag]; - } else { - optionValue = [[[currentOption commonMetadata] - valueForKey:@"value"] - objectAtIndex:0]; - } - if ([value isEqualToString:optionValue]) { - mediaOption = currentOption; - break; - } - } - //} else if ([type isEqualToString:@"default"]) { - // option = group.defaultOption; */ - } else if ([type isEqualToString:@"index"]) { - if ([_selectedTextTrack[@"value"] isKindOfClass:[NSNumber class]]) { - int index = [_selectedTextTrack[@"value"] intValue]; - if (group.options.count > index) { - mediaOption = [group.options objectAtIndex:index]; - } - } - } else { // default. invalid type or "system" - [_player.currentItem selectMediaOptionAutomaticallyInMediaSelectionGroup:group]; - return; - } - - // If a match isn't found, option will be nil and text tracks will be disabled - [_player.currentItem selectMediaOption:mediaOption inMediaSelectionGroup:group]; -} - -- (void)setTextTracks:(NSArray*) textTracks; -{ - _textTracks = textTracks; - - // in case textTracks was set after selectedTextTrack - if (_selectedTextTrack) [self setSelectedTextTrack:_selectedTextTrack]; -} - -- (NSArray *)getAudioTrackInfo -{ - NSMutableArray *audioTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *audioTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [audioTracks addObject:audioTrack]; - } - return audioTracks; -} - -- (NSArray *)getTextTrackInfo -{ - // if sideloaded, textTracks will already be set - if (_textTracks) return _textTracks; - - // if streaming video, we extract the text tracks - NSMutableArray *textTracks = [[NSMutableArray alloc] init]; - AVMediaSelectionGroup *group = [_player.currentItem.asset - mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - for (int i = 0; i < group.options.count; ++i) { - AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; - NSString *title = @""; - NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; - if (values.count > 0) { - title = [values objectAtIndex:0]; - } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; - NSDictionary *textTrack = @{ - @"index": [NSNumber numberWithInt:i], - @"title": title, - @"language": language - }; - [textTracks addObject:textTrack]; - } - return textTracks; -} - -- (BOOL)getFullscreen -{ - return _fullscreenPlayerPresented; -} - -- (void)setFullscreen:(BOOL) fullscreen { - if( fullscreen && !_fullscreenPlayerPresented && _player ) - { - // Ensure player view controller is not null - if( !_playerViewController ) - { - [self usePlayerViewController]; - } - // Set presentation style to fullscreen - [_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen]; - - // Find the nearest view controller - UIViewController *viewController = [self firstAvailableUIViewController]; - if( !viewController ) - { - UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; - viewController = keyWindow.rootViewController; - if( viewController.childViewControllers.count > 0 ) - { - viewController = viewController.childViewControllers.lastObject; - } - } - if( viewController ) - { - _presentingViewController = viewController; - if(self.onVideoFullscreenPlayerWillPresent) { - self.onVideoFullscreenPlayerWillPresent(@{@"target": self.reactTag}); - } - [viewController presentViewController:_playerViewController animated:true completion:^{ - _playerViewController.showsPlaybackControls = YES; - _fullscreenPlayerPresented = fullscreen; - _playerViewController.autorotate = _fullscreenAutorotate; - if(self.onVideoFullscreenPlayerDidPresent) { - self.onVideoFullscreenPlayerDidPresent(@{@"target": self.reactTag}); - } - }]; - } - } - else if ( !fullscreen && _fullscreenPlayerPresented ) - { - [self videoPlayerViewControllerWillDismiss:_playerViewController]; - [_presentingViewController dismissViewControllerAnimated:true completion:^{ - [self videoPlayerViewControllerDidDismiss:_playerViewController]; - }]; - } -} - -- (void)setFullscreenAutorotate:(BOOL)autorotate { - _fullscreenAutorotate = autorotate; - if (_fullscreenPlayerPresented) { - _playerViewController.autorotate = autorotate; - } -} - -- (void)setFullscreenOrientation:(NSString *)orientation { - _fullscreenOrientation = orientation; - if (_fullscreenPlayerPresented) { - _playerViewController.preferredOrientation = orientation; - } -} - -- (void)usePlayerViewController -{ - if( _player ) - { - if (!_playerViewController) { - _playerViewController = [self createPlayerViewController:_player withPlayerItem:_playerItem]; - } - // to prevent video from being animated when resizeMode is 'cover' - // resize mode must be set before subview is added - [self setResizeMode:_resizeMode]; - - if (_controls) { - UIViewController *viewController = [self reactViewController]; - [viewController addChildViewController:_playerViewController]; - [self addSubview:_playerViewController.view]; - } - - [_playerViewController addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; - - [_playerViewController.contentOverlayView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; - } -} - -- (void)usePlayerLayer -{ - if( _player ) - { - _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; - _playerLayer.frame = self.bounds; - _playerLayer.needsDisplayOnBoundsChange = YES; - - // to prevent video from being animated when resizeMode is 'cover' - // resize mode must be set before layer is added - [self setResizeMode:_resizeMode]; - [_playerLayer addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; - _playerLayerObserverSet = YES; - - [self.layer addSublayer:_playerLayer]; - self.layer.needsDisplayOnBoundsChange = YES; - #if TARGET_OS_IOS - [self setupPipController]; - #endif - } -} - -- (void)setControls:(BOOL)controls -{ - if( _controls != controls || (!_playerLayer && !_playerViewController) ) - { - _controls = controls; - if( _controls ) - { - [self removePlayerLayer]; - [self usePlayerViewController]; - } - else - { - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; - [self usePlayerLayer]; - } - } -} - -- (void)setProgressUpdateInterval:(float)progressUpdateInterval -{ - _progressUpdateInterval = progressUpdateInterval; - - if (_timeObserver) { - [self removePlayerTimeObserver]; - [self addPlayerTimeObserver]; - } -} - -- (void)removePlayerLayer -{ - if (_loadingRequest != nil) { - [_loadingRequest finishLoading]; - } - _requestingCertificate = NO; - _requestingCertificateErrored = NO; - [_playerLayer removeFromSuperlayer]; - if (_playerLayerObserverSet) { - [_playerLayer removeObserver:self forKeyPath:readyForDisplayKeyPath]; - _playerLayerObserverSet = NO; - } - _playerLayer = nil; -} - -#pragma mark - RCTVideoPlayerViewControllerDelegate - -- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController -{ - if (_playerViewController == playerViewController && _fullscreenPlayerPresented && self.onVideoFullscreenPlayerWillDismiss) - { - @try{ - [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; - [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; - }@catch(id anException){ - } - self.onVideoFullscreenPlayerWillDismiss(@{@"target": self.reactTag}); - } -} - -- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController -{ - if (_playerViewController == playerViewController && _fullscreenPlayerPresented) - { - _fullscreenPlayerPresented = false; - _presentingViewController = nil; - _playerViewController = nil; - [self applyModifiers]; - if(self.onVideoFullscreenPlayerDidDismiss) { - self.onVideoFullscreenPlayerDidDismiss(@{@"target": self.reactTag}); - } - } -} - -- (void)setFilter:(NSString *)filterName { - _filterName = filterName; - - if (!_filterEnabled) { - return; - } else if ([[_source objectForKey:@"uri"] rangeOfString:@"m3u8"].location != NSNotFound) { - return; // filters don't work for HLS... return - } else if (!_playerItem.asset) { - return; - } - - CIFilter *filter = [CIFilter filterWithName:filterName]; - _playerItem.videoComposition = [AVVideoComposition - videoCompositionWithAsset:_playerItem.asset - applyingCIFiltersWithHandler:^(AVAsynchronousCIImageFilteringRequest *_Nonnull request) { - if (filter == nil) { - [request finishWithImage:request.sourceImage context:nil]; - } else { - CIImage *image = request.sourceImage.imageByClampingToExtent; - [filter setValue:image forKey:kCIInputImageKey]; - CIImage *output = [filter.outputImage imageByCroppingToRect:request.sourceImage.extent]; - [request finishWithImage:output context:nil]; - } - }]; -} - -- (void)setFilterEnabled:(BOOL)filterEnabled { - _filterEnabled = filterEnabled; -} - -#pragma mark - React View Management - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - // We are early in the game and somebody wants to set a subview. - // That can only be in the context of playerViewController. - if( !_controls && !_playerLayer && !_playerViewController ) - { - [self setControls:true]; - } - - if( _controls ) - { - view.frame = self.bounds; - [_playerViewController.contentOverlayView insertSubview:view atIndex:atIndex]; - } - else - { - RCTLogError(@"video cannot have any subviews"); - } - return; -} - -- (void)removeReactSubview:(UIView *)subview -{ - if( _controls ) - { - [subview removeFromSuperview]; - } - else - { - RCTLogError(@"video cannot have any subviews"); - } - return; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - if( _controls ) - { - _playerViewController.view.frame = self.bounds; - - // also adjust all subviews of contentOverlayView - for (UIView* subview in _playerViewController.contentOverlayView.subviews) { - subview.frame = self.bounds; - } - } - else - { - [CATransaction begin]; - [CATransaction setAnimationDuration:0]; - _playerLayer.frame = self.bounds; - [CATransaction commit]; - } -} - -#pragma mark - Lifecycle - -- (void)removeFromSuperview -{ - [_player pause]; - if (_playbackRateObserverRegistered) { - [_player removeObserver:self forKeyPath:playbackRate context:nil]; - _playbackRateObserverRegistered = NO; - } - if (_isExternalPlaybackActiveObserverRegistered) { - [_player removeObserver:self forKeyPath:externalPlaybackActive context:nil]; - _isExternalPlaybackActiveObserverRegistered = NO; - } - _player = nil; - - [self removePlayerLayer]; - - [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; - [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; - [_playerViewController.view removeFromSuperview]; - _playerViewController.rctDelegate = nil; - _playerViewController.player = nil; - _playerViewController = nil; - - [self removePlayerTimeObserver]; - [self removePlayerItemObservers]; - - _eventDispatcher = nil; - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - [super removeFromSuperview]; -} - -#pragma mark - Export - -- (void)save:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - - AVAsset *asset = _playerItem.asset; - - if (asset != nil) { - - AVAssetExportSession *exportSession = [AVAssetExportSession - exportSessionWithAsset:asset presetName:AVAssetExportPresetHighestQuality]; - - if (exportSession != nil) { - NSString *path = nil; - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - path = [self generatePathInDirectory:[[self cacheDirectoryPath] stringByAppendingPathComponent:@"Videos"] - withExtension:@".mp4"]; - NSURL *url = [NSURL fileURLWithPath:path]; - exportSession.outputFileType = AVFileTypeMPEG4; - exportSession.outputURL = url; - exportSession.videoComposition = _playerItem.videoComposition; - exportSession.shouldOptimizeForNetworkUse = true; - [exportSession exportAsynchronouslyWithCompletionHandler:^{ - - switch ([exportSession status]) { - case AVAssetExportSessionStatusFailed: - reject(@"ERROR_COULD_NOT_EXPORT_VIDEO", @"Could not export video", exportSession.error); - break; - case AVAssetExportSessionStatusCancelled: - reject(@"ERROR_EXPORT_SESSION_CANCELLED", @"Export session was cancelled", exportSession.error); - break; - default: - resolve(@{@"uri": url.absoluteString}); - break; - } - - }]; - - } else { - - reject(@"ERROR_COULD_NOT_CREATE_EXPORT_SESSION", @"Could not create export session", nil); - - } - - } else { - - reject(@"ERROR_ASSET_NIL", @"Asset is nil", nil); - - } -} - -- (void)setLicenseResult:(NSString *)license { - NSData *respondData = [self base64DataFromBase64String:license]; - if (_loadingRequest != nil && respondData != nil) { - AVAssetResourceLoadingDataRequest *dataRequest = [_loadingRequest dataRequest]; - [dataRequest respondWithData:respondData]; - [_loadingRequest finishLoading]; - } else { - [self setLicenseResultError:@"No data from JS license response"]; - } -} - -- (BOOL)setLicenseResultError:(NSString *)error { - if (_loadingRequest != nil) { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorFromJSPart - userInfo: @{ - NSLocalizedDescriptionKey: error, - NSLocalizedFailureReasonErrorKey: error, - NSLocalizedRecoverySuggestionErrorKey: error - } - ]; - [self finishLoadingWithError:licenseError]; - } - return NO; -} - -- (BOOL)finishLoadingWithError:(NSError *)error { - if (_loadingRequest && error != nil) { - NSError *licenseError = error; - [_loadingRequest finishLoadingWithError:licenseError]; - if (self.onVideoError) { - self.onVideoError(@{@"error": @{@"code": [NSNumber numberWithInteger: error.code], - @"localizedDescription": [error localizedDescription] == nil ? @"" : [error localizedDescription], - @"localizedFailureReason": [error localizedFailureReason] == nil ? @"" : [error localizedFailureReason], - @"localizedRecoverySuggestion": [error localizedRecoverySuggestion] == nil ? @"" : [error localizedRecoverySuggestion], - @"domain": _playerItem.error == nil ? @"RCTVideo" : _playerItem.error.domain}, - @"target": self.reactTag}); - } - } - return NO; -} - -- (BOOL)ensureDirExistsWithPath:(NSString *)path { - BOOL isDir = NO; - NSError *error; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; - if (!(exists && isDir)) { - [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) { - return NO; - } - } - return YES; -} - -- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension { - NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; - [self ensureDirExistsWithPath:directory]; - return [directory stringByAppendingPathComponent:fileName]; -} - -- (NSString *)cacheDirectoryPath { - NSArray *array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - return array[0]; -} - -#pragma mark - AVAssetResourceLoaderDelegate - -- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { - return [self loadingRequestHandling:renewalRequest]; -} - -- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { - return [self loadingRequestHandling:loadingRequest]; -} - -- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader -didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { - NSLog(@"didCancelLoadingRequest"); -} - -- (BOOL)loadingRequestHandling:(AVAssetResourceLoadingRequest *)loadingRequest { - if (self->_requestingCertificate) { - return YES; - } else if (self->_requestingCertificateErrored) { - return NO; - } - _loadingRequest = loadingRequest; - NSURL *url = loadingRequest.request.URL; - if (self->_drm != nil) { - NSString *contentId; - NSString *contentIdOverride = (NSString *)[self->_drm objectForKey:@"contentId"]; - if (contentIdOverride != nil) { - contentId = contentIdOverride; - } else if (self.onGetLicense) { - contentId = url.host; - } else { - contentId = [url.absoluteString stringByReplacingOccurrencesOfString:@"skd://" withString:@""]; - } - NSString *drmType = (NSString *)[self->_drm objectForKey:@"type"]; - if ([drmType isEqualToString:@"fairplay"]) { - NSString *certificateStringUrl = (NSString *)[self->_drm objectForKey:@"certificateUrl"]; - if (certificateStringUrl != nil) { - NSURL *certificateURL = [NSURL URLWithString:[certificateStringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; - if ([self->_drm objectForKey:@"base64Certificate"]) { - certificateData = [[NSData alloc] initWithBase64EncodedData:certificateData options:NSDataBase64DecodingIgnoreUnknownCharacters]; - } - - if (certificateData != nil) { - NSData *contentIdData; - if(self.onGetLicense) { - contentIdData = [contentId dataUsingEncoding:NSUTF8StringEncoding]; - } else { - contentIdData = [NSData dataWithBytes: [contentId cStringUsingEncoding:NSUTF8StringEncoding] length:[contentId lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; - } - AVAssetResourceLoadingDataRequest *dataRequest = [loadingRequest dataRequest]; - if (dataRequest != nil) { - NSError *spcError = nil; - NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:certificateData contentIdentifier:contentIdData options:nil error:&spcError]; - // Request CKC to the server - NSString *licenseServer = (NSString *)[self->_drm objectForKey:@"licenseServer"]; - if (spcError != nil) { - [self finishLoadingWithError:spcError]; - self->_requestingCertificateErrored = YES; - } - if (spcData != nil) { - if(self.onGetLicense) { - NSString *base64Encoded = [spcData base64EncodedStringWithOptions:0]; - self->_requestingCertificate = YES; - if (licenseServer == nil) { - licenseServer = @""; - } - self.onGetLicense(@{@"licenseUrl": licenseServer, - @"contentId": contentId, - @"spcBase64": base64Encoded, - @"target": self.reactTag} - ); - } else if(licenseServer != nil) { - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setHTTPMethod:@"POST"]; - [request setURL:[NSURL URLWithString:licenseServer]]; - // HEADERS - NSDictionary *headers = (NSDictionary *)[self->_drm objectForKey:@"headers"]; - if (headers != nil) { - for (NSString *key in headers) { - NSString *value = headers[key]; - [request setValue:value forHTTPHeaderField:key]; - } - } - - if(self.onGetLicense) { - [request setHTTPBody: spcData]; - } else { - NSString *spcEncoded = [spcData base64EncodedStringWithOptions:0]; - NSString *spcUrlEncoded = (NSString *) CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)spcEncoded, NULL, CFSTR("?=&+"), kCFStringEncodingUTF8)); - NSString *post = [NSString stringWithFormat:@"spc=%@&%@", spcUrlEncoded, contentId]; - NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; - [request setHTTPBody: postData]; - } - - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; - NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; - if (error != nil) { - NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - [self finishLoadingWithError:error]; - self->_requestingCertificateErrored = YES; - } else { - if([httpResponse statusCode] != 200){ - NSLog(@"Error getting license from %@, HTTP status code %li", url, (long)[httpResponse statusCode]); - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorLicenseRequestNotOk - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining license.", - NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"License server responded with status code %li", (long)[httpResponse statusCode]], - NSLocalizedRecoverySuggestionErrorKey: @"Did you send the correct data to the license Server? Is the server ok?" - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } else if (data != nil) { - if(self.onGetLicense) { - [dataRequest respondWithData:data]; - } else { - NSData *decodedData = [[NSData alloc] initWithBase64EncodedData:data options:0]; - [dataRequest respondWithData:decodedData]; - } - [loadingRequest finishLoading]; - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataFromLicenseRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No data received from the license server.", - NSLocalizedRecoverySuggestionErrorKey: @"Is the licenseServer ok?." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - - } - }]; - [postDataTask resume]; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoSPC - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining license.", - NSLocalizedFailureReasonErrorKey: @"No spc received.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM config." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDataRequest - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No dataRequest found.", - NSLocalizedRecoverySuggestionErrorKey: @"Check your DRM configuration." - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateData - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No certificate data obtained from the specificied url.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified a valid 'certificateUrl'?" - } - ]; - [self finishLoadingWithError:licenseError]; - self->_requestingCertificateErrored = YES; - } - }); - return YES; - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoCertificateURL - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM License.", - NSLocalizedFailureReasonErrorKey: @"No certificate URL has been found.", - NSLocalizedRecoverySuggestionErrorKey: @"Did you specified the prop certificateUrl?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoFairplayDRM - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"Not a valid DRM Scheme has found", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' 'type' as fairplay?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } - - } else { - NSError *licenseError = [NSError errorWithDomain: @"RCTVideo" - code: RCTVideoErrorNoDRMData - userInfo: @{ - NSLocalizedDescriptionKey: @"Error obtaining DRM license.", - NSLocalizedFailureReasonErrorKey: @"No drm object found.", - NSLocalizedRecoverySuggestionErrorKey: @"Have you specified the 'drm' prop?" - } - ]; - return [self finishLoadingWithError:licenseError]; - } - - - return NO; -} - -- (NSData *)base64DataFromBase64String: (NSString *)base64String { - if (base64String != nil) { - // NSData from the Base64 encoded str - NSData *base64Data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSASCIIStringEncoding]; - return base64Data; - } - return nil; -} -#pragma mark - Picture in Picture - -#if TARGET_OS_IOS -- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onPictureInPictureStatusChanged) { - self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:false] - }); - } -} - -- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onPictureInPictureStatusChanged) { - self.onPictureInPictureStatusChanged(@{ - @"isActive": [NSNumber numberWithBool:true] - }); - } -} - -- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - -} - -- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - -} - -- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error { - -} - -- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler { - NSAssert(_restoreUserInterfaceForPIPStopCompletionHandler == NULL, @"restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited."); - if (self.onRestoreUserInterfaceForPictureInPictureStop) { - self.onRestoreUserInterfaceForPictureInPictureStop(@{}); - } - _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler; -} -#endif - -@end diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift new file mode 100644 index 00000000..fb5b66ba --- /dev/null +++ b/ios/Video/RCTVideo.swift @@ -0,0 +1,1147 @@ +import AVFoundation +import AVKit +import Foundation +import React + +class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { + + private var _player:AVPlayer? + private var _playerItem:AVPlayerItem? + private var _source:VideoSource? + private var _playerBufferEmpty:Bool = true + private var _playerLayer:AVPlayerLayer? + + private var _playerViewController:RCTVideoPlayerViewController? + private var _videoURL:NSURL? + + /* DRM */ + private var _drm:DRMParams? + + /* Required to publish events */ + private var _eventDispatcher:RCTEventDispatcher? + private var _videoLoadStarted:Bool = false + + private var _pendingSeek:Bool = false + private var _pendingSeekTime:Float = 0.0 + private var _lastSeekTime:Float = 0.0 + + /* For sending videoProgress events */ + private var _controls:Bool = false + + /* Keep track of any modifiers, need to be applied after each play */ + private var _volume:Float = 1.0 + private var _rate:Float = 1.0 + private var _maxBitRate:Float? + + private var _automaticallyWaitsToMinimizeStalling:Bool = true + private var _muted:Bool = false + private var _paused:Bool = false + private var _repeat:Bool = false + private var _allowsExternalPlayback:Bool = true + private var _textTracks:[TextTrack]? + private var _selectedTextTrackCriteria:SelectedTrackCriteria? + private var _selectedAudioTrackCriteria:SelectedTrackCriteria? + private var _playbackStalled:Bool = false + private var _playInBackground:Bool = false + private var _preventsDisplaySleepDuringVideoPlayback:Bool = true + private var _preferredForwardBufferDuration:Float = 0.0 + private var _playWhenInactive:Bool = false + private var _ignoreSilentSwitch:String! = "inherit" // inherit, ignore, obey + private var _mixWithOthers:String! = "inherit" // inherit, mix, duck + private var _resizeMode:String! = "AVLayerVideoGravityResizeAspectFill" + private var _fullscreen:Bool = false + private var _fullscreenAutorotate:Bool = true + private var _fullscreenOrientation:String! = "all" + private var _fullscreenPlayerPresented:Bool = false + private var _filterName:String! + private var _filterEnabled:Bool = false + private var _presentingViewController:UIViewController? + + private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? + private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver() + +#if canImport(RCTVideoCache) + private var _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler(self.playerItemPrepareText) +#endif + +#if TARGET_OS_IOS + private let _pip:RCTPictureInPicture = RCTPictureInPicture(self.onPictureInPictureStatusChanged, self.onRestoreUserInterfaceForPictureInPictureStop) +#endif + + // Events + @objc var onVideoLoadStart: RCTDirectEventBlock? + @objc var onVideoLoad: RCTDirectEventBlock? + @objc var onVideoBuffer: RCTDirectEventBlock? + @objc var onVideoError: RCTDirectEventBlock? + @objc var onVideoProgress: RCTDirectEventBlock? + @objc var onBandwidthUpdate: RCTDirectEventBlock? + @objc var onVideoSeek: RCTDirectEventBlock? + @objc var onVideoEnd: RCTDirectEventBlock? + @objc var onTimedMetadata: RCTDirectEventBlock? + @objc var onVideoAudioBecomingNoisy: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerWillPresent: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerDidPresent: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerWillDismiss: RCTDirectEventBlock? + @objc var onVideoFullscreenPlayerDidDismiss: RCTDirectEventBlock? + @objc var onReadyForDisplay: RCTDirectEventBlock? + @objc var onPlaybackStalled: RCTDirectEventBlock? + @objc var onPlaybackResume: RCTDirectEventBlock? + @objc var onPlaybackRateChange: RCTDirectEventBlock? + @objc var onVideoExternalPlaybackChange: RCTDirectEventBlock? + @objc var onPictureInPictureStatusChanged: RCTDirectEventBlock? + @objc var onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock? + @objc var onGetLicense: RCTDirectEventBlock? + + init(eventDispatcher:RCTEventDispatcher!) { + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + + _eventDispatcher = eventDispatcher + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillResignActive(notification:)), + name: UIApplication.willResignActiveNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground(notification:)), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillEnterForeground(notification:)), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(audioRouteChanged(notification:)), + name: AVAudioSession.routeChangeNotification, + object: nil + ) + _playerObserver._handlers = self + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + deinit { + NotificationCenter.default.removeObserver(self) + self.removePlayerLayer() + _playerObserver.clearPlayer() + } + + // MARK: - App lifecycle handlers + + @objc func applicationWillResignActive(notification:NSNotification!) { + if _playInBackground || _playWhenInactive || _paused {return} + + _player?.pause() + _player?.rate = 0.0 + } + + @objc func applicationDidEnterBackground(notification:NSNotification!) { + if _playInBackground { + // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html + _playerLayer?.player = nil + _playerViewController?.player = nil + } + } + + @objc func applicationWillEnterForeground(notification:NSNotification!) { + self.applyModifiers() + if _playInBackground { + _playerLayer?.player = _player + _playerViewController?.player = _player + } + } + + // MARK: - Audio events + + @objc func audioRouteChanged(notification:NSNotification!) { + if let userInfo = notification.userInfo { + let reason:AVAudioSession.RouteChangeReason! = userInfo[AVAudioSessionRouteChangeReasonKey] as? AVAudioSession.RouteChangeReason + // let previousRoute:NSNumber! = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? NSNumber + if reason == .oldDeviceUnavailable, let onVideoAudioBecomingNoisy = onVideoAudioBecomingNoisy { + onVideoAudioBecomingNoisy(["target": reactTag as Any]) + } + } + } + + // MARK: - Progress + + func sendProgressUpdate() { + if let video = _player?.currentItem, + video == nil || video.status != AVPlayerItem.Status.readyToPlay { + return + } + + let playerDuration:CMTime = RCTVideoUtils.playerItemDuration(_player) + if CMTIME_IS_INVALID(playerDuration) { + return + } + + let currentTime = _player?.currentTime() + let currentPlaybackTime = _player?.currentItem?.currentDate() + let duration = CMTimeGetSeconds(playerDuration) + let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) + + NotificationCenter.default.post(name: NSNotification.Name("RCTVideo_progress"), object: nil, userInfo: [ + "progress": NSNumber(value: currentTimeSecs / duration) + ]) + + if currentTimeSecs >= 0 { + onVideoProgress?([ + "currentTime": NSNumber(value: Float(currentTimeSecs)), + "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player), + "atValue": NSNumber(value: currentTime?.value ?? .zero), + "currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), + "target": reactTag, + "seekableDuration": RCTVideoUtils.calculateSeekableDuration(_player) + ]) + } + } + + // MARK: - Player and source + + @objc + func setSrc(_ source:NSDictionary!) { + _source = VideoSource(source) + removePlayerLayer() + _playerObserver.player = nil + _playerObserver.playerItem = nil + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0)) / Double(NSEC_PER_SEC), execute: { [weak self] in + guard let self = self else {return} + // perform on next run loop, otherwise other passed react-props may not be set + self.playerItemForSource(withCallback:{ (playerItem:AVPlayerItem!) in + self._player?.pause() + self._playerItem = playerItem + self._playerObserver.playerItem = self._playerItem + self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration) + self.setFilter(self._filterName) + if let maxBitRate = self._maxBitRate { + self._playerItem?.preferredPeakBitRate = Double(maxBitRate) + } + + self._player = AVPlayer(playerItem: self._playerItem) + self._playerObserver.player = self._player + + self._player?.actionAtItemEnd = .none + + if #available(iOS 10.0, *) { + self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) + } + + //Perform on next run loop, otherwise onVideoLoadStart is nil + self.onVideoLoadStart?([ + "src": [ + "uri": self._source?.uri ?? NSNull(), + "type": self._source?.type ?? NSNull(), + "isNetwork": NSNumber(value: self._source?.isNetwork ?? false) + ], + "drm": self._drm?.json ?? NSNull(), + "target": self.reactTag + ]) + + }) + }) + _videoLoadStarted = true + } + + @objc + func setDrm(_ drm:NSDictionary!) { + _drm = DRMParams(drm) + } + + func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?, withCallback handler:(AVPlayerItem?)->Void) { + if (_textTracks == nil) || _textTracks?.count==0 { + handler(AVPlayerItem(asset: asset)) + return + } + + // AVPlayer can't airplay AVMutableCompositions + _allowsExternalPlayback = false + + // sideload text tracks + let mixComposition:AVMutableComposition! = AVMutableComposition() + + let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first + let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid) + do { + try videoCompTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), + of: videoAsset, + at: .zero) + } catch { + } + + let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first + let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid) + do { + try audioCompTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), + of: audioAsset, + at: .zero) + } catch { + } + + var validTextTracks:[TextTrack] = [] + if let textTracks = _textTracks, let textTrackCount = _textTracks?.count { + for i in 0..Void) { + var asset:AVURLAsset! + guard let source = _source, source.uri != nil && source.uri != "" else { + DebugLog("Could not find video URL in source '\(_source)'") + return + } + + let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" + let url = source.isNetwork || source.isAsset + ? URL(string: source.uri ?? "") + : URL(fileURLWithPath: bundlePath) + + let assetOptions:NSMutableDictionary! = NSMutableDictionary() + + if url != nil && source.isNetwork { + if let headers = source.requestHeaders, headers.count > 0 { + assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying) + } + let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies + assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying) +#if canImport(RCTVideoCache) + if _videoCache.playerItemForSourceUsingCache(shouldCache:shouldCache, textTracks:_textTracks, uri:uri, assetOptions:assetOptions, handler:handler) { + return + } +#endif + + asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any]) + } else { + asset = AVURLAsset(url: url!) + } + + if _drm != nil { + _resouceLoaderDelegate = RCTResourceLoaderDelegate( + asset: asset, + drm: _drm, + onVideoError: onVideoError, + onGetLicense: onGetLicense, + reactTag: reactTag + ) + } + + self.playerItemPrepareText(asset: asset, assetOptions:assetOptions, withCallback:handler) + } + + // MARK: - Prop setters + + @objc + func setResizeMode(_ mode: String?) { + if _controls { + _playerViewController?.videoGravity = AVLayerVideoGravity(rawValue: mode ?? "") + } else { + _playerLayer?.videoGravity = AVLayerVideoGravity(rawValue: mode ?? "") + } + _resizeMode = mode + } + + @objc + func setPlayInBackground(_ playInBackground:Bool) { + _playInBackground = playInBackground + } + + @objc + func setPreventsDisplaySleepDuringVideoPlayback(_ preventsDisplaySleepDuringVideoPlayback:Bool) { + _preventsDisplaySleepDuringVideoPlayback = preventsDisplaySleepDuringVideoPlayback + self.applyModifiers() + } + + @objc + func setAllowsExternalPlayback(_ allowsExternalPlayback:Bool) { + _allowsExternalPlayback = allowsExternalPlayback + _player?.allowsExternalPlayback = _allowsExternalPlayback + } + + @objc + func setPlayWhenInactive(_ playWhenInactive:Bool) { + _playWhenInactive = playWhenInactive + } + + @objc + func setPictureInPicture(_ pictureInPicture:Bool) { +#if TARGET_OS_IOS + _pip.setPictureInPicture(pictureInPicture) +#endif + } + + @objc + func setRestoreUserInterfaceForPIPStopCompletionHandler(_ restore:Bool) { +#if TARGET_OS_IOS + _pip.setRestoreUserInterfaceForPIPStopCompletionHandler(restore) +#endif + } + + @objc + func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String!) { + _ignoreSilentSwitch = ignoreSilentSwitch + self.applyModifiers() + } + + @objc + func setMixWithOthers(_ mixWithOthers:String!) { + _mixWithOthers = mixWithOthers + self.applyModifiers() + } + + @objc + func setPaused(_ paused:Bool) { + if paused { + _player?.pause() + _player?.rate = 0.0 + } else { + let session:AVAudioSession! = AVAudioSession.sharedInstance() + var category:AVAudioSession.Category? = nil + var options:AVAudioSession.CategoryOptions? = nil + + if (_ignoreSilentSwitch == "ignore") { + category = AVAudioSession.Category.playback + } else if (_ignoreSilentSwitch == "obey") { + category = AVAudioSession.Category.ambient + } + + if (_mixWithOthers == "mix") { + options = .mixWithOthers + } else if (_mixWithOthers == "duck") { + options = .duckOthers + } + + if let category = category, let options = options { + do { + try session.setCategory(category, options: options) + } catch { + } + } else if let category = category, options == nil { + do { + try session.setCategory(category) + } catch { + } + } else if category == nil, let options = options { + do { + try session.setCategory(session.category, options: options) + } catch { + } + } + + if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling { + _player?.playImmediately(atRate: _rate) + } else { + _player?.play() + _player?.rate = _rate + } + _player?.rate = _rate + } + + _paused = paused + } + + @objc + func setCurrentTime(_ currentTime:Float) { + let info:NSDictionary! = [ + "time": NSNumber(value: currentTime), + "tolerance": NSNumber(value: 100) + ] + setSeek(info) + } + + @objc + func setSeek(_ info:NSDictionary!) { + let seekTime:NSNumber! = info["time"] as! NSNumber + let seekTolerance:NSNumber! = info["tolerance"] as! NSNumber + + let timeScale:Int = 1000 + + let item:AVPlayerItem! = _player?.currentItem + guard item != nil && item.status == AVPlayerItem.Status.readyToPlay else { + _pendingSeek = true + _pendingSeekTime = seekTime.floatValue + return + } + + // TODO check loadedTimeRanges + let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime.floatValue), preferredTimescale: Int32(timeScale)) + let current:CMTime = item.currentTime() + // TODO figure out a good tolerance level + let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance.floatValue), timescale: Int32(timeScale)) + let wasPaused:Bool = _paused + + guard CMTimeCompare(current, cmSeekTime) != 0 else { return } + if !wasPaused { _player?.pause() } + + _player?.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ [weak self] (finished:Bool) in + guard let self = self else { return } + + self._playerObserver.addTimeObserverIfNotSet() + if !wasPaused { + self.setPaused(false) + } + self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), + "seekTime": seekTime, + "target": self.reactTag]) + }) + + _pendingSeek = false + } + + @objc + func setRate(_ rate:Float) { + _rate = rate + applyModifiers() + } + + @objc + func setMuted(_ muted:Bool) { + _muted = muted + applyModifiers() + } + + @objc + func setVolume(_ volume:Float) { + _volume = volume + applyModifiers() + } + + @objc + func setMaxBitRate(_ maxBitRate:Float) { + _maxBitRate = maxBitRate + _playerItem?.preferredPeakBitRate = Double(maxBitRate) + } + + @objc + func setPreferredForwardBufferDuration(_ preferredForwardBufferDuration:Float) { + _preferredForwardBufferDuration = preferredForwardBufferDuration + if #available(iOS 10.0, *) { + _playerItem?.preferredForwardBufferDuration = TimeInterval(preferredForwardBufferDuration) + } else { + // Fallback on earlier versions + } + } + + @objc + func setAutomaticallyWaitsToMinimizeStalling(_ waits:Bool) { + _automaticallyWaitsToMinimizeStalling = waits + if #available(iOS 10.0, *) { + _player?.automaticallyWaitsToMinimizeStalling = waits + } else { + // Fallback on earlier versions + } + } + + + func applyModifiers() { + if _muted { + if !_controls { + _player?.volume = 0 + } + _player?.isMuted = true + } else { + _player?.volume = _volume + _player?.isMuted = false + } + + if #available(iOS 12.0, *) { + _player?.preventsDisplaySleepDuringVideoPlayback = _preventsDisplaySleepDuringVideoPlayback + } else { + // Fallback on earlier versions + } + + if let _maxBitRate = _maxBitRate { + setMaxBitRate(_maxBitRate) + } + + setSelectedAudioTrack(_selectedAudioTrackCriteria) + setSelectedTextTrack(_selectedTextTrackCriteria) + setResizeMode(_resizeMode) + setRepeat(_repeat) + setPaused(_paused) + setControls(_controls) + setAllowsExternalPlayback(_allowsExternalPlayback) + } + + @objc + func setRepeat(_ `repeat`: Bool) { + _repeat = `repeat` + } + + + + @objc + func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary!) { + setSelectedAudioTrack(SelectedTrackCriteria(selectedAudioTrack)) + } + + func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria!) { + _selectedAudioTrackCriteria = selectedAudioTrack + RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.audible, + criteria:_selectedAudioTrackCriteria) + } + + @objc + func setSelectedTextTrack(_ selectedTextTrack:NSDictionary!) { + setSelectedTextTrack(SelectedTrackCriteria(selectedTextTrack)) + } + + func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria!) { + _selectedTextTrackCriteria = selectedTextTrack + if (_textTracks != nil) { // sideloaded text tracks + RCTPlayerOperations.setSideloadedText(player:_player, textTracks:_textTracks, criteria:_selectedTextTrackCriteria) + } else { // text tracks included in the HLS playlist + RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.legible, + criteria:_selectedTextTrackCriteria) + } + } + + @objc + func setTextTracks(_ textTracks:[NSDictionary]!) { + setTextTracks(textTracks.map { TextTrack($0) }) + } + + func setTextTracks(_ textTracks:[TextTrack]!) { + _textTracks = textTracks + + // in case textTracks was set after selectedTextTrack + if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)} + } + + @objc + func setFullscreen(_ fullscreen:Bool) { + if fullscreen && !_fullscreenPlayerPresented && _player != nil { + // Ensure player view controller is not null + if _playerViewController == nil { + self.usePlayerViewController() + } + + // Set presentation style to fullscreen + _playerViewController?.modalPresentationStyle = .fullScreen + + // Find the nearest view controller + var viewController:UIViewController! = self.firstAvailableUIViewController() + if (viewController == nil) { + let keyWindow:UIWindow! = UIApplication.shared.keyWindow + viewController = keyWindow.rootViewController + if viewController.children.count > 0 + { + viewController = viewController.children.last + } + } + if viewController != nil { + _presentingViewController = viewController + + self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any]) + + viewController.present(viewController, animated:true, completion:{ + self._playerViewController?.showsPlaybackControls = true + self._fullscreenPlayerPresented = fullscreen + self._playerViewController?.autorotate = self._fullscreenAutorotate + + self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag]) + + }) + } + } else if !fullscreen && _fullscreenPlayerPresented, let _playerViewController = _playerViewController { + self.videoPlayerViewControllerWillDismiss(playerViewController: _playerViewController) + _presentingViewController?.dismiss(animated: true, completion:{ + self.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController) + }) + } + } + + @objc + func setFullscreenAutorotate(_ autorotate:Bool) { + _fullscreenAutorotate = autorotate + if _fullscreenPlayerPresented { + _playerViewController?.autorotate = autorotate + } + } + + @objc + func setFullscreenOrientation(_ orientation:String!) { + _fullscreenOrientation = orientation + if _fullscreenPlayerPresented { + _playerViewController?.preferredOrientation = orientation + } + } + + func usePlayerViewController() { + guard _player != nil else { return } + + if _playerViewController == nil { + _playerViewController = createPlayerViewController(player: _player, withPlayerItem:_playerItem) + + } + // to prevent video from being animated when resizeMode is 'cover' + // resize mode must be set before subview is added + setResizeMode(_resizeMode) + + guard let _playerViewController = _playerViewController else { return } + + if _controls { + let viewController:UIViewController! = self.reactViewController() + viewController.addChild(_playerViewController) + self.addSubview(_playerViewController.view) + } + + _playerObserver.playerViewController = _playerViewController + } + + func createPlayerViewController(player:AVPlayer!, withPlayerItem playerItem:AVPlayerItem!) -> RCTVideoPlayerViewController! { + let viewController:RCTVideoPlayerViewController! = RCTVideoPlayerViewController() + viewController.showsPlaybackControls = true + viewController.rctDelegate = self + viewController.preferredOrientation = _fullscreenOrientation + + viewController.view.frame = self.bounds + viewController.player = player + return viewController + } + + func usePlayerLayer() { + if let _player = _player { + _playerLayer = AVPlayerLayer(player: _player) + _playerLayer?.frame = self.bounds + _playerLayer?.needsDisplayOnBoundsChange = true + + // to prevent video from being animated when resizeMode is 'cover' + // resize mode must be set before layer is added + setResizeMode(_resizeMode) + _playerObserver.playerLayer = _playerLayer + + if let _playerLayer = _playerLayer { + self.layer.addSublayer(_playerLayer) + } + self.layer.needsDisplayOnBoundsChange = true +#if TARGET_OS_IOS + _pip.setupPipController(_playerLayer) +#endif + } + } + + @objc + func setControls(_ controls:Bool) { + if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) + { + _controls = controls + if _controls + { + self.removePlayerLayer() + self.usePlayerViewController() + } + else + { + _playerViewController?.view.removeFromSuperview() + _playerViewController = nil + _playerObserver.playerViewController = nil + self.usePlayerLayer() + } + } + } + + @objc + func setProgressUpdateInterval(_ progressUpdateInterval:Float) { + _playerObserver.replaceTimeObserverIfSet(Float64(progressUpdateInterval)) + } + + func removePlayerLayer() { + _resouceLoaderDelegate = nil + _playerLayer?.removeFromSuperlayer() + _playerLayer = nil + _playerObserver.playerLayer = nil + } + + // MARK: - RCTVideoPlayerViewControllerDelegate + + func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) { + if _playerViewController == playerViewController && _fullscreenPlayerPresented, let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss { + _playerObserver.removePlayerViewControllerObservers() + onVideoFullscreenPlayerWillDismiss(["target": reactTag as Any]) + } + } + + + func videoPlayerViewControllerDidDismiss(playerViewController:AVPlayerViewController) { + if _playerViewController == playerViewController && _fullscreenPlayerPresented { + _fullscreenPlayerPresented = false + _presentingViewController = nil + _playerViewController = nil + _playerObserver.playerViewController = nil + self.applyModifiers() + + onVideoFullscreenPlayerDidDismiss?(["target": reactTag as Any]) + } + } + + @objc + func setFilter(_ filterName:String!) { + _filterName = filterName + + if !_filterEnabled { + return + } else if let uri = _source?.uri, uri.contains("m3u8") { + return // filters don't work for HLS... return + } else if _playerItem?.asset == nil { + return + } + + let filter:CIFilter! = CIFilter(name: filterName) + if #available(iOS 9.0, *), let _playerItem = _playerItem { + self._playerItem?.videoComposition = AVVideoComposition( + asset: _playerItem.asset, + applyingCIFiltersWithHandler: { (request:AVAsynchronousCIImageFilteringRequest) in + if filter == nil { + request.finish(with: request.sourceImage, context:nil) + } else { + let image:CIImage! = request.sourceImage.clampedToExtent() + filter.setValue(image, forKey:kCIInputImageKey) + let output:CIImage! = filter.outputImage?.cropped(to: request.sourceImage.extent) + request.finish(with: output, context:nil) + } + }) + } else { + // Fallback on earlier versions + } + } + + @objc + func setFilterEnabled(_ filterEnabled:Bool) { + _filterEnabled = filterEnabled + } + + // MARK: - React View Management + + func insertReactSubview(view:UIView!, atIndex:Int) { + // We are early in the game and somebody wants to set a subview. + // That can only be in the context of playerViewController. + if !_controls && (_playerLayer == nil) && (_playerViewController == nil) { + setControls(true) + } + + if _controls { + view.frame = self.bounds + _playerViewController?.contentOverlayView?.insertSubview(view, at:atIndex) + } else { + RCTLogError("video cannot have any subviews") + } + return + } + + func removeReactSubview(subview:UIView!) { + if _controls { + subview.removeFromSuperview() + } else { + RCTLog("video cannot have any subviews") + } + return + } + + override func layoutSubviews() { + super.layoutSubviews() + if _controls, let _playerViewController = _playerViewController { + _playerViewController.view.frame = bounds + + // also adjust all subviews of contentOverlayView + for subview in _playerViewController.contentOverlayView?.subviews ?? [] { + subview.frame = bounds + } + } else { + CATransaction.begin() + CATransaction.setAnimationDuration(0) + _playerLayer?.frame = bounds + CATransaction.commit() + } + } + + // MARK: - Lifecycle + + override func removeFromSuperview() { + _player?.pause() + _player = nil + _playerObserver.clearPlayer() + + self.removePlayerLayer() + + if let _playerViewController = _playerViewController { + _playerViewController.view.removeFromSuperview() + _playerViewController.rctDelegate = nil + _playerViewController.player = nil + self._playerViewController = nil + _playerObserver.playerViewController = nil + } + + _eventDispatcher = nil + NotificationCenter.default.removeObserver(self) + + super.removeFromSuperview() + } + + // MARK: - Export + + @objc + func save(options:NSDictionary!, resolve: @escaping RCTPromiseResolveBlock, reject:@escaping RCTPromiseRejectBlock) { + RCTVideoSave.save( + options:options, + resolve:resolve, + reject:reject, + playerItem:_playerItem + ) + } + + func setLicenseResult(_ license:String!) { + _resouceLoaderDelegate?.setLicenseResult(license) + } + + func setLicenseResultError(_ error:String!) { + _resouceLoaderDelegate?.setLicenseResultError(error) + } + + // MARK: - RCTPlayerObserverHandler + + func handleTimeUpdate(time:CMTime) { + sendProgressUpdate() + } + + func handleReadyForDisplay(changeObject: Any, change:NSKeyValueObservedChange) { + onReadyForDisplay?([ + "target": reactTag + ]) + } + + // When timeMetadata is read the event onTimedMetadata is triggered + func handleTimeMetadataChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange<[AVMetadataItem]?>) { + guard let newValue = change.newValue, let _items = newValue, _items.count > 0 else { + return + } + + var metadata: [[String:String?]?] = [] + for item in _items { + let value = item.value as? String + let identifier = item.identifier?.rawValue + + if let value = value { + metadata.append(["value":value, "identifier":identifier]) + } + } + + onTimedMetadata?([ + "target": reactTag, + "metadata": metadata + ]) + } + + // Handle player item status change. + func handlePlayerItemStatusChange(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + guard let _playerItem = _playerItem else { + return + } + + if _playerItem.status == .readyToPlay { + handleReadyToPlay() + } else if _playerItem.status == .failed { + handlePlaybackFailed() + } + } + + func handleReadyToPlay() { + guard let _playerItem = _playerItem else { return } + var duration:Float = Float(CMTimeGetSeconds(_playerItem.asset.duration)) + + if duration.isNaN { + duration = 0.0 + } + + var width: Float? = nil + var height: Float? = nil + var orientation = "undefined" + + if _playerItem.asset.tracks(withMediaType: AVMediaType.video).count > 0 { + let videoTrack = _playerItem.asset.tracks(withMediaType: .video)[0] + width = Float(videoTrack.naturalSize.width) + height = Float(videoTrack.naturalSize.height) + let preferredTransform = videoTrack.preferredTransform + + if (videoTrack.naturalSize.width == preferredTransform.tx + && videoTrack.naturalSize.height == preferredTransform.ty) + || (preferredTransform.tx == 0 && preferredTransform.ty == 0) + { + orientation = "landscape" + } else { + orientation = "portrait" + } + } else if _playerItem.presentationSize.height != 0.0 { + width = Float(_playerItem.presentationSize.width) + height = Float(_playerItem.presentationSize.height) + orientation = _playerItem.presentationSize.width > _playerItem.presentationSize.height ? "landscape" : "portrait" + } + + if _pendingSeek { + setCurrentTime(_pendingSeekTime) + _pendingSeek = false + } + + if _videoLoadStarted { + onVideoLoad?(["duration": NSNumber(value: duration), + "currentTime": NSNumber(value: Float(CMTimeGetSeconds(_playerItem.currentTime()))), + "canPlayReverse": NSNumber(value: _playerItem.canPlayReverse), + "canPlayFastForward": NSNumber(value: _playerItem.canPlayFastForward), + "canPlaySlowForward": NSNumber(value: _playerItem.canPlaySlowForward), + "canPlaySlowReverse": NSNumber(value: _playerItem.canPlaySlowReverse), + "canStepBackward": NSNumber(value: _playerItem.canStepBackward), + "canStepForward": NSNumber(value: _playerItem.canStepForward), + "naturalSize": [ + "width": width != nil ? NSNumber(value: width!) : "undefinded", + "height": width != nil ? NSNumber(value: height!) : "undefinded", + "orientation": orientation + ], + "audioTracks": RCTVideoUtils.getAudioTrackInfo(_player), + "textTracks": _textTracks ?? RCTVideoUtils.getTextTrackInfo(_player), + "target": reactTag as Any]) + } + _videoLoadStarted = false + _playerObserver.attachPlayerEventListeners() + applyModifiers() + } + + func handlePlaybackFailed() { + guard let _playerItem = _playerItem else { return } + onVideoError?( + [ + "error": [ + "code": NSNumber(value: (_playerItem.error! as NSError).code), + "localizedDescription": _playerItem.error?.localizedDescription == nil ? "" : _playerItem.error?.localizedDescription, + "localizedFailureReason": ((_playerItem.error! as NSError).localizedFailureReason == nil ? "" : (_playerItem.error! as NSError).localizedFailureReason) ?? "", + "localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ? "" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "", + "domain": (_playerItem.error as! NSError).domain + ], + "target": reactTag + ]) + } + + func handlePlaybackBufferKeyEmpty(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + _playerBufferEmpty = true + onVideoBuffer?(["isBuffering": true, "target": reactTag as Any]) + } + + // Continue playing (or not if paused) after being paused due to hitting an unbuffered zone. + func handlePlaybackLikelyToKeepUp(playerItem:AVPlayerItem, change:NSKeyValueObservedChange) { + if (!(_controls || _fullscreenPlayerPresented) || _playerBufferEmpty) && ((_playerItem?.isPlaybackLikelyToKeepUp) != nil) { + setPaused(_paused) + } + _playerBufferEmpty = false + onVideoBuffer?(["isBuffering": false, "target": reactTag as Any]) + } + + func handlePlaybackRateChange(player: AVPlayer, change: NSKeyValueObservedChange) { + guard let _player = _player else { return } + onPlaybackRateChange?(["playbackRate": NSNumber(value: _player.rate), + "target": reactTag as Any]) + if _playbackStalled && _player.rate > 0 { + onPlaybackResume?(["playbackRate": NSNumber(value: _player.rate), + "target": reactTag as Any]) + _playbackStalled = false + } + } + + func handleExternalPlaybackActiveChange(player: AVPlayer, change: NSKeyValueObservedChange) { + guard let _player = _player else { return } + onVideoExternalPlaybackChange?(["isExternalPlaybackActive": NSNumber(value: _player.isExternalPlaybackActive), + "target": reactTag as Any]) + } + + func handleViewControllerOverlayViewFrameChange(overlayView:UIView, change:NSKeyValueObservedChange) { + let oldRect = change.oldValue + let newRect = change.newValue + if !oldRect!.equalTo(newRect!) { + if newRect!.equalTo(UIScreen.main.bounds) { + NSLog("in fullscreen") + + self.reactViewController().view.frame = UIScreen.main.bounds + self.reactViewController().view.setNeedsLayout() + } else {NSLog("not fullscreen")} + } + } + + @objc func handleDidFailToFinishPlaying(notification:NSNotification!) { + let error:NSError! = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError + onVideoError?( + [ + "error": [ + "code": NSNumber(value: (error as NSError).code), + "localizedDescription": error.localizedDescription ?? "", + "localizedFailureReason": (error as NSError).localizedFailureReason ?? "", + "localizedRecoverySuggestion": (error as NSError).localizedRecoverySuggestion ?? "", + "domain": (error as NSError).domain + ], + "target": reactTag + ]) + } + + @objc func handlePlaybackStalled(notification:NSNotification!) { + onPlaybackStalled?(["target": reactTag as Any]) + _playbackStalled = true + } + + @objc func handlePlayerItemDidReachEnd(notification:NSNotification!) { + onVideoEnd?(["target": reactTag as Any]) + + if _repeat { + let item:AVPlayerItem! = notification.object as? AVPlayerItem + item.seek(to: CMTime.zero) + self.applyModifiers() + } else { + _playerObserver.removePlayerTimeObserver() + } + } + + //unused +// @objc func handleAVPlayerAccess(notification:NSNotification!) { +// let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog() +// let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last +// +// /* TODO: get this working +// if (self.onBandwidthUpdate) { +// self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); +// } +// */ +// } +} diff --git a/ios/Video/RCTVideoManager.h b/ios/Video/RCTVideoManager.h deleted file mode 100644 index b3bfccb5..00000000 --- a/ios/Video/RCTVideoManager.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface RCTVideoManager : RCTViewManager - -@end diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 000a9e83..ece0a1e3 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -1,22 +1,7 @@ -#import "RCTVideoManager.h" -#import "RCTVideo.h" #import -#import -#import +#import "React/RCTViewManager.h" -@implementation RCTVideoManager - -RCT_EXPORT_MODULE(); - -- (UIView *)view -{ - return [[RCTVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; -} - -- (dispatch_queue_t)methodQueue -{ - return self.bridge.uiManager.methodQueue; -} +@interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary); @@ -49,6 +34,7 @@ RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); + /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock); @@ -70,64 +56,18 @@ RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGetLicense, RCTDirectEventBlock); -RCT_REMAP_METHOD(save, - options:(NSDictionary *)options - reactTag:(nonnull NSNumber *)reactTag - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTVideo *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RCTVideo class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); - } else { - [view save:options resolve:resolve reject:reject]; - } - }]; -}; -RCT_REMAP_METHOD(setLicenseResult, - license:(NSString *)license - reactTag:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTVideo *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RCTVideo class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); - } else { - [view setLicenseResult:license]; - } - }]; -}; - -RCT_REMAP_METHOD(setLicenseResultError, - error:(NSString *)error - reactTag:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager prependUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RCTVideo *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RCTVideo class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RCTVideo, got: %@", view); - } else { - [view setLicenseResultError:error]; - } - }]; -}; RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock); -- (NSDictionary *)constantsToExport -{ - return @{ - @"ScaleNone": AVLayerVideoGravityResizeAspect, - @"ScaleToFill": AVLayerVideoGravityResize, - @"ScaleAspectFit": AVLayerVideoGravityResizeAspect, - @"ScaleAspectFill": AVLayerVideoGravityResizeAspectFill - }; -} +RCT_EXTERN_METHOD(save:(NSDictionary *)options + reactTag:(nonnull NSNumber *)reactTag + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) -+ (BOOL)requiresMainQueueSetup -{ - return YES; -} +RCT_EXTERN_METHOD(setLicenseResult:(NSString *)license + reactTag:(nonnull NSNumber *)reactTag) + +RCT_EXTERN_METHOD(setLicenseResultError(NSString *)error + reactTag:(nonnull NSNumber *)reactTag) @end diff --git a/ios/Video/RCTVideoManager.swift b/ios/Video/RCTVideoManager.swift new file mode 100644 index 00000000..bf42f707 --- /dev/null +++ b/ios/Video/RCTVideoManager.swift @@ -0,0 +1,63 @@ +import AVFoundation +import React + +@objc(RCTVideoManager) +class RCTVideoManager: RCTViewManager { + + override func view() -> UIView { + return RCTVideo(eventDispatcher: bridge.eventDispatcher()) + } + + func methodQueue() -> DispatchQueue { + return bridge.uiManager.methodQueue + } + + @objc(save:reactTag:resolver:rejecter:) + func save(options: NSDictionary, reactTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void { + bridge.uiManager.prependUIBlock({_ , viewRegistry in + let view = viewRegistry?[reactTag] + if !(view is RCTVideo) { + RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) + } else if let view = view as? RCTVideo { + view.save(options: options, resolve: resolve, reject: reject) + } + }) + } + + @objc(setLicenseResult:reactTag:) + func setLicenseResult(license: NSString, reactTag: NSNumber) -> Void { + bridge.uiManager.prependUIBlock({_ , viewRegistry in + let view = viewRegistry?[reactTag] + if !(view is RCTVideo) { + RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) + } else if let view = view as? RCTVideo { + view.setLicenseResult(license as String) + } + }) + } + + @objc(setLicenseResultError:reactTag:) + func setLicenseResultError(error: NSString, reactTag: NSNumber) -> Void { + bridge.uiManager.prependUIBlock({_ , viewRegistry in + let view = viewRegistry?[reactTag] + if !(view is RCTVideo) { + RCTLogError("Invalid view returned from registry, expecting RCTVideo, got: %@", String(describing: view)) + } else if let view = view as? RCTVideo { + view.setLicenseResultError(error as String) + } + }) + } + + override func constantsToExport() -> [AnyHashable : Any]? { + return [ + "ScaleNone": AVLayerVideoGravity.resizeAspect, + "ScaleToFill": AVLayerVideoGravity.resize, + "ScaleAspectFit": AVLayerVideoGravity.resizeAspect, + "ScaleAspectFill": AVLayerVideoGravity.resizeAspectFill + ] + } + + override class func requiresMainQueueSetup() -> Bool { + return true + } +} diff --git a/ios/Video/RCTVideoPlayerViewController.h b/ios/Video/RCTVideoPlayerViewController.h deleted file mode 100644 index ed9ebdde..00000000 --- a/ios/Video/RCTVideoPlayerViewController.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// RCTVideoPlayerViewController.h -// RCTVideo -// -// Created by Stanisław Chmiela on 31.03.2016. -// Copyright © 2016 Facebook. All rights reserved. -// - -#import -#import "RCTVideo.h" -#import "RCTVideoPlayerViewControllerDelegate.h" - -@interface RCTVideoPlayerViewController : AVPlayerViewController -@property (nonatomic, weak) id rctDelegate; - -// Optional paramters -@property (nonatomic, weak) NSString* preferredOrientation; -@property (nonatomic) BOOL autorotate; - -@end diff --git a/ios/Video/RCTVideoPlayerViewController.m b/ios/Video/RCTVideoPlayerViewController.m deleted file mode 100644 index 548a06ce..00000000 --- a/ios/Video/RCTVideoPlayerViewController.m +++ /dev/null @@ -1,43 +0,0 @@ -#import "RCTVideoPlayerViewController.h" - -@interface RCTVideoPlayerViewController () - -@end - -@implementation RCTVideoPlayerViewController - -- (BOOL)shouldAutorotate { - - if (self.autorotate || self.preferredOrientation.lowercaseString == nil || [self.preferredOrientation.lowercaseString isEqualToString:@"all"]) - return YES; - - return NO; -} - -- (void)viewDidDisappear:(BOOL)animated -{ - [super viewDidDisappear:animated]; - [_rctDelegate videoPlayerViewControllerWillDismiss:self]; - [_rctDelegate videoPlayerViewControllerDidDismiss:self]; -} - -#if !TARGET_OS_TV -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return UIInterfaceOrientationMaskAll; -} - -- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { - if ([self.preferredOrientation.lowercaseString isEqualToString:@"landscape"]) { - return UIInterfaceOrientationLandscapeRight; - } - else if ([self.preferredOrientation.lowercaseString isEqualToString:@"portrait"]) { - return UIInterfaceOrientationPortrait; - } - else { // default case - UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; - return orientation; - } -} -#endif - -@end diff --git a/ios/Video/RCTVideoPlayerViewController.swift b/ios/Video/RCTVideoPlayerViewController.swift new file mode 100644 index 00000000..95c926af --- /dev/null +++ b/ios/Video/RCTVideoPlayerViewController.swift @@ -0,0 +1,44 @@ +import AVKit + +class RCTVideoPlayerViewController: AVPlayerViewController { + + var rctDelegate:RCTVideoPlayerViewControllerDelegate! + + // Optional paramters + var preferredOrientation:String? + var autorotate:Bool? + + func shouldAutorotate() -> Bool { + + if autorotate! || preferredOrientation == nil || (preferredOrientation!.lowercased() == "all") { + return true + } + + return false + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self) + rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self) + } + + #if !TARGET_OS_TV + + func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { + return .all + } + + func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation { + if preferredOrientation?.lowercased() == "landscape" { + return .landscapeRight + } else if preferredOrientation?.lowercased() == "portrait" { + return .portrait + } else { + // default case + let orientation = UIApplication.shared.statusBarOrientation + return orientation + } + } + #endif +} diff --git a/ios/Video/RCTVideoPlayerViewControllerDelegate.h b/ios/Video/RCTVideoPlayerViewControllerDelegate.h deleted file mode 100644 index e84b3f52..00000000 --- a/ios/Video/RCTVideoPlayerViewControllerDelegate.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import "AVKit/AVKit.h" - -@protocol RCTVideoPlayerViewControllerDelegate -- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController; -- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController; -@end diff --git a/ios/Video/RCTVideoPlayerViewControllerDelegate.swift b/ios/Video/RCTVideoPlayerViewControllerDelegate.swift new file mode 100644 index 00000000..6635975f --- /dev/null +++ b/ios/Video/RCTVideoPlayerViewControllerDelegate.swift @@ -0,0 +1,7 @@ +import Foundation +import AVKit + +protocol RCTVideoPlayerViewControllerDelegate : NSObject { + func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) + func videoPlayerViewControllerDidDismiss(playerViewController:AVPlayerViewController) +} diff --git a/ios/Video/UIView+FindUIViewController.h b/ios/Video/UIView+FindUIViewController.h deleted file mode 100644 index 09214261..00000000 --- a/ios/Video/UIView+FindUIViewController.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// UIView+FindUIViewController.h -// RCTVideo -// -// Created by Stanisław Chmiela on 31.03.2016. -// Copyright © 2016 Facebook. All rights reserved. -// -// Source: http://stackoverflow.com/a/3732812/1123156 - -#import - -@interface UIView (FindUIViewController) -- (UIViewController *) firstAvailableUIViewController; -- (id) traverseResponderChainForUIViewController; -@end diff --git a/ios/Video/UIView+FindUIViewController.m b/ios/Video/UIView+FindUIViewController.m deleted file mode 100644 index c29c742a..00000000 --- a/ios/Video/UIView+FindUIViewController.m +++ /dev/null @@ -1,21 +0,0 @@ -// Source: http://stackoverflow.com/a/3732812/1123156 - -#import "UIView+FindUIViewController.h" - -@implementation UIView (FindUIViewController) -- (UIViewController *) firstAvailableUIViewController { - // convenience function for casting and to "mask" the recursive function - return (UIViewController *)[self traverseResponderChainForUIViewController]; -} - -- (id) traverseResponderChainForUIViewController { - id nextResponder = [self nextResponder]; - if ([nextResponder isKindOfClass:[UIViewController class]]) { - return nextResponder; - } else if ([nextResponder isKindOfClass:[UIView class]]) { - return [nextResponder traverseResponderChainForUIViewController]; - } else { - return nil; - } -} -@end diff --git a/ios/Video/UIView+FindUIViewController.swift b/ios/Video/UIView+FindUIViewController.swift new file mode 100644 index 00000000..2947e1e3 --- /dev/null +++ b/ios/Video/UIView+FindUIViewController.swift @@ -0,0 +1,18 @@ +// Source: http://stackoverflow.com/a/3732812/1123156 + +extension UIView { + func firstAvailableUIViewController() -> UIViewController? { + // convenience function for casting and to "mask" the recursive function + return traverseResponderChainForUIViewController() + } + + func traverseResponderChainForUIViewController() -> UIViewController? { + if let nextUIViewController = next as? UIViewController { + return nextUIViewController + } else if let nextUIView = next as? UIView { + return nextUIView.traverseResponderChainForUIViewController() + } else { + return nil + } + } +} diff --git a/ios/VideoCaching/RCTVideoCachingHandler.swift b/ios/VideoCaching/RCTVideoCachingHandler.swift new file mode 100644 index 00000000..8e37f11a --- /dev/null +++ b/ios/VideoCaching/RCTVideoCachingHandler.swift @@ -0,0 +1,81 @@ +import Foundation +import AVFoundation +import DVAssetLoaderDelegate + +class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { + + private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance() + private var _playerItemPrepareText: (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void + + init(_ playerItemPrepareText: @escaping (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void) { + _playerItemPrepareText = playerItemPrepareText + } + + func playerItemForSourceUsingCache(shouldCache:Bool, textTracks:[AnyObject]?, uri:String, assetOptions:NSMutableDictionary, handler:@escaping (AVPlayerItem?)->Void) -> Bool { + if shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) { + /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying + * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. + * Until this is fixed, we need to bypass caching when text tracks are specified. + */ + DebugLog("Caching is not supported for uri '\(uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + playerItemForSourceUsingCache(uri: uri, assetOptions:assetOptions, withCallback:handler) + return true + } + return false + } + + func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!, withCallback handler: @escaping (AVPlayerItem?)->Void) { + let url = URL(string: uri) + _videoCache.getItemForUri(uri, withCallback:{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in + guard let self = self else { return } + switch (videoCacheStatus) { + case .missingFileExtension: + DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) + self._playerItemPrepareText(asset, options, handler) + return + + case .unsupportedFileExtension: + DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") + let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) + self._playerItemPrepareText(asset, options, handler) + return + + default: + if let cachedAsset = cachedAsset { + DebugLog("Playing back uri '\(uri)' from cache") + // See note in playerItemForSource about not being able to support text tracks & caching + handler(AVPlayerItem(asset: cachedAsset)) + return + } + } + + let asset:DVURLAsset! = DVURLAsset(url:url, options:options as! [String : Any], networkTimeout:10000) + asset.loaderDelegate = self + + /* More granular code to have control over the DVURLAsset + let resourceLoaderDelegate = DVAssetLoaderDelegate(url: url) + resourceLoaderDelegate.delegate = self + let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false) + components?.scheme = DVAssetLoaderDelegate.scheme() + var asset: AVURLAsset? = nil + if let url = components?.url { + asset = AVURLAsset(url: url, options: options) + } + asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main) + */ + + handler(AVPlayerItem(asset: asset)) + }) + } + + // MARK: - DVAssetLoaderDelegate + + func dvAssetLoaderDelegate(loaderDelegate:DVAssetLoaderDelegate!, didLoadData data:NSData!, forURL url:NSURL!) { + _videoCache.storeItem(data as Data?, forUri:url.absoluteString, withCallback:{ (success:Bool) in + DebugLog("Cache data stored successfully 🎉") + }) + } + +} + diff --git a/react-native-video.podspec b/react-native-video.podspec index 7013f957..5c3f5e5d 100644 --- a/react-native-video.podspec +++ b/react-native-video.podspec @@ -16,8 +16,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.subspec "Video" do |ss| - ss.source_files = "ios/Video/*.{h,m}" - s.static_framework = true + ss.source_files = "ios/Video/**/*.{h,m,swift}" end s.subspec "VideoCaching" do |ss| @@ -25,14 +24,15 @@ Pod::Spec.new do |s| ss.dependency "SPTPersistentCache", "~> 1.1.0" ss.dependency "DVAssetLoaderDelegate", "~> 0.3.1" - ss.source_files = "ios/VideoCaching/**/*.{h,m}" - s.static_framework = true + ss.source_files = "ios/VideoCaching/**/*.{h,m,swift}" end s.dependency "React-Core" s.default_subspec = "Video" + s.static_framework = true + s.xcconfig = { 'OTHER_LDFLAGS': '-ObjC', } From e27baeb065e407256b6f06e9f124b22e15342cac Mon Sep 17 00:00:00 2001 From: Nick Fujita Date: Thu, 28 Oct 2021 10:34:05 +0900 Subject: [PATCH 029/194] VEX-5938: Update resource loader to handle encrypted local files (#12) Adds offline decryption key and uses it to decrypt content during offline playback Jira: VEX-5938 https://jira.tenkasu.net/browse/VEX-5938 - Update to accept scheme for key required to play offline playback - Uses provided scheme to intercept call from player and return the key - Fixes player item observer removal pattern ### Reviews - Major reviewer (domain expert): @armadilio3 --- README.md | 13 + Video.js | 1 + ios/Video/Features/RCTPlayerObserver.swift | 32 +- ios/Video/Features/RCTPlayerOperations.swift | 20 ++ .../Features/RCTResourceLoaderDelegate.swift | 239 +++++--------- ios/Video/Features/RCTVideoDRM.swift | 168 ++++++++++ .../Features/RCTVideoErrorHandling.swift | 32 +- ios/Video/Features/RCTVideoUtils.swift | 107 +++++++ ios/Video/RCTVideo.swift | 294 +++++++----------- ios/Video/RCTVideoManager.m | 1 + ios/VideoCaching/RCTVideoCachingHandler.swift | 42 +-- react-native-video.podspec | 3 +- 12 files changed, 573 insertions(+), 379 deletions(-) create mode 100644 ios/Video/Features/RCTVideoDRM.swift diff --git a/README.md b/README.md index d46e8d39..ca6745d3 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ var styles = StyleSheet.create({ * [trackId](#trackId) * [useTextureView](#usetextureview) * [volume](#volume) +* [localSourceEncryptionKeyScheme](#localSourceEncryptionKeyScheme) ### Event props * [onAudioBecomingNoisy](#onaudiobecomingnoisy) @@ -915,6 +916,18 @@ Adjust the volume. Platforms: all +#### localSourceEncryptionKeyScheme +Set the url scheme for stream encryption key for local assets + +Type: String + +Example: +``` +localSourceEncryptionKeyScheme="my-offline-key" +``` + +Platforms: iOS + ### Event props diff --git a/Video.js b/Video.js index 88d90324..62a179a4 100644 --- a/Video.js +++ b/Video.js @@ -418,6 +418,7 @@ Video.propTypes = { certificateUrl: PropTypes.string, getLicense: PropTypes.func, }), + localSourceEncryptionKeyScheme: PropTypes.string, minLoadRetryCount: PropTypes.number, maxBitRate: PropTypes.number, resizeMode: PropTypes.string, diff --git a/ios/Video/Features/RCTPlayerObserver.swift b/ios/Video/Features/RCTPlayerObserver.swift index d17b27da..2f17a19c 100644 --- a/ios/Video/Features/RCTPlayerObserver.swift +++ b/ios/Video/Features/RCTPlayerObserver.swift @@ -28,39 +28,43 @@ class RCTPlayerObserver: NSObject { var _handlers: RCTPlayerObserverHandler! var player:AVPlayer? { + willSet { + removePlayerObservers() + removePlayerTimeObserver() + } didSet { - if player == nil { - removePlayerObservers() - removePlayerTimeObserver() - } else { + if player != nil { addPlayerObservers() addPlayerTimeObserver() } } } var playerItem:AVPlayerItem? { + willSet { + removePlayerItemObservers() + } didSet { - if playerItem == nil { - removePlayerItemObservers() - } else { + if playerItem != nil { addPlayerItemObservers() } } } var playerViewController:AVPlayerViewController? { + willSet { + removePlayerViewControllerObservers() + } didSet { - if playerViewController == nil { - removePlayerViewControllerObservers() - } else { + if playerViewController != nil { addPlayerViewControllerObservers() } } } var playerLayer:AVPlayerLayer? { + willSet { + removePlayerLayerObserver() + } didSet { if playerLayer == nil { - removePlayerLayerObserver() - } else { addPlayerLayerObserver() } } @@ -148,8 +152,8 @@ class RCTPlayerObserver: NSObject { /* Cancels the previously registered time observer. */ func removePlayerTimeObserver() { - if let timeObserver = _timeObserver { - player?.removeTimeObserver(timeObserver) + if _timeObserver != nil { + player?.removeTimeObserver(_timeObserver) _timeObserver = nil } } diff --git a/ios/Video/Features/RCTPlayerOperations.swift b/ios/Video/Features/RCTPlayerOperations.swift index 8d70f46d..094cbf44 100644 --- a/ios/Video/Features/RCTPlayerOperations.swift +++ b/ios/Video/Features/RCTPlayerOperations.swift @@ -1,5 +1,6 @@ import AVFoundation import MediaAccessibility +import Promises let RCTVideoUnset = -1 @@ -157,4 +158,23 @@ enum RCTPlayerOperations { } } + + static func seek(player: AVPlayer, playerItem:AVPlayerItem, paused:Bool, seekTime:Float, seekTolerance:Float) -> Promise { + let timeScale:Int = 1000 + let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime), preferredTimescale: Int32(timeScale)) + let current:CMTime = playerItem.currentTime() + let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance), timescale: Int32(timeScale)) + + return Promise(on: .global()) { fulfill, reject in + guard CMTimeCompare(current, cmSeekTime) != 0 else { + reject(NSError()) + return + } + if !paused { player.pause() } + + player.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ (finished:Bool) in + fulfill(finished) + }) + } + } } diff --git a/ios/Video/Features/RCTResourceLoaderDelegate.swift b/ios/Video/Features/RCTResourceLoaderDelegate.swift index 5ead7fef..e1f11528 100644 --- a/ios/Video/Features/RCTResourceLoaderDelegate.swift +++ b/ios/Video/Features/RCTResourceLoaderDelegate.swift @@ -1,4 +1,5 @@ import AVFoundation +import Promises class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate { @@ -6,6 +7,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes private var _requestingCertificate:Bool = false private var _requestingCertificateErrored:Bool = false private var _drm: DRMParams? + private var _localSourceEncryptionKeyScheme: String? private var _reactTag: NSNumber? private var _onVideoError: RCTDirectEventBlock? private var _onGetLicense: RCTDirectEventBlock? @@ -14,6 +16,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes init( asset: AVURLAsset, drm: DRMParams?, + localSourceEncryptionKeyScheme: String?, onVideoError: RCTDirectEventBlock?, onGetLicense: RCTDirectEventBlock?, reactTag: NSNumber @@ -25,6 +28,7 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes _onVideoError = onVideoError _onGetLicense = onGetLicense _drm = drm + _localSourceEncryptionKeyScheme = localSourceEncryptionKeyScheme } deinit { @@ -42,16 +46,9 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes func resourceLoader(_ resourceLoader:AVAssetResourceLoader, didCancel loadingRequest:AVAssetResourceLoadingRequest) { NSLog("didCancelLoadingRequest") } - - func base64DataFromBase64String(base64String:String?) -> Data? { - if let base64String = base64String { - return Data(base64Encoded:base64String) - } - return nil - } - + func setLicenseResult(_ license:String!) { - guard let respondData = self.base64DataFromBase64String(base64String: license), + guard let respondData = RCTVideoUtils.base64DataFromBase64String(base64String: license), let _loadingRequest = _loadingRequest else { setLicenseResultError("No data from JS license response") return @@ -67,14 +64,13 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes } } - func finishLoadingWithError(error:NSError!) -> Bool { + func finishLoadingWithError(error:Error!) -> Bool { if let _loadingRequest = _loadingRequest, let error = error { - let licenseError:NSError! = error - _loadingRequest.finishLoading(with: licenseError) + _loadingRequest.finishLoading(with: error as! NSError) _onVideoError?([ "error": [ - "code": NSNumber(value: error.code), + "code": NSNumber(value: (error as NSError).code), "localizedDescription": error.localizedDescription == nil ? "" : error.localizedDescription, "localizedFailureReason": ((error as NSError).localizedFailureReason == nil ? "" : (error as NSError).localizedFailureReason) ?? "", "localizedRecoverySuggestion": ((error as NSError).localizedRecoverySuggestion == nil ? "" : (error as NSError).localizedRecoverySuggestion) ?? "", @@ -88,6 +84,35 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes } func loadingRequestHandling(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { + if handleEmbeddedKey(loadingRequest) { + return true + } + + if _drm != nil { + return handleDrm(loadingRequest) + } + + return false + } + + func handleEmbeddedKey(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { + guard let url = loadingRequest.request.url, + let _localSourceEncryptionKeyScheme = _localSourceEncryptionKeyScheme, + let persistentKeyData = RCTVideoUtils.extractDataFromCustomSchemeUrl(from: url, scheme: _localSourceEncryptionKeyScheme) + else { + return false + } + + loadingRequest.contentInformationRequest?.contentType = AVStreamingKeyDeliveryPersistentContentKeyType + loadingRequest.contentInformationRequest?.isByteRangeAccessSupported = true + loadingRequest.contentInformationRequest?.contentLength = Int64(persistentKeyData.count) + loadingRequest.dataRequest?.respond(with: persistentKeyData) + loadingRequest.finishLoading() + + return true + } + + func handleDrm(_ loadingRequest:AVAssetResourceLoadingRequest!) -> Bool { if _requestingCertificate { return true } else if _requestingCertificateErrored { @@ -95,164 +120,48 @@ class RCTResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSes } _loadingRequest = loadingRequest - let url = loadingRequest.request.url - guard let _drm = _drm else { + guard let _drm = _drm, let drmType = _drm.type, drmType == "fairplay" else { return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) } - var contentId:String! - let contentIdOverride:String! = _drm.contentId - if contentIdOverride != nil { - contentId = contentIdOverride - } else if (_onGetLicense != nil) { - contentId = url?.host - } else { - contentId = url?.absoluteString.replacingOccurrences(of: "skd://", with:"") - } - - let drmType:String! = _drm.type - guard drmType == "fairplay" else { - return finishLoadingWithError(error: RCTVideoErrorHandler.noDRMData) - } - - let certificateStringUrl:String! = _drm.certificateUrl - guard let certificateStringUrl = certificateStringUrl, let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { - return finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateURL) - } - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - var certificateData:Data? - if (_drm.base64Certificate != nil) { - certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters) - } else { - do { - certificateData = try Data(contentsOf: certificateURL) - } catch {} - } - - guard let certificateData = certificateData else { - self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData) - self._requestingCertificateErrored = true - return - } - - var contentIdData:NSData! - if self._onGetLicense != nil { - contentIdData = contentId.data(using: .utf8) as NSData? - } else { - contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8)) - } - - let dataRequest:AVAssetResourceLoadingDataRequest! = loadingRequest.dataRequest - guard dataRequest != nil else { - self.finishLoadingWithError(error: RCTVideoErrorHandler.noCertificateData) - self._requestingCertificateErrored = true - return - } - - var spcError:NSError! - var spcData: Data? - do { - spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil) - } catch let spcError { - print("SPC error") - } - // Request CKC to the server - var licenseServer:String! = _drm.licenseServer - if spcError != nil { - self.finishLoadingWithError(error: spcError) - self._requestingCertificateErrored = true - } - - guard spcData != nil else { - self.finishLoadingWithError(error: RCTVideoErrorHandler.noSPC) - self._requestingCertificateErrored = true - return - } - - // js client has a onGetLicense callback and will handle license fetching - if let _onGetLicense = self._onGetLicense { - let base64Encoded = spcData?.base64EncodedString(options: []) + var promise: Promise + if _onGetLicense != nil { + let contentId = _drm.contentId ?? loadingRequest.request.url?.host + promise = RCTVideoDRM.handleWithOnGetLicense( + loadingRequest:loadingRequest, + contentId:contentId, + certificateUrl:_drm.certificateUrl, + base64Certificate:_drm.base64Certificate + ) .then{ spcData -> Void in self._requestingCertificate = true - if licenseServer == nil { - licenseServer = "" - } - _onGetLicense(["licenseUrl": licenseServer, - "contentId": contentId, - "spcBase64": base64Encoded, - "target": self._reactTag]) - - - } else if licenseServer != nil { - self.fetchLicense( - licenseServer: licenseServer, - spcData: spcData, - contentId: contentId, - dataRequest: dataRequest - ) + self._onGetLicense?(["licenseUrl": self._drm?.licenseServer ?? "", + "contentId": contentId, + "spcBase64": spcData.base64EncodedString(options: []), + "target": self._reactTag]) } + } else { + promise = RCTVideoDRM.handleInternalGetLicense( + loadingRequest:loadingRequest, + contentId:_drm.contentId, + licenseServer:_drm.licenseServer, + certificateUrl:_drm.certificateUrl, + base64Certificate:_drm.base64Certificate, + headers:_drm.headers + ) .then{ data -> Void in + guard let dataRequest = loadingRequest.dataRequest else { + throw RCTVideoErrorHandler.noCertificateData + } + dataRequest.respond(with:data) + loadingRequest.finishLoading() + } } + + + promise.catch{ error in + self.finishLoadingWithError(error:error) + self._requestingCertificateErrored = true + } + return true } - - func fetchLicense( - licenseServer: String, - spcData: Data?, - contentId: String, - dataRequest: AVAssetResourceLoadingDataRequest! - ) { - var request = URLRequest(url: URL(string: licenseServer)!) - request.httpMethod = "POST" - - // HEADERS - if let headers = _drm?.headers { - for item in headers { - guard let key = item.key as? String, let value = item.value as? String else { - continue - } - request.setValue(value, forHTTPHeaderField: key) - } - } - - if (_onGetLicense != nil) { - request.httpBody = spcData - } else { - let spcEncoded = spcData?.base64EncodedString(options: []) - let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String - let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId) - let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true) - request.httpBody = postData - } - - let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ [weak self] (data:Data!,response:URLResponse!,error:Error!) in - guard let self = self else { return } - let httpResponse:HTTPURLResponse! = response as! HTTPURLResponse - guard error == nil else { - print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") - self.finishLoadingWithError(error: error as NSError?) - self._requestingCertificateErrored = true - return - } - guard httpResponse.statusCode == 200 else { - print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") - self.finishLoadingWithError(error: RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode)) - self._requestingCertificateErrored = true - return - } - - guard data != nil else { - self.finishLoadingWithError(error: RCTVideoErrorHandler.noDataFromLicenseRequest) - self._requestingCertificateErrored = true - return - } - - if (self._onGetLicense != nil) { - dataRequest.respond(with: data) - } else if let decodedData = Data(base64Encoded: data, options: []) { - dataRequest.respond(with: decodedData) - } - self._loadingRequest?.finishLoading() - }) - postDataTask.resume() - } } diff --git a/ios/Video/Features/RCTVideoDRM.swift b/ios/Video/Features/RCTVideoDRM.swift new file mode 100644 index 00000000..d059bbc4 --- /dev/null +++ b/ios/Video/Features/RCTVideoDRM.swift @@ -0,0 +1,168 @@ +import AVFoundation +import Promises + +struct RCTVideoDRM { + @available(*, unavailable) private init() {} + + static func fetchLicense( + licenseServer: String, + spcData: Data?, + contentId: String, + headers: [String:Any]? + ) -> Promise { + let request = createLicenseRequest(licenseServer:licenseServer, spcData:spcData, contentId:contentId, headers:headers) + + return Promise(on: .global()) { fulfill, reject in + let postDataTask = URLSession.shared.dataTask(with: request as URLRequest, completionHandler:{ (data:Data!,response:URLResponse!,error:Error!) in + + let httpResponse:HTTPURLResponse! = (response as! HTTPURLResponse) + + guard error == nil else { + print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") + reject(error) + return + } + guard httpResponse.statusCode == 200 else { + print("Error getting license from \(licenseServer), HTTP status code \(httpResponse.statusCode)") + reject(RCTVideoErrorHandler.licenseRequestNotOk(httpResponse.statusCode)) + return + } + + guard data != nil, let decodedData = Data(base64Encoded: data, options: []) else { + reject(RCTVideoErrorHandler.noDataFromLicenseRequest) + return + } + + fulfill(decodedData) + }) + postDataTask.resume() + } + } + + static func createLicenseRequest( + licenseServer: String, + spcData: Data?, + contentId: String, + headers: [String:Any]? + ) -> URLRequest { + var request = URLRequest(url: URL(string: licenseServer)!) + request.httpMethod = "POST" + + if let headers = headers { + for item in headers { + guard let key = item.key as? String, let value = item.value as? String else { + continue + } + request.setValue(value, forHTTPHeaderField: key) + } + } + + let spcEncoded = spcData?.base64EncodedString(options: []) + let spcUrlEncoded = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, spcEncoded as? CFString? as! CFString, nil, "?=&+" as CFString, CFStringBuiltInEncodings.UTF8.rawValue) as? String + let post = String(format:"spc=%@&%@", spcUrlEncoded as! CVarArg, contentId) + let postData = post.data(using: String.Encoding.utf8, allowLossyConversion:true) + request.httpBody = postData + + return request + } + + static func fetchSpcData( + loadingRequest: AVAssetResourceLoadingRequest, + certificateData: Data, + contentIdData: Data + ) -> Promise { + return Promise(on: .global()) { fulfill, reject in + var spcError:NSError! + var spcData: Data? + do { + spcData = try loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData as Data, options: nil) + } catch _ { + print("SPC error") + } + + if spcError != nil { + reject(spcError) + } + + guard let spcData = spcData else { + reject(RCTVideoErrorHandler.noSPC) + return + } + + fulfill(spcData) + } + } + + static func createCertificateData(certificateStringUrl:String?, base64Certificate:Bool?) -> Promise { + return Promise(on: .global()) { fulfill, reject in + + guard let certificateStringUrl = certificateStringUrl, + let certificateURL = URL(string: certificateStringUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) ?? "") else { + reject(RCTVideoErrorHandler.noCertificateURL) + return + } + + var certificateData:Data? + do { + certificateData = try Data(contentsOf: certificateURL) + if (base64Certificate != nil) { + certificateData = Data(base64Encoded: certificateData! as Data, options: .ignoreUnknownCharacters) + } + } catch {} + + guard let certificateData = certificateData else { + reject(RCTVideoErrorHandler.noCertificateData) + return + } + + fulfill(certificateData) + } + } + + static func handleWithOnGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, certificateUrl:String?, base64Certificate:Bool?) -> Promise { + let contentIdData = contentId?.data(using: .utf8) + + return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate) + .then{ certificateData -> Promise in + guard let contentIdData = contentIdData else { + throw RCTVideoError.invalidContentId as! Error + } + + return RCTVideoDRM.fetchSpcData( + loadingRequest:loadingRequest, + certificateData:certificateData, + contentIdData:contentIdData + ) + } + } + + static func handleInternalGetLicense(loadingRequest: AVAssetResourceLoadingRequest, contentId:String?, licenseServer:String?, certificateUrl:String?, base64Certificate:Bool?, headers: [String:Any]?) -> Promise { + let url = loadingRequest.request.url + + guard let contentId = contentId ?? url?.absoluteString.replacingOccurrences(of: "skd://", with:"") else { + return Promise(RCTVideoError.invalidContentId as! Error) + } + + let contentIdData = NSData(bytes: contentId.cString(using: String.Encoding.utf8), length:contentId.lengthOfBytes(using: String.Encoding.utf8)) as Data + + return RCTVideoDRM.createCertificateData(certificateStringUrl:certificateUrl, base64Certificate:base64Certificate) + .then{ certificateData in + return RCTVideoDRM.fetchSpcData( + loadingRequest:loadingRequest, + certificateData:certificateData, + contentIdData:contentIdData + ) + } + .then{ spcData -> Promise in + guard let licenseServer = licenseServer else { + throw RCTVideoError.noLicenseServerURL as! Error + } + return RCTVideoDRM.fetchLicense( + licenseServer: licenseServer, + spcData: spcData, + contentId: contentId, + headers: headers + ) + } + } +} diff --git a/ios/Video/Features/RCTVideoErrorHandling.swift b/ios/Video/Features/RCTVideoErrorHandling.swift index 2b65a969..e795aa28 100644 --- a/ios/Video/Features/RCTVideoErrorHandling.swift +++ b/ios/Video/Features/RCTVideoErrorHandling.swift @@ -1,5 +1,6 @@ enum RCTVideoError : Int { case fromJSPart + case noLicenseServerURL case licenseRequestNotOk case noDataFromLicenseRequest case noSPC @@ -8,11 +9,12 @@ enum RCTVideoError : Int { case noCertificateURL case noFairplayDRM case noDRMData + case invalidContentId } enum RCTVideoErrorHandler { - static let noDRMData: NSError = NSError( + static let noDRMData = NSError( domain: "RCTVideo", code: RCTVideoError.noDRMData.rawValue, userInfo: [ @@ -21,7 +23,7 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: "Have you specified the 'drm' prop?" ]) - static let noCertificateURL: NSError = NSError( + static let noCertificateURL = NSError( domain: "RCTVideo", code: RCTVideoError.noCertificateURL.rawValue, userInfo: [ @@ -30,7 +32,7 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop certificateUrl?" ]) - static let noCertificateData: NSError = NSError( + static let noCertificateData = NSError( domain: "RCTVideo", code: RCTVideoError.noCertificateData.rawValue, userInfo: [ @@ -39,7 +41,7 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: "Have you specified a valid 'certificateUrl'?" ]) - static let noSPC:NSError! = NSError( + static let noSPC = NSError( domain: "RCTVideo", code: RCTVideoError.noSPC.rawValue, userInfo: [ @@ -48,13 +50,22 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: "Check your DRM config." ]) - static let noDataFromLicenseRequest:NSError! = NSError( + static let noLicenseServerURL = NSError( + domain: "RCTVideo", + code: RCTVideoError.noLicenseServerURL.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM License.", + NSLocalizedFailureReasonErrorKey: "No license server URL has been found.", + NSLocalizedRecoverySuggestionErrorKey: "Did you specified the prop licenseServer?" + ]) + + static let noDataFromLicenseRequest = NSError( domain: "RCTVideo", code: RCTVideoError.noDataFromLicenseRequest.rawValue, userInfo: [ NSLocalizedDescriptionKey: "Error obtaining DRM license.", NSLocalizedFailureReasonErrorKey: "No data received from the license server.", - NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?." + NSLocalizedRecoverySuggestionErrorKey: "Is the licenseServer ok?" ]) static func licenseRequestNotOk(_ statusCode: Int) -> NSError { @@ -80,4 +91,13 @@ enum RCTVideoErrorHandler { NSLocalizedRecoverySuggestionErrorKey: error ]) } + + static let invalidContentId = NSError( + domain: "RCTVideo", + code: RCTVideoError.invalidContentId.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "Error obtaining DRM license.", + NSLocalizedFailureReasonErrorKey: "No valide content Id received", + NSLocalizedRecoverySuggestionErrorKey: "Is the contentId and url ok?" + ]) } diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index 51a6aae7..69c127af 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -1,4 +1,5 @@ import AVFoundation +import Promises /*! * Collection of pure functions @@ -140,4 +141,110 @@ enum RCTVideoUtils { static func getCurrentTime(playerItem:AVPlayerItem?) -> Float { return Float(CMTimeGetSeconds(playerItem?.currentTime() ?? .zero)) } + + static func base64DataFromBase64String(base64String:String?) -> Data? { + if let base64String = base64String { + return Data(base64Encoded:base64String) + } + return nil + } + + static func replaceURLScheme(url: URL, scheme: String?) -> URL? { + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + urlComponents?.scheme = scheme + + return urlComponents?.url + } + + static func extractDataFromCustomSchemeUrl(from url: URL, scheme: String) -> Data? { + guard url.scheme == scheme, + let adoptURL = RCTVideoUtils.replaceURLScheme(url:url, scheme: nil) else { return nil } + + return Data(base64Encoded: adoptURL.absoluteString) + } + + static func generateMixComposition(_ asset:AVAsset) -> AVMutableComposition { + let mixComposition:AVMutableComposition = AVMutableComposition() + + let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first + let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid) + do { + try videoCompTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), + of: videoAsset, + at: .zero) + } catch { + } + + let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first + let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid) + do { + try audioCompTrack.insertTimeRange( + CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), + of: audioAsset, + at: .zero) + } catch { + } + + return mixComposition + } + + static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] { + let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first + var validTextTracks:[TextTrack] = [] + if let textTracks = textTracks, textTracks.count > 0 { + for i in 0.. Promise { + return Promise(on: .global()) { fulfill, reject in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: { + fulfill(()) + }) + } + } + + static func prepareAsset(source:VideoSource) -> (asset:AVURLAsset?, assetOptions:NSMutableDictionary?)? { + guard source.uri != nil && source.uri != "" else { return nil } + var asset:AVURLAsset! + let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" + let url = source.isNetwork || source.isAsset + ? URL(string: source.uri ?? "") + : URL(fileURLWithPath: bundlePath) + let assetOptions:NSMutableDictionary! = NSMutableDictionary() + + if source.isNetwork { + if let headers = source.requestHeaders, headers.count > 0 { + assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying) + } + let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies + assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying) + asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any]) + } else { + asset = AVURLAsset(url: url!) + } + return (asset, assetOptions) + } } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index fb5b66ba..d124998d 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -2,6 +2,7 @@ import AVFoundation import AVKit import Foundation import React +import Promises class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverHandler { @@ -17,6 +18,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH /* DRM */ private var _drm:DRMParams? + private var _localSourceEncryptionKeyScheme:String? + /* Required to publish events */ private var _eventDispatcher:RCTEventDispatcher? private var _videoLoadStarted:Bool = false @@ -61,7 +64,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver() #if canImport(RCTVideoCache) - private var _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler(self.playerItemPrepareText) + private let _videoCache:RCTVideoCachingHandler = RCTVideoCachingHandler() #endif #if TARGET_OS_IOS @@ -125,6 +128,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH object: nil ) _playerObserver._handlers = self +#if canImport(RCTVideoCache) + _videoCache.playerItemPrepareText = playerItemPrepareText +#endif } required init?(coder aDecoder: NSCoder) { @@ -216,10 +222,39 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH removePlayerLayer() _playerObserver.player = nil _playerObserver.playerItem = nil - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(0)) / Double(NSEC_PER_SEC), execute: { [weak self] in - guard let self = self else {return} - // perform on next run loop, otherwise other passed react-props may not be set - self.playerItemForSource(withCallback:{ (playerItem:AVPlayerItem!) in + + // perform on next run loop, otherwise other passed react-props may not be set + RCTVideoUtils.delay() + .then{ [weak self] in + guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} + guard let source = self._source, + let assetResult = RCTVideoUtils.prepareAsset(source: source), + let asset = assetResult.asset, + let assetOptions = assetResult.assetOptions else { + DebugLog("Could not find video URL in source '\(self._source)'") + throw NSError(domain: "", code: 0, userInfo: nil) + } + +#if canImport(RCTVideoCache) + if self._videoCache.shouldCache(source:source, textTracks:self._textTracks) { + return self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions:assetOptions) + } +#endif + + if self._drm != nil || self._localSourceEncryptionKeyScheme != nil { + self._resouceLoaderDelegate = RCTResourceLoaderDelegate( + asset: asset, + drm: self._drm, + localSourceEncryptionKeyScheme: self._localSourceEncryptionKeyScheme, + onVideoError: self.onVideoError, + onGetLicense: self.onGetLicense, + reactTag: self.reactTag + ) + } + return Promise{self.playerItemPrepareText(asset: asset, assetOptions:assetOptions)} + }.then{[weak self] (playerItem:AVPlayerItem!) in + guard let self = self else {throw NSError(domain: "", code: 0, userInfo: nil)} + self._player?.pause() self._playerItem = playerItem self._playerObserver.playerItem = self._playerItem @@ -233,7 +268,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self._playerObserver.player = self._player self._player?.actionAtItemEnd = .none - + if #available(iOS 10.0, *) { self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) } @@ -248,122 +283,38 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH "drm": self._drm?.json ?? NSNull(), "target": self.reactTag ]) - - }) - }) + }.catch{_ in } _videoLoadStarted = true } @objc - func setDrm(_ drm:NSDictionary!) { + func setDrm(_ drm:NSDictionary) { _drm = DRMParams(drm) } - func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?, withCallback handler:(AVPlayerItem?)->Void) { + @objc + func setLocalSourceEncryptionKeyScheme(_ keyScheme:String) { + _localSourceEncryptionKeyScheme = keyScheme + } + + func playerItemPrepareText(asset:AVAsset!, assetOptions:NSDictionary?) -> AVPlayerItem { if (_textTracks == nil) || _textTracks?.count==0 { - handler(AVPlayerItem(asset: asset)) - return + return AVPlayerItem(asset: asset) } // AVPlayer can't airplay AVMutableCompositions _allowsExternalPlayback = false - - // sideload text tracks - let mixComposition:AVMutableComposition! = AVMutableComposition() - - let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first - let videoCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID:kCMPersistentTrackID_Invalid) - do { - try videoCompTrack.insertTimeRange( - CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), - of: videoAsset, - at: .zero) - } catch { - } - - let audioAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.audio).first - let audioCompTrack:AVMutableCompositionTrack! = mixComposition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID:kCMPersistentTrackID_Invalid) - do { - try audioCompTrack.insertTimeRange( - CMTimeRangeMake(start: .zero, duration: videoAsset.timeRange.duration), - of: audioAsset, - at: .zero) - } catch { - } - - var validTextTracks:[TextTrack] = [] - if let textTracks = _textTracks, let textTrackCount = _textTracks?.count { - for i in 0..Void) { - var asset:AVURLAsset! - guard let source = _source, source.uri != nil && source.uri != "" else { - DebugLog("Could not find video URL in source '\(_source)'") - return - } - - let bundlePath = Bundle.main.path(forResource: source.uri, ofType: source.type) ?? "" - let url = source.isNetwork || source.isAsset - ? URL(string: source.uri ?? "") - : URL(fileURLWithPath: bundlePath) - - let assetOptions:NSMutableDictionary! = NSMutableDictionary() - - if url != nil && source.isNetwork { - if let headers = source.requestHeaders, headers.count > 0 { - assetOptions.setObject(headers, forKey:"AVURLAssetHTTPHeaderFieldsKey" as NSCopying) - } - let cookies:[AnyObject]! = HTTPCookieStorage.shared.cookies - assetOptions.setObject(cookies, forKey:AVURLAssetHTTPCookiesKey as NSCopying) -#if canImport(RCTVideoCache) - if _videoCache.playerItemForSourceUsingCache(shouldCache:shouldCache, textTracks:_textTracks, uri:uri, assetOptions:assetOptions, handler:handler) { - return - } -#endif - - asset = AVURLAsset(url: url!, options:assetOptions as! [String : Any]) - } else { - asset = AVURLAsset(url: url!) - } - - if _drm != nil { - _resouceLoaderDelegate = RCTResourceLoaderDelegate( - asset: asset, - drm: _drm, - onVideoError: onVideoError, - onGetLicense: onGetLicense, - reactTag: reactTag - ) - } - - self.playerItemPrepareText(asset: asset, assetOptions:assetOptions, withCallback:handler) + return AVPlayerItem(asset: mixComposition) } // MARK: - Prop setters @@ -415,13 +366,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String!) { + func setIgnoreSilentSwitch(_ ignoreSilentSwitch:String?) { _ignoreSilentSwitch = ignoreSilentSwitch self.applyModifiers() } @objc - func setMixWithOthers(_ mixWithOthers:String!) { + func setMixWithOthers(_ mixWithOthers:String?) { _mixWithOthers = mixWithOthers self.applyModifiers() } @@ -479,7 +430,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func setCurrentTime(_ currentTime:Float) { - let info:NSDictionary! = [ + let info:NSDictionary = [ "time": NSNumber(value: currentTime), "tolerance": NSNumber(value: 100) ] @@ -490,37 +441,31 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func setSeek(_ info:NSDictionary!) { let seekTime:NSNumber! = info["time"] as! NSNumber let seekTolerance:NSNumber! = info["tolerance"] as! NSNumber - - let timeScale:Int = 1000 - - let item:AVPlayerItem! = _player?.currentItem - guard item != nil && item.status == AVPlayerItem.Status.readyToPlay else { + let item:AVPlayerItem? = _player?.currentItem + guard item != nil, let player = _player, let item = item, item.status == AVPlayerItem.Status.readyToPlay else { _pendingSeek = true _pendingSeekTime = seekTime.floatValue return } + let wasPaused = _paused - // TODO check loadedTimeRanges - let cmSeekTime:CMTime = CMTimeMakeWithSeconds(Float64(seekTime.floatValue), preferredTimescale: Int32(timeScale)) - let current:CMTime = item.currentTime() - // TODO figure out a good tolerance level - let tolerance:CMTime = CMTimeMake(value: Int64(seekTolerance.floatValue), timescale: Int32(timeScale)) - let wasPaused:Bool = _paused - - guard CMTimeCompare(current, cmSeekTime) != 0 else { return } - if !wasPaused { _player?.pause() } - - _player?.seek(to: cmSeekTime, toleranceBefore:tolerance, toleranceAfter:tolerance, completionHandler:{ [weak self] (finished:Bool) in - guard let self = self else { return } - - self._playerObserver.addTimeObserverIfNotSet() - if !wasPaused { - self.setPaused(false) - } - self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), - "seekTime": seekTime, - "target": self.reactTag]) - }) + RCTPlayerOperations.seek( + player:player, + playerItem:item, + paused:wasPaused, + seekTime:seekTime.floatValue, + seekTolerance:seekTolerance.floatValue) + .then{ [weak self] (finished:Bool) in + guard let self = self else { return } + + self._playerObserver.addTimeObserverIfNotSet() + if !wasPaused { + self.setPaused(false) + } + self.onVideoSeek?(["currentTime": NSNumber(value: Float(CMTimeGetSeconds(item.currentTime()))), + "seekTime": seekTime, + "target": self.reactTag]) + }.catch{_ in } _pendingSeek = false } @@ -605,46 +550,46 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _repeat = `repeat` } - + @objc - func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary!) { + func setSelectedAudioTrack(_ selectedAudioTrack:NSDictionary?) { setSelectedAudioTrack(SelectedTrackCriteria(selectedAudioTrack)) } - func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria!) { + func setSelectedAudioTrack(_ selectedAudioTrack:SelectedTrackCriteria?) { _selectedAudioTrackCriteria = selectedAudioTrack RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.audible, - criteria:_selectedAudioTrackCriteria) + criteria:_selectedAudioTrackCriteria) } @objc - func setSelectedTextTrack(_ selectedTextTrack:NSDictionary!) { + func setSelectedTextTrack(_ selectedTextTrack:NSDictionary?) { setSelectedTextTrack(SelectedTrackCriteria(selectedTextTrack)) } - func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria!) { + func setSelectedTextTrack(_ selectedTextTrack:SelectedTrackCriteria?) { _selectedTextTrackCriteria = selectedTextTrack if (_textTracks != nil) { // sideloaded text tracks RCTPlayerOperations.setSideloadedText(player:_player, textTracks:_textTracks, criteria:_selectedTextTrackCriteria) } else { // text tracks included in the HLS playlist RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player:_player, characteristic: AVMediaCharacteristic.legible, - criteria:_selectedTextTrackCriteria) + criteria:_selectedTextTrackCriteria) } } @objc - func setTextTracks(_ textTracks:[NSDictionary]!) { - setTextTracks(textTracks.map { TextTrack($0) }) + func setTextTracks(_ textTracks:[NSDictionary]?) { + setTextTracks(textTracks?.map { TextTrack($0) }) } - func setTextTracks(_ textTracks:[TextTrack]!) { + func setTextTracks(_ textTracks:[TextTrack]?) { _textTracks = textTracks // in case textTracks was set after selectedTextTrack if (_selectedTextTrackCriteria != nil) {setSelectedTextTrack(_selectedTextTrackCriteria)} } - + @objc func setFullscreen(_ fullscreen:Bool) { if fullscreen && !_fullscreenPlayerPresented && _player != nil { @@ -697,7 +642,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc - func setFullscreenOrientation(_ orientation:String!) { + func setFullscreenOrientation(_ orientation:String?) { _fullscreenOrientation = orientation if _fullscreenPlayerPresented { _playerViewController?.preferredOrientation = orientation @@ -705,18 +650,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } func usePlayerViewController() { - guard _player != nil else { return } + guard let _player = _player, let _playerItem = _playerItem else { return } if _playerViewController == nil { - _playerViewController = createPlayerViewController(player: _player, withPlayerItem:_playerItem) - + _playerViewController = createPlayerViewController(player:_player, withPlayerItem:_playerItem) } // to prevent video from being animated when resizeMode is 'cover' // resize mode must be set before subview is added setResizeMode(_resizeMode) guard let _playerViewController = _playerViewController else { return } - + if _controls { let viewController:UIViewController! = self.reactViewController() viewController.addChild(_playerViewController) @@ -726,8 +670,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _playerObserver.playerViewController = _playerViewController } - func createPlayerViewController(player:AVPlayer!, withPlayerItem playerItem:AVPlayerItem!) -> RCTVideoPlayerViewController! { - let viewController:RCTVideoPlayerViewController! = RCTVideoPlayerViewController() + func createPlayerViewController(player:AVPlayer, withPlayerItem playerItem:AVPlayerItem) -> RCTVideoPlayerViewController { + let viewController = RCTVideoPlayerViewController() viewController.showsPlaybackControls = true viewController.rctDelegate = self viewController.preferredOrientation = _fullscreenOrientation @@ -953,20 +897,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH return } - var metadata: [[String:String?]?] = [] - for item in _items { - let value = item.value as? String - let identifier = item.identifier?.rawValue + var metadata: [[String:String?]?] = [] + for item in _items { + let value = item.value as? String + let identifier = item.identifier?.rawValue - if let value = value { - metadata.append(["value":value, "identifier":identifier]) - } - } + if let value = value { + metadata.append(["value":value, "identifier":identifier]) + } + } - onTimedMetadata?([ - "target": reactTag, - "metadata": metadata - ]) + onTimedMetadata?([ + "target": reactTag, + "metadata": metadata + ]) } // Handle player item status change. @@ -1134,14 +1078,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } //unused -// @objc func handleAVPlayerAccess(notification:NSNotification!) { -// let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog() -// let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last -// -// /* TODO: get this working -// if (self.onBandwidthUpdate) { -// self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); -// } -// */ -// } + // @objc func handleAVPlayerAccess(notification:NSNotification!) { + // let accessLog:AVPlayerItemAccessLog! = (notification.object as! AVPlayerItem).accessLog() + // let lastEvent:AVPlayerItemAccessLogEvent! = accessLog.events.last + // + // /* TODO: get this working + // if (self.onBandwidthUpdate) { + // self.onBandwidthUpdate(@{@"bitrate": [NSNumber numberWithFloat:lastEvent.observedBitrate]}); + // } + // */ + // } } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index ece0a1e3..9979b440 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -34,6 +34,7 @@ RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); +RCT_EXPORT_VIEW_PROPERTY(localSourceEncryptionKeyScheme, NSString); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock); diff --git a/ios/VideoCaching/RCTVideoCachingHandler.swift b/ios/VideoCaching/RCTVideoCachingHandler.swift index 8e37f11a..5f6ffb77 100644 --- a/ios/VideoCaching/RCTVideoCachingHandler.swift +++ b/ios/VideoCaching/RCTVideoCachingHandler.swift @@ -1,52 +1,50 @@ import Foundation import AVFoundation import DVAssetLoaderDelegate +import Promises class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { private var _videoCache:RCTVideoCache! = RCTVideoCache.sharedInstance() - private var _playerItemPrepareText: (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void + var playerItemPrepareText: ((AVAsset?, NSDictionary?) -> AVPlayerItem)? - init(_ playerItemPrepareText: @escaping (AVAsset?, NSDictionary?, (AVPlayerItem?)->Void) -> Void) { - _playerItemPrepareText = playerItemPrepareText + override init() { + super.init() } - func playerItemForSourceUsingCache(shouldCache:Bool, textTracks:[AnyObject]?, uri:String, assetOptions:NSMutableDictionary, handler:@escaping (AVPlayerItem?)->Void) -> Bool { - if shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) { + func shouldCache(source: VideoSource, textTracks:[TextTrack]?) -> Bool { + if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.count == 0)) { /* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying * to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded. * Until this is fixed, we need to bypass caching when text tracks are specified. */ - DebugLog("Caching is not supported for uri '\(uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") - playerItemForSourceUsingCache(uri: uri, assetOptions:assetOptions, withCallback:handler) + DebugLog("Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") return true } return false } - func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!, withCallback handler: @escaping (AVPlayerItem?)->Void) { + func playerItemForSourceUsingCache(uri:String!, assetOptions options:NSDictionary!) -> Promise { let url = URL(string: uri) - _videoCache.getItemForUri(uri, withCallback:{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in - guard let self = self else { return } + return getItemForUri(uri) + .then{ [weak self] (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) -> AVPlayerItem in + guard let self = self, let playerItemPrepareText = self.playerItemPrepareText else {throw NSError(domain: "", code: 0, userInfo: nil)} switch (videoCacheStatus) { case .missingFileExtension: DebugLog("Could not generate cache key for uri '\(uri)'. It is currently not supported to cache urls that do not include a file extension. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) - self._playerItemPrepareText(asset, options, handler) - return + return playerItemPrepareText(asset, options) case .unsupportedFileExtension: DebugLog("Could not generate cache key for uri '\(uri)'. The file extension of that uri is currently not supported. The video file will not be cached. Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md") let asset:AVURLAsset! = AVURLAsset(url: url!, options:options as! [String : Any]) - self._playerItemPrepareText(asset, options, handler) - return + return playerItemPrepareText(asset, options) default: if let cachedAsset = cachedAsset { DebugLog("Playing back uri '\(uri)' from cache") // See note in playerItemForSource about not being able to support text tracks & caching - handler(AVPlayerItem(asset: cachedAsset)) - return + return AVPlayerItem(asset: cachedAsset) } } @@ -65,8 +63,16 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate { asset?.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main) */ - handler(AVPlayerItem(asset: asset)) - }) + return AVPlayerItem(asset: asset) + } + } + + func getItemForUri(_ uri:String) -> Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> { + return Promise<(videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?)> { fulfill, reject in + self._videoCache.getItemForUri(uri, withCallback:{ (videoCacheStatus:RCTVideoCacheStatus,cachedAsset:AVAsset?) in + fulfill((videoCacheStatus, cachedAsset)) + }) + } } // MARK: - DVAssetLoaderDelegate diff --git a/react-native-video.podspec b/react-native-video.podspec index 5c3f5e5d..e33dcf9d 100644 --- a/react-native-video.podspec +++ b/react-native-video.podspec @@ -12,11 +12,12 @@ Pod::Spec.new do |s| s.homepage = 'https://github.com/react-native-community/react-native-video' s.source = { :git => "https://github.com/react-native-community/react-native-video.git", :tag => "#{s.version}" } - s.ios.deployment_target = "8.0" + s.ios.deployment_target = "9.0" s.tvos.deployment_target = "9.0" s.subspec "Video" do |ss| ss.source_files = "ios/Video/**/*.{h,m,swift}" + ss.dependency "PromisesSwift" end s.subspec "VideoCaching" do |ss| From 0ab7ff476b947a3552a8901123d5137c17bf31ba Mon Sep 17 00:00:00 2001 From: Gabriel Rivero Date: Wed, 3 Nov 2021 19:14:19 -0400 Subject: [PATCH 030/194] VEX-6030-reduce-buffer-size-based-on-heap --- .../com/brentvatne/exoplayer/ReactExoplayerView.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 24dda940..6ce67812 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -2,6 +2,7 @@ package com.brentvatne.exoplayer; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.ActivityManager; import android.content.Context; import android.media.AudioManager; import android.net.Uri; @@ -198,7 +199,7 @@ class ReactExoplayerView extends FrameLayout implements public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) { Timeline.Window window = new Timeline.Window(); - if(!player.getCurrentTimeline().isEmpty()) { + if(!player.getCurrentTimeline().isEmpty()) { player.getCurrentTimeline().getWindow(player.getCurrentWindowIndex(), window); } return window.windowStartTimeMs + currentPosition; @@ -401,6 +402,7 @@ class ReactExoplayerView extends FrameLayout implements } private class RNVLoadControl extends DefaultLoadControl { + private int availableHeapInBytes = 0; public RNVLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { super(allocator, minBufferMs, @@ -411,10 +413,18 @@ class ReactExoplayerView extends FrameLayout implements prioritizeTimeOverSizeThresholds, backBufferDurationMs, retainBackBufferFromKeyframe); + if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.N) { + ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(themedReactContext.ACTIVITY_SERVICE); + availableHeapInBytes = activityManager.getMemoryClass() / 2 * 1024 * 1024; + } } @Override public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { + int loadedBytes = getAllocator().getTotalBytesAllocated(); + if (availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes) { + return false; + } if (ReactExoplayerView.this.disableBuffering) { return false; } From 40e450b0b1e2026e66f2accf5d3fc034de4aaead Mon Sep 17 00:00:00 2001 From: Gabriel Rivero Date: Wed, 3 Nov 2021 19:35:27 -0400 Subject: [PATCH 031/194] VEX-6030-reduce-buffer-size-based-on-heap --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 6ce67812..981bca22 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -413,7 +413,7 @@ class ReactExoplayerView extends FrameLayout implements prioritizeTimeOverSizeThresholds, backBufferDurationMs, retainBackBufferFromKeyframe); - if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.N) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) { ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(themedReactContext.ACTIVITY_SERVICE); availableHeapInBytes = activityManager.getMemoryClass() / 2 * 1024 * 1024; } From 7dbc5eb078f530168394c68731c1185fb334982a Mon Sep 17 00:00:00 2001 From: Gabriel Rivero Date: Thu, 4 Nov 2021 13:54:43 -0400 Subject: [PATCH 032/194] add config parameter maxHeapAllocationPercent --- README.md | 1 + Video.js | 1 + .../exoplayer/ReactExoplayerView.java | 19 +++++++++++-------- .../exoplayer/ReactExoplayerViewManager.java | 6 +++++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ca6745d3..564423f8 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,7 @@ minBufferMs | number | The default minimum duration of media that the player wil maxBufferMs | number | The default maximum duration of media that the player will attempt to buffer, in milliseconds. bufferForPlaybackMs | number | The default duration of media that must be buffered for playback to start or resume following a user action such as a seek, in milliseconds. bufferForPlaybackAfterRebufferMs | number | The default duration of media that must be buffered for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. +maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1 This prop should only be set when you are setting the source, changing it after the media is loaded will cause it to be reloaded. diff --git a/Video.js b/Video.js index 62a179a4..fcad8437 100644 --- a/Video.js +++ b/Video.js @@ -468,6 +468,7 @@ Video.propTypes = { maxBufferMs: PropTypes.number, bufferForPlaybackMs: PropTypes.number, bufferForPlaybackAfterRebufferMs: PropTypes.number, + maxHeapAllocationPercent: PropTypes.number, }), stereoPan: PropTypes.number, rate: PropTypes.number, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 981bca22..37c9657e 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -98,6 +98,8 @@ class ReactExoplayerView extends FrameLayout implements MetadataOutput, DrmSessionEventListener { + public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1; + private static final String TAG = "ReactExoplayerView"; private static final CookieManager DEFAULT_COOKIE_MANAGER; @@ -142,6 +144,7 @@ class ReactExoplayerView extends FrameLayout implements private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; + private double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; private Handler mainHandler; private Timer bufferCheckTimer; @@ -413,19 +416,18 @@ class ReactExoplayerView extends FrameLayout implements prioritizeTimeOverSizeThresholds, backBufferDurationMs, retainBackBufferFromKeyframe); - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) { - ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(themedReactContext.ACTIVITY_SERVICE); - availableHeapInBytes = activityManager.getMemoryClass() / 2 * 1024 * 1024; - } + ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(themedReactContext.ACTIVITY_SERVICE); + availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeapAllocationPercent * 1024 * 1024); } @Override public boolean shouldContinueLoading(long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) { - int loadedBytes = getAllocator().getTotalBytesAllocated(); - if (availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes) { + if (ReactExoplayerView.this.disableBuffering) { return false; } - if (ReactExoplayerView.this.disableBuffering) { + int loadedBytes = getAllocator().getTotalBytesAllocated(); + boolean isHeapReached = availableHeapInBytes > 0 && loadedBytes >= availableHeapInBytes; + if (isHeapReached) { return false; } return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed); @@ -1528,11 +1530,12 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setHideShutterView(hideShutterView); } - public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs) { + public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent) { minBufferMs = newMinBufferMs; maxBufferMs = newMaxBufferMs; bufferForPlaybackMs = newBufferForPlaybackMs; bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs; + maxHeapAllocationPercent = newMaxHeapAllocationPercent; releasePlayer(); initializePlayer(); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index fc9a1354..3bffbc37 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -55,6 +55,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Fri, 5 Nov 2021 12:42:45 +0100 Subject: [PATCH 033/194] Disable pipController init if pictureInPicture is false Disable pipController init if pictureInPicture is set to false prevents going to PIP mode on iOS when the app goes to the background. --- ios/Video/RCTVideo.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index a757c08a..4226db9d 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -907,7 +907,7 @@ static int const RCTVideoUnset = -1; } - (void)setupPipController { - if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { + if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported] && _pictureInPicture) { // Create new controller passing reference to the AVPlayerLayer _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; _pipController.delegate = self; From f712eecb4fa1df4a07705cf51135410227883233 Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Tue, 9 Nov 2021 14:22:32 +0200 Subject: [PATCH 034/194] VEX-6011: Align AVOD resolutions with available resolutions on Content (#14) Add support for content tracks and improve track selection to work even during ads playback. --- Video.js | 1 + .../exoplayer/ReactExoplayerView.java | 142 ++++++++++++++++++ .../exoplayer/ReactExoplayerViewManager.java | 6 + 3 files changed, 149 insertions(+) diff --git a/Video.js b/Video.js index 62a179a4..fdfa141e 100644 --- a/Video.js +++ b/Video.js @@ -477,6 +477,7 @@ Video.propTypes = { playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), reportBandwidth: PropTypes.bool, + contentStartTime: PropTypes.number, disableFocus: PropTypes.bool, disableBuffering: PropTypes.bool, controls: PropTypes.bool, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 24dda940..7873d95d 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -76,6 +76,13 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.Descriptor; import java.net.CookieHandler; import java.net.CookieManager; @@ -86,6 +93,13 @@ import java.util.UUID; import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.List; +import java.lang.Thread; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.lang.Integer; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -136,6 +150,8 @@ class ReactExoplayerView extends FrameLayout implements private int maxBitRate = 0; private long seekTime = C.TIME_UNSET; private boolean hasDrmFailed = false; + private boolean isUsingContentResolution = false; + private boolean selectTrackWhenReady = false; private int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; private int maxBufferMs = DefaultLoadControl.DEFAULT_MAX_BUFFER_MS; @@ -159,6 +175,7 @@ class ReactExoplayerView extends FrameLayout implements private ReadableArray textTracks; private boolean disableFocus; private boolean disableBuffering; + private long contentStartTime; private boolean disableDisconnectError; private boolean preventsDisplaySleepDuringVideoPlayback = true; private float mProgressUpdateInterval = 250.0f; @@ -850,6 +867,10 @@ class ReactExoplayerView extends FrameLayout implements onBuffering(false); startProgressHandler(); videoLoaded(); + if (selectTrackWhenReady && isUsingContentResolution) { + selectTrackWhenReady = false; + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + } // Setting the visibility for the playerControlView if (playerControlView != null) { playerControlView.show(); @@ -921,6 +942,13 @@ class ReactExoplayerView extends FrameLayout implements return audioTracks; } private WritableArray getVideoTrackInfo() { + + WritableArray contentVideoTracks = this.getVideoTrackInfoFromManifest(); + if (contentVideoTracks != null) { + isUsingContentResolution = true; + return contentVideoTracks; + } + WritableArray videoTracks = Arguments.createArray(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); @@ -947,9 +975,73 @@ class ReactExoplayerView extends FrameLayout implements } } } + return videoTracks; } + private WritableArray getVideoTrackInfoFromManifest() { + ExecutorService es = Executors.newSingleThreadExecutor(); + final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); + final Uri sourceUri = this.srcUri; + final Timeline timelineRef = this.player.getCurrentTimeline(); + final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset + + Future result = es.submit(new Callable() { + DataSource ds = dataSource; + Uri uri = sourceUri; + Timeline timeline = timelineRef; + long startTimeUs = startTime * 1000; // ms -> us + + public WritableArray call() throws Exception { + WritableArray videoTracks = Arguments.createArray(); + try { + DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri); + int periodCount = manifest.getPeriodCount(); + for (int i = 0; i < periodCount; i++) { + Period period = manifest.getPeriod(i); + for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) { + AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex); + if (adaptation.type != C.TRACK_TYPE_VIDEO) { + continue; + } + boolean hasFoundContentPeriod = false; + for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) { + Representation representation = adaptation.representations.get(representationIndex); + Format format = representation.format; + if (representation.presentationTimeOffsetUs <= startTimeUs) { + break; + } + hasFoundContentPeriod = true; + WritableMap videoTrack = Arguments.createMap(); + videoTrack.putInt("width", format.width == Format.NO_VALUE ? 0 : format.width); + videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height); + videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); + videoTrack.putString("codecs", format.codecs != null ? format.codecs : ""); + videoTrack.putString("trackId", + format.id == null ? String.valueOf(representationIndex) : format.id); + if (isFormatSupported(format)) { + videoTracks.pushMap(videoTrack); + } + } + if (hasFoundContentPeriod) { + return videoTracks; + } + } + } + } catch (Exception e) {} + return null; + } + }); + + try { + WritableArray results = result.get(); + es.shutdown(); + return results; + } catch (Exception e) {} + + return null; + } + private WritableArray getTextTrackInfo() { WritableArray textTracks = Arguments.createArray(); @@ -993,6 +1085,11 @@ class ReactExoplayerView extends FrameLayout implements // which they seeked. updateResumePosition(); } + if (isUsingContentResolution) { + // Discontinuity events might have a different track list so we update the selected track + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + selectTrackWhenReady = true; + } // 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 == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION @@ -1010,6 +1107,10 @@ class ReactExoplayerView extends FrameLayout implements public void onSeekProcessed() { eventEmitter.seek(player.getCurrentPosition(), seekTime); seekTime = C.TIME_UNSET; + if (isUsingContentResolution) { + // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + } } @Override @@ -1278,14 +1379,51 @@ class ReactExoplayerView extends FrameLayout implements int height = value.asInt(); for (int i = 0; i < groups.length; ++i) { // Search for the exact height TrackGroup group = groups.get(i); + Format closestFormat = null; + int closestTrackIndex = -1; + boolean usingExactMatch = false; for (int j = 0; j < group.length; j++) { Format format = group.getFormat(j); if (format.height == height) { groupIndex = i; tracks[0] = j; + closestFormat = null; + closestTrackIndex = -1; + usingExactMatch = true; break; + } else if (isUsingContentResolution) { + // When using content resolution rather than ads, we need to try and find the closest match if there is no exact match + if (closestFormat != null) { + if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) { + // Higher quality match + closestFormat = format; + closestTrackIndex = j; + } + } else if(format.height < height) { + closestFormat = format; + closestTrackIndex = j; + } } } + // This is a fallback if the new period contains only higher resolutions than the user has selected + if (closestFormat == null && isUsingContentResolution && !usingExactMatch) { + // No close match found - so we pick the lowest quality + int minHeight = Integer.MAX_VALUE; + for (int j = 0; j < group.length; j++) { + Format format = group.getFormat(j); + if (format.height < minHeight) { + minHeight = format.height; + groupIndex = i; + tracks[0] = j; + } + } + } + // Selecting the closest match found + if (closestFormat != null && closestTrackIndex != -1) { + // We found the closest match instead of an exact one + groupIndex = i; + tracks[0] = closestTrackIndex; + } } } else if (rendererIndex == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default // Use system settings if possible @@ -1468,6 +1606,10 @@ class ReactExoplayerView extends FrameLayout implements this.backBufferDurationMs = backBufferDurationMs; } + public void setContentStartTime(int contentStartTime) { + this.contentStartTime = (long)contentStartTime; + } + public void setDisableBuffering(boolean disableBuffering) { this.disableBuffering = disableBuffering; } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index fc9a1354..0e7a8c66 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -63,6 +63,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Mon, 15 Nov 2021 15:57:24 -0800 Subject: [PATCH 035/194] remove unsupported arch and fix uimanager for rnw release --- Video.js | 8 ++-- examples/basic/windows/VideoPlayer.sln | 36 --------------- .../windows/VideoPlayer/VideoPlayer.vcxproj | 8 ---- windows/ReactNativeVideoCPP.sln | 39 ---------------- .../ReactNativeVideoCPP.vcxproj | 8 ---- windows/ReactNativeVideoCPP61.sln | 30 ------------- .../ReactNativeVideoCPP61.vcxproj | 8 ---- windows/ReactNativeVideoCPP62.sln | 45 ------------------- 8 files changed, 4 insertions(+), 178 deletions(-) diff --git a/Video.js b/Video.js index b492d48b..34037684 100644 --- a/Video.js +++ b/Video.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle } from 'react-native'; +import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, ViewPropTypes, Image, Platform, findNodeHandle } from 'react-native'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import TextTrackType from './TextTrackType'; import FilterType from './FilterType'; @@ -254,10 +254,10 @@ export default class Video extends Component { } } getViewManagerConfig = viewManagerName => { - if (!NativeModules.UIManager.getViewManagerConfig) { - return NativeModules.UIManager[viewManagerName]; + if (!UIManager.getViewManagerConfig) { + return UIManager[viewManagerName]; } - return NativeModules.UIManager.getViewManagerConfig(viewManagerName); + return UIManager.getViewManagerConfig(viewManagerName); }; render() { diff --git a/examples/basic/windows/VideoPlayer.sln b/examples/basic/windows/VideoPlayer.sln index c3c7265b..d4490e0a 100644 --- a/examples/basic/windows/VideoPlayer.sln +++ b/examples/basic/windows/VideoPlayer.sln @@ -59,19 +59,14 @@ Global ..\node_modules\react-native-windows\Shared\Shared.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|ARM = Release|ARM Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|ARM.ActiveCfg = Debug|ARM - {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|ARM.Build.0 = Debug|ARM - {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|ARM.Deploy.0 = Debug|ARM {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|ARM64.ActiveCfg = Debug|ARM64 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|ARM64.Build.0 = Debug|ARM64 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|ARM64.Deploy.0 = Debug|ARM64 @@ -81,9 +76,6 @@ Global {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|x86.ActiveCfg = Debug|Win32 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|x86.Build.0 = Debug|Win32 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Debug|x86.Deploy.0 = Debug|Win32 - {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|ARM.ActiveCfg = Release|ARM - {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|ARM.Build.0 = Release|ARM - {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|ARM.Deploy.0 = Release|ARM {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|ARM64.ActiveCfg = Release|ARM64 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|ARM64.Build.0 = Release|ARM64 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|ARM64.Deploy.0 = Release|ARM64 @@ -93,112 +85,84 @@ Global {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|x86.ActiveCfg = Release|Win32 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|x86.Build.0 = Release|Win32 {ADF1CF02-8224-4167-A737-8CBE1A0D5208}.Release|x86.Deploy.0 = Release|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.ActiveCfg = Debug|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.Build.0 = Debug|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.Build.0 = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.ActiveCfg = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.Build.0 = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.ActiveCfg = Debug|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.Build.0 = Debug|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.ActiveCfg = Release|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.Build.0 = Release|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.ActiveCfg = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.Build.0 = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.ActiveCfg = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.Build.0 = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.ActiveCfg = Release|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.Build.0 = Release|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.ActiveCfg = Debug|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.Build.0 = Debug|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.Build.0 = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.ActiveCfg = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.Build.0 = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.ActiveCfg = Debug|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.Build.0 = Debug|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.ActiveCfg = Release|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.Build.0 = Release|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.ActiveCfg = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.Build.0 = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.ActiveCfg = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.Build.0 = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.ActiveCfg = Release|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.Build.0 = Release|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM.ActiveCfg = Debug|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM.Build.0 = Debug|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM64.ActiveCfg = Debug|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM64.Build.0 = Debug|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x64.ActiveCfg = Debug|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x64.Build.0 = Debug|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x86.ActiveCfg = Debug|Win32 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x86.Build.0 = Debug|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM.ActiveCfg = Release|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM.Build.0 = Release|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM64.ActiveCfg = Release|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM64.Build.0 = Release|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x64.ActiveCfg = Release|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x64.Build.0 = Release|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x86.ActiveCfg = Release|Win32 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x86.Build.0 = Release|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.ActiveCfg = Debug|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.Build.0 = Debug|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.Build.0 = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.ActiveCfg = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.Build.0 = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.ActiveCfg = Debug|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.Build.0 = Debug|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.ActiveCfg = Release|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.Build.0 = Release|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.ActiveCfg = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.Build.0 = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.ActiveCfg = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.Build.0 = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.ActiveCfg = Release|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.Build.0 = Release|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.ActiveCfg = Debug|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.Build.0 = Debug|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.Build.0 = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.ActiveCfg = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.Build.0 = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.ActiveCfg = Debug|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.Build.0 = Debug|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.ActiveCfg = Release|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.Build.0 = Release|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.ActiveCfg = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.Build.0 = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.ActiveCfg = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.Build.0 = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.ActiveCfg = Release|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.Build.0 = Release|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.ActiveCfg = Debug|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.Build.0 = Debug|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.Build.0 = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.ActiveCfg = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.Build.0 = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.ActiveCfg = Debug|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.Build.0 = Debug|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.ActiveCfg = Release|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.Build.0 = Release|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.ActiveCfg = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.Build.0 = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.ActiveCfg = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.Build.0 = Release|Win32 - {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM.ActiveCfg = Debug|ARM - {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM.Build.0 = Debug|ARM {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM64.ActiveCfg = Debug|ARM64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM64.Build.0 = Debug|ARM64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x64.ActiveCfg = Debug|x64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x64.Build.0 = Debug|x64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x86.ActiveCfg = Debug|Win32 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x86.Build.0 = Debug|Win32 - {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM.ActiveCfg = Release|ARM - {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM.Build.0 = Release|ARM {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM64.ActiveCfg = Release|ARM64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM64.Build.0 = Release|ARM64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|x64.ActiveCfg = Release|x64 diff --git a/examples/basic/windows/VideoPlayer/VideoPlayer.vcxproj b/examples/basic/windows/VideoPlayer/VideoPlayer.vcxproj index 94a0299d..6b91ab55 100644 --- a/examples/basic/windows/VideoPlayer/VideoPlayer.vcxproj +++ b/examples/basic/windows/VideoPlayer/VideoPlayer.vcxproj @@ -20,10 +20,6 @@ - - Debug - ARM - Debug ARM64 @@ -36,10 +32,6 @@ Debug x64 - - Release - ARM - Release ARM64 diff --git a/windows/ReactNativeVideoCPP.sln b/windows/ReactNativeVideoCPP.sln index 3c3f4dbc..940cdf44 100644 --- a/windows/ReactNativeVideoCPP.sln +++ b/windows/ReactNativeVideoCPP.sln @@ -45,158 +45,119 @@ Global ..\node_modules\react-native-windows\Shared\Shared.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|ARM = Release|ARM Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 - WinUI3|ARM = WinUI3|ARM WinUI3|ARM64 = WinUI3|ARM64 WinUI3|x64 = WinUI3|x64 WinUI3|x86 = WinUI3|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM.ActiveCfg = Debug|ARM - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM.Build.0 = Debug|ARM {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM64.ActiveCfg = Debug|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM64.Build.0 = Debug|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x64.ActiveCfg = Debug|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x64.Build.0 = Debug|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x86.ActiveCfg = Debug|Win32 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x86.Build.0 = Debug|Win32 - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM.ActiveCfg = Release|ARM - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM.Build.0 = Release|ARM {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM64.ActiveCfg = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM64.Build.0 = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x64.ActiveCfg = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x64.Build.0 = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x86.ActiveCfg = Release|Win32 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x86.Build.0 = Release|Win32 - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM.ActiveCfg = Release|ARM - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM.Build.0 = Release|ARM {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM64.Build.0 = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x64.ActiveCfg = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x64.Build.0 = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x86.ActiveCfg = Release|Win32 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x86.Build.0 = Release|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.ActiveCfg = Debug|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.Build.0 = Debug|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.Build.0 = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.ActiveCfg = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.Build.0 = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.ActiveCfg = Debug|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.Build.0 = Debug|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.ActiveCfg = Release|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.Build.0 = Release|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.ActiveCfg = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.Build.0 = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.ActiveCfg = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.Build.0 = Release|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM.ActiveCfg = Release|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM.Build.0 = Release|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM64.Build.0 = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x64.ActiveCfg = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x86.Build.0 = Release|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.ActiveCfg = Debug|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.Build.0 = Debug|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.Build.0 = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.ActiveCfg = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.Build.0 = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.ActiveCfg = Debug|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.Build.0 = Debug|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.ActiveCfg = Release|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.Build.0 = Release|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.ActiveCfg = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.Build.0 = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.ActiveCfg = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.Build.0 = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.ActiveCfg = Release|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.Build.0 = Release|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM.ActiveCfg = Release|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM.Build.0 = Release|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM64.Build.0 = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x64.ActiveCfg = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x64.Build.0 = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x86.ActiveCfg = Release|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x86.Build.0 = Release|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.ActiveCfg = Debug|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.Build.0 = Debug|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.Build.0 = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.ActiveCfg = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.Build.0 = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.ActiveCfg = Debug|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.Build.0 = Debug|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.ActiveCfg = Release|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.Build.0 = Release|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.ActiveCfg = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.Build.0 = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.ActiveCfg = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.Build.0 = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.ActiveCfg = Release|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.Build.0 = Release|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM.ActiveCfg = Release|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM.Build.0 = Release|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM64.Build.0 = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x64.ActiveCfg = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x64.Build.0 = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x86.ActiveCfg = Release|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x86.Build.0 = Release|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.ActiveCfg = Debug|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.Build.0 = Debug|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.Build.0 = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.ActiveCfg = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.Build.0 = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.ActiveCfg = Debug|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.Build.0 = Debug|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.ActiveCfg = Release|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.Build.0 = Release|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.ActiveCfg = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.Build.0 = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.ActiveCfg = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.Build.0 = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.ActiveCfg = Release|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.Build.0 = Release|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM.ActiveCfg = Release|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM.Build.0 = Release|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM64.Build.0 = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x64.ActiveCfg = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x64.Build.0 = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x86.ActiveCfg = Release|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x86.Build.0 = Release|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.ActiveCfg = Debug|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.Build.0 = Debug|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.Build.0 = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.ActiveCfg = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.Build.0 = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.ActiveCfg = Debug|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.Build.0 = Debug|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.ActiveCfg = Release|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.Build.0 = Release|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.ActiveCfg = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.Build.0 = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.ActiveCfg = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.Build.0 = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.ActiveCfg = Release|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.Build.0 = Release|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM.ActiveCfg = Release|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM.Build.0 = Release|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM64.Build.0 = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|x64.ActiveCfg = Release|x64 diff --git a/windows/ReactNativeVideoCPP/ReactNativeVideoCPP.vcxproj b/windows/ReactNativeVideoCPP/ReactNativeVideoCPP.vcxproj index 83bddbf0..a24d1de2 100644 --- a/windows/ReactNativeVideoCPP/ReactNativeVideoCPP.vcxproj +++ b/windows/ReactNativeVideoCPP/ReactNativeVideoCPP.vcxproj @@ -21,10 +21,6 @@ $([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - Debug - ARM - Debug ARM64 @@ -37,10 +33,6 @@ Debug x64 - - Release - ARM - Release ARM64 diff --git a/windows/ReactNativeVideoCPP61.sln b/windows/ReactNativeVideoCPP61.sln index 043183d2..b8cc040f 100644 --- a/windows/ReactNativeVideoCPP61.sln +++ b/windows/ReactNativeVideoCPP61.sln @@ -53,121 +53,91 @@ Global ..\node_modules\react-native-windows\Shared\Shared.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|ARM = Release|ARM Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.ActiveCfg = Debug|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.Build.0 = Debug|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.Build.0 = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.ActiveCfg = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.Build.0 = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.ActiveCfg = Debug|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.Build.0 = Debug|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.ActiveCfg = Release|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.Build.0 = Release|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.ActiveCfg = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.Build.0 = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.ActiveCfg = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.Build.0 = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.ActiveCfg = Release|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.Build.0 = Release|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.ActiveCfg = Debug|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.Build.0 = Debug|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.Build.0 = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.ActiveCfg = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.Build.0 = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.ActiveCfg = Debug|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.Build.0 = Debug|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.ActiveCfg = Release|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.Build.0 = Release|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.ActiveCfg = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.Build.0 = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.ActiveCfg = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.Build.0 = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.ActiveCfg = Release|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.Build.0 = Release|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM.ActiveCfg = Debug|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM.Build.0 = Debug|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM64.ActiveCfg = Debug|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM64.Build.0 = Debug|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x64.ActiveCfg = Debug|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x64.Build.0 = Debug|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x86.ActiveCfg = Debug|Win32 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x86.Build.0 = Debug|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM.ActiveCfg = Release|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM.Build.0 = Release|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM64.ActiveCfg = Release|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM64.Build.0 = Release|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x64.ActiveCfg = Release|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x64.Build.0 = Release|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x86.ActiveCfg = Release|Win32 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x86.Build.0 = Release|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.ActiveCfg = Debug|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.Build.0 = Debug|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.Build.0 = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.ActiveCfg = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.Build.0 = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.ActiveCfg = Debug|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.Build.0 = Debug|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.ActiveCfg = Release|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.Build.0 = Release|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.ActiveCfg = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.Build.0 = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.ActiveCfg = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.Build.0 = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.ActiveCfg = Release|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.Build.0 = Release|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.ActiveCfg = Debug|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.Build.0 = Debug|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.Build.0 = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.ActiveCfg = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.Build.0 = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.ActiveCfg = Debug|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.Build.0 = Debug|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.ActiveCfg = Release|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.Build.0 = Release|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.ActiveCfg = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.Build.0 = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.ActiveCfg = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.Build.0 = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.ActiveCfg = Release|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.Build.0 = Release|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.ActiveCfg = Debug|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.Build.0 = Debug|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.Build.0 = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.ActiveCfg = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.Build.0 = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.ActiveCfg = Debug|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.Build.0 = Debug|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.ActiveCfg = Release|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.Build.0 = Release|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.ActiveCfg = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.Build.0 = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.ActiveCfg = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.Build.0 = Release|Win32 - {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM.ActiveCfg = Debug|ARM - {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM.Build.0 = Debug|ARM {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|ARM64.ActiveCfg = Debug|Win32 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x64.ActiveCfg = Debug|x64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x64.Build.0 = Debug|x64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x86.ActiveCfg = Debug|Win32 {765365E4-9553-4900-9F69-E26D4309C8DA}.Debug|x86.Build.0 = Debug|Win32 - {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM.ActiveCfg = Release|ARM - {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM.Build.0 = Release|ARM {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|ARM64.ActiveCfg = Release|Win32 {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|x64.ActiveCfg = Release|x64 {765365E4-9553-4900-9F69-E26D4309C8DA}.Release|x64.Build.0 = Release|x64 diff --git a/windows/ReactNativeVideoCPP61/ReactNativeVideoCPP61.vcxproj b/windows/ReactNativeVideoCPP61/ReactNativeVideoCPP61.vcxproj index 90c7430b..588c0bc1 100644 --- a/windows/ReactNativeVideoCPP61/ReactNativeVideoCPP61.vcxproj +++ b/windows/ReactNativeVideoCPP61/ReactNativeVideoCPP61.vcxproj @@ -21,10 +21,6 @@ $([MSBuild]::GetDirectoryNameOfFileAbove($(SolutionDir), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ - - Debug - ARM - Debug ARM64 @@ -37,10 +33,6 @@ Debug x64 - - Release - ARM - Release ARM64 diff --git a/windows/ReactNativeVideoCPP62.sln b/windows/ReactNativeVideoCPP62.sln index 4ab5516d..bac50f46 100644 --- a/windows/ReactNativeVideoCPP62.sln +++ b/windows/ReactNativeVideoCPP62.sln @@ -48,182 +48,137 @@ Global ..\node_modules\react-native-windows\Shared\Shared.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM = Debug|ARM Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|ARM = Release|ARM Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 - WinUI3|ARM = WinUI3|ARM WinUI3|ARM64 = WinUI3|ARM64 WinUI3|x64 = WinUI3|x64 WinUI3|x86 = WinUI3|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM.ActiveCfg = Debug|ARM - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM.Build.0 = Debug|ARM {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM64.ActiveCfg = Debug|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|ARM64.Build.0 = Debug|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x64.ActiveCfg = Debug|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x64.Build.0 = Debug|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x86.ActiveCfg = Debug|Win32 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Debug|x86.Build.0 = Debug|Win32 - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM.ActiveCfg = Release|ARM - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM.Build.0 = Release|ARM {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM64.ActiveCfg = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|ARM64.Build.0 = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x64.ActiveCfg = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x64.Build.0 = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x86.ActiveCfg = Release|Win32 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.Release|x86.Build.0 = Release|Win32 - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM.ActiveCfg = Release|ARM - {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM.Build.0 = Release|ARM {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|ARM64.Build.0 = Release|ARM64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x64.ActiveCfg = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x64.Build.0 = Release|x64 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x86.ActiveCfg = Release|Win32 {0D1E54D3-4BE1-4DAF-98BF-124C28C85014}.WinUI3|x86.Build.0 = Release|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.ActiveCfg = Debug|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM.Build.0 = Debug|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|ARM64.Build.0 = Debug|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.ActiveCfg = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x64.Build.0 = Debug|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.ActiveCfg = Debug|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Debug|x86.Build.0 = Debug|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.ActiveCfg = Release|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM.Build.0 = Release|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.ActiveCfg = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|ARM64.Build.0 = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.ActiveCfg = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.Build.0 = Release|Win32 - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM.ActiveCfg = Release|ARM - {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM.Build.0 = Release|ARM {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|ARM64.Build.0 = Release|ARM64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x64.ActiveCfg = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.WinUI3|x86.Build.0 = Release|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.ActiveCfg = Debug|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM.Build.0 = Debug|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|ARM64.Build.0 = Debug|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.ActiveCfg = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x64.Build.0 = Debug|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.ActiveCfg = Debug|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Debug|x86.Build.0 = Debug|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.ActiveCfg = Release|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM.Build.0 = Release|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.ActiveCfg = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|ARM64.Build.0 = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.ActiveCfg = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x64.Build.0 = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.ActiveCfg = Release|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.Release|x86.Build.0 = Release|Win32 - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM.ActiveCfg = Release|ARM - {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM.Build.0 = Release|ARM {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|ARM64.Build.0 = Release|ARM64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x64.ActiveCfg = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x64.Build.0 = Release|x64 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x86.ActiveCfg = Release|Win32 {A990658C-CE31-4BCC-976F-0FC6B1AF693D}.WinUI3|x86.Build.0 = Release|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.ActiveCfg = Debug|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM.Build.0 = Debug|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|ARM64.Build.0 = Debug|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.ActiveCfg = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x64.Build.0 = Debug|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.ActiveCfg = Debug|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Debug|x86.Build.0 = Debug|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.ActiveCfg = Release|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM.Build.0 = Release|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.ActiveCfg = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|ARM64.Build.0 = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.ActiveCfg = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x64.Build.0 = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.ActiveCfg = Release|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.Release|x86.Build.0 = Release|Win32 - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM.ActiveCfg = Release|ARM - {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM.Build.0 = Release|ARM {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|ARM64.Build.0 = Release|ARM64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x64.ActiveCfg = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x64.Build.0 = Release|x64 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x86.ActiveCfg = Release|Win32 {A62D504A-16B8-41D2-9F19-E2E86019E5E4}.WinUI3|x86.Build.0 = Release|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.ActiveCfg = Debug|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM.Build.0 = Debug|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|ARM64.Build.0 = Debug|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.ActiveCfg = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x64.Build.0 = Debug|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.ActiveCfg = Debug|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Debug|x86.Build.0 = Debug|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.ActiveCfg = Release|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM.Build.0 = Release|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.ActiveCfg = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|ARM64.Build.0 = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.ActiveCfg = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x64.Build.0 = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.ActiveCfg = Release|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.Release|x86.Build.0 = Release|Win32 - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM.ActiveCfg = Release|ARM - {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM.Build.0 = Release|ARM {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|ARM64.Build.0 = Release|ARM64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x64.ActiveCfg = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x64.Build.0 = Release|x64 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x86.ActiveCfg = Release|Win32 {F7D32BD0-2749-483E-9A0D-1635EF7E3136}.WinUI3|x86.Build.0 = Release|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.ActiveCfg = Debug|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM.Build.0 = Debug|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|ARM64.Build.0 = Debug|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.ActiveCfg = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x64.Build.0 = Debug|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.ActiveCfg = Debug|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Debug|x86.Build.0 = Debug|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.ActiveCfg = Release|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM.Build.0 = Release|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.ActiveCfg = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|ARM64.Build.0 = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.ActiveCfg = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x64.Build.0 = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.ActiveCfg = Release|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.Release|x86.Build.0 = Release|Win32 - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM.ActiveCfg = Release|ARM - {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM.Build.0 = Release|ARM {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM64.ActiveCfg = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|ARM64.Build.0 = Release|ARM64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|x64.ActiveCfg = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|x64.Build.0 = Release|x64 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|x86.ActiveCfg = Release|Win32 {A9D95A91-4DB7-4F72-BEB6-FE8A5C89BFBD}.WinUI3|x86.Build.0 = Release|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM.ActiveCfg = Debug|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM.Build.0 = Debug|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM64.ActiveCfg = Debug|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|ARM64.Build.0 = Debug|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x64.ActiveCfg = Debug|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x64.Build.0 = Debug|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x86.ActiveCfg = Debug|Win32 {11C084A3-A57C-4296-A679-CAC17B603144}.Debug|x86.Build.0 = Debug|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM.ActiveCfg = Release|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM.Build.0 = Release|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM64.ActiveCfg = Release|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|ARM64.Build.0 = Release|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x64.ActiveCfg = Release|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x64.Build.0 = Release|x64 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x86.ActiveCfg = Release|Win32 {11C084A3-A57C-4296-A679-CAC17B603144}.Release|x86.Build.0 = Release|Win32 - {11C084A3-A57C-4296-A679-CAC17B603144}.WinUI3|ARM.ActiveCfg = WinUI3|ARM - {11C084A3-A57C-4296-A679-CAC17B603144}.WinUI3|ARM.Build.0 = WinUI3|ARM {11C084A3-A57C-4296-A679-CAC17B603144}.WinUI3|ARM64.ActiveCfg = WinUI3|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.WinUI3|ARM64.Build.0 = WinUI3|ARM64 {11C084A3-A57C-4296-A679-CAC17B603144}.WinUI3|x64.ActiveCfg = WinUI3|x64 From 2b310cbf7ef5d07934d348c6f731328e164de195 Mon Sep 17 00:00:00 2001 From: Gabriel Rivero Date: Tue, 16 Nov 2021 20:35:30 -0400 Subject: [PATCH 036/194] force garbage collector when free memory reaches 0 --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index ea27b947..8843702e 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -423,6 +423,7 @@ class ReactExoplayerView extends FrameLayout implements private class RNVLoadControl extends DefaultLoadControl { private int availableHeapInBytes = 0; + private Runtime runtime; public RNVLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, int bufferForPlaybackMs, int bufferForPlaybackAfterRebufferMs, int targetBufferBytes, boolean prioritizeTimeOverSizeThresholds, int backBufferDurationMs, boolean retainBackBufferFromKeyframe) { super(allocator, minBufferMs, @@ -433,6 +434,7 @@ class ReactExoplayerView extends FrameLayout implements prioritizeTimeOverSizeThresholds, backBufferDurationMs, retainBackBufferFromKeyframe); + runtime = Runtime.getRuntime(); ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(themedReactContext.ACTIVITY_SERVICE); availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeapAllocationPercent * 1024 * 1024); } @@ -447,6 +449,11 @@ class ReactExoplayerView extends FrameLayout implements if (isHeapReached) { return false; } + if (runtime.freeMemory() == 0) { + Log.w("ExoPlayer Warning", "free memory reached 0, forcing garbage collection"); + runtime.gc(); + return false; + } return super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed); } } From ab202c96b6f68ca89304dcb3fccd49d526c5df95 Mon Sep 17 00:00:00 2001 From: Gabriel Rivero Date: Tue, 16 Nov 2021 21:40:38 -0400 Subject: [PATCH 037/194] typo --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 8843702e..eb4dfa8d 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -450,7 +450,7 @@ class ReactExoplayerView extends FrameLayout implements return false; } if (runtime.freeMemory() == 0) { - Log.w("ExoPlayer Warning", "free memory reached 0, forcing garbage collection"); + Log.w("ExoPlayer Warning", "Free memory reached 0, forcing garbage collection"); runtime.gc(); return false; } From d10f7c5b85b1e6e81bab7e55dcc3f8e466e1f368 Mon Sep 17 00:00:00 2001 From: mike castleman Date: Mon, 6 Dec 2021 12:44:47 -0500 Subject: [PATCH 038/194] clarify onProgress documentation The docs for `progressUpdateInterval` say that the value is in milliseconds, but the docs for `onProgress` said that the value was in seconds. This fixes the latter documentation to be consistent with the former. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab9715d4..6b41ca8e 100644 --- a/README.md +++ b/README.md @@ -1113,7 +1113,7 @@ Platforms: all #### onProgress -Callback function that is called every progressUpdateInterval seconds with info about which position the media is currently playing. +Callback function that is called every progressUpdateInterval milliseconds with info about which position the media is currently playing. Property | Type | Description --- | --- | --- From 7373761a4c9566650179e40bba52681ff9b8cec2 Mon Sep 17 00:00:00 2001 From: Tuan Luong Date: Sun, 12 Dec 2021 13:47:50 +0700 Subject: [PATCH 039/194] check null before trigger action --- .../ExoPlayerFullscreenVideoActivity.java | 46 ++++++++++++------- .../exoplayer/ReactExoplayerView.java | 18 ++------ 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java index 09b2cdfd..d2e6f2d3 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerFullscreenVideoActivity.java @@ -15,19 +15,23 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ui.PlayerControlView; public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implements ReactExoplayerView.FullScreenDelegate { - public static final String EXTRA_ID = "extra_id"; + public static final String EXTRA_EXO_PLAYER_VIEW_ID = "extra_id"; public static final String EXTRA_ORIENTATION = "extra_orientation"; - - private int id; + + private ReactExoplayerView exoplayerView; private PlayerControlView playerControlView; private SimpleExoPlayer player; @Override public void onCreate(Bundle savedInstanceState) { + int exoplayerViewId = getIntent().getIntExtra(EXTRA_EXO_PLAYER_VIEW_ID, -1); + exoplayerView = ReactExoplayerView.getViewInstance(exoplayerViewId); + if (exoplayerView == null) { + finish(); + return; + } super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - id = getIntent().getIntExtra(EXTRA_ID, -1); String orientation = getIntent().getStringExtra(EXTRA_ORIENTATION); if ("landscape".equals(orientation)) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); @@ -35,7 +39,7 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); } setContentView(R.layout.exo_player_fullscreen_video); - player = ReactExoplayerView.getViewInstance(id).getPlayer(); + player = exoplayerView.getPlayer(); ExoPlayerView playerView = findViewById(R.id.player_view); playerView.setPlayer(player); @@ -47,25 +51,35 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen ImageView fullscreenIcon = playerControlView.findViewById(R.id.exo_fullscreen_icon); fullscreenIcon.setImageResource(R.drawable.exo_controls_fullscreen_exit); playerControlView.findViewById(R.id.exo_fullscreen_button) - .setOnClickListener(v -> ReactExoplayerView.getViewInstance(id).setFullscreen(false)); + .setOnClickListener(v -> { + if (exoplayerView != null) { + exoplayerView.setFullscreen(false); + } + }); //Handling the playButton click event playerControlView.findViewById(R.id.exo_play).setOnClickListener(v -> { if (player != null && player.getPlaybackState() == Player.STATE_ENDED) { player.seekTo(0); } - ReactExoplayerView.getViewInstance(id).setPausedModifier(false); + if (exoplayerView != null) { + exoplayerView.setPausedModifier(false); + } }); //Handling the pauseButton click event - playerControlView.findViewById(R.id.exo_pause).setOnClickListener(v -> ReactExoplayerView.getViewInstance(id).setPausedModifier(true)); + playerControlView.findViewById(R.id.exo_pause).setOnClickListener(v -> { + if (exoplayerView != null) { + exoplayerView.setPausedModifier(true); + } + }); } @Override public void onResume() { super.onResume(); - if (ReactExoplayerView.getViewInstance(id) != null) { - ReactExoplayerView.getViewInstance(id).syncPlayerState(); - ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(this); + if (exoplayerView != null) { + exoplayerView.syncPlayerState(); + exoplayerView.registerFullScreenDelegate(this); } } @@ -73,8 +87,8 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen public void onPause() { super.onPause(); player.setPlayWhenReady(false); - if (ReactExoplayerView.getViewInstance(id) != null) { - ReactExoplayerView.getViewInstance(id).registerFullScreenDelegate(null); + if (exoplayerView != null) { + exoplayerView.registerFullScreenDelegate(null); } } @@ -89,8 +103,8 @@ public class ExoPlayerFullscreenVideoActivity extends AppCompatActivity implemen @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if ((keyCode == KeyEvent.KEYCODE_BACK)) { - if (ReactExoplayerView.getViewInstance(id) != null) { - ReactExoplayerView.getViewInstance(id).setFullscreen(false); + if (exoplayerView != null) { + exoplayerView.setFullscreen(false); return false; } return true; diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index edd3c45b..482ff939 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -38,7 +37,6 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; @@ -60,8 +58,8 @@ 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.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -77,8 +75,8 @@ import java.net.CookiePolicy; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; -import java.util.UUID; import java.util.Map; +import java.util.UUID; @SuppressLint("ViewConstructor") class ReactExoplayerView extends FrameLayout implements @@ -101,8 +99,6 @@ class ReactExoplayerView extends FrameLayout implements } private static Map instances = new HashMap<>(); - private static int UNIQUE_ID = 0; - private int uid = ++UNIQUE_ID; private FullScreenDelegate fullScreenDelegate; private final VideoEventEmitter eventEmitter; @@ -141,8 +137,6 @@ class ReactExoplayerView extends FrameLayout implements private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; - private Handler mainHandler; - // Props from React private Uri srcUri; private String extension; @@ -233,8 +227,6 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setLayoutParams(layoutParams); addView(exoPlayerView, 0, layoutParams); - - mainHandler = new Handler(); } @Override @@ -286,7 +278,7 @@ class ReactExoplayerView extends FrameLayout implements public void cleanUpResources() { stopPlayback(); - instances.remove(uid); + instances.remove(this.getId()); } //BandwidthMeter.EventListener implementation @@ -344,9 +336,9 @@ class ReactExoplayerView extends FrameLayout implements } private void showFullscreen() { - instances.put(uid, this); + instances.put(this.getId(), this); Intent intent = new Intent(getContext(), ExoPlayerFullscreenVideoActivity.class); - intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ID, this.uid); + intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_EXO_PLAYER_VIEW_ID, this.getId()); intent.putExtra(ExoPlayerFullscreenVideoActivity.EXTRA_ORIENTATION, this.fullScreenOrientation); getContext().startActivity(intent); isInFullscreen = true; From 24434f44c2da6a9cfdf21315f56ceaf911b95c67 Mon Sep 17 00:00:00 2001 From: Preetam D'Souza Date: Fri, 14 Jan 2022 23:48:11 -0500 Subject: [PATCH 040/194] Fix NPE with null codecInfo --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index fe95fdf4..90ea2cc7 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -947,7 +947,8 @@ class ReactExoplayerView extends FrameLayout implements // Special case for decoder initialization failures. MediaCodecRenderer.DecoderInitializationException decoderInitializationException = (MediaCodecRenderer.DecoderInitializationException) cause; - if (decoderInitializationException.codecInfo.name == null) { + if (decoderInitializationException.codecInfo == null + || decoderInitializationException.codecInfo.name == null) { if (decoderInitializationException.getCause() instanceof MediaCodecUtil.DecoderQueryException) { errorString = getResources().getString(R.string.error_querying_decoders); } else if (decoderInitializationException.secureDecoderRequired) { From 2ab029995f91dd3a073f896d2b4065d618d74282 Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Fri, 21 Jan 2022 14:10:22 +0200 Subject: [PATCH 041/194] VEX-6365: Improve memory management (#17) Improve memory management to reduce pressure on low end devices. JIRA: VEX-6365 --- README.md | 2 ++ .../exoplayer/ReactExoplayerView.java | 27 +++++++++++++++++-- .../exoplayer/ReactExoplayerViewManager.java | 11 +++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 564423f8..c0a34da6 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,8 @@ maxBufferMs | number | The default maximum duration of media that the player wil bufferForPlaybackMs | number | The default duration of media that must be buffered for playback to start or resume following a user action such as a seek, in milliseconds. bufferForPlaybackAfterRebufferMs | number | The default duration of media that must be buffered for playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user action. maxHeapAllocationPercent | number | The percentage of available heap that the video can use to buffer, between 0 and 1 +minBackBufferMemoryReservePercent | number | The percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1 +minBufferMemoryReservePercent | number | The percentage of available app memory to keep in reserve that prevents buffer from using it, between 0 and 1 This prop should only be set when you are setting the source, changing it after the media is loaded will cause it to be reloaded. diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index eb4dfa8d..5dcb5e0d 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -113,6 +113,8 @@ class ReactExoplayerView extends FrameLayout implements DrmSessionEventListener { public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1; + public static final double DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE = 0; + public static final double DEFAULT_MIN_BUFFER_MEMORY_RESERVE = 0; private static final String TAG = "ReactExoplayerView"; @@ -161,7 +163,8 @@ class ReactExoplayerView extends FrameLayout implements private int bufferForPlaybackMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS; private int bufferForPlaybackAfterRebufferMs = DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS; private double maxHeapAllocationPercent = ReactExoplayerView.DEFAULT_MAX_HEAP_ALLOCATION_PERCENT; - + private double minBackBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE; + private double minBufferMemoryReservePercent = ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE; private Handler mainHandler; private Timer bufferCheckTimer; @@ -449,6 +452,14 @@ class ReactExoplayerView extends FrameLayout implements if (isHeapReached) { return false; } + long usedMemory = runtime.totalMemory() - runtime.freeMemory(); + long freeMemory = runtime.maxMemory() - usedMemory; + long reserveMemory = (long)minBufferMemoryReservePercent * runtime.maxMemory(); + long bufferedMs = bufferedDurationUs / (long)1000; + if (reserveMemory > freeMemory && bufferedMs > 2000) { + // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead + return false; + } if (runtime.freeMemory() == 0) { Log.w("ExoPlayer Warning", "Free memory reached 0, forcing garbage collection"); runtime.gc(); @@ -1622,6 +1633,16 @@ class ReactExoplayerView extends FrameLayout implements } public void setBackBufferDurationMs(int backBufferDurationMs) { + Runtime runtime = Runtime.getRuntime(); + long usedMemory = runtime.totalMemory() - runtime.freeMemory(); + long freeMemory = runtime.maxMemory() - usedMemory; + long reserveMemory = (long)minBackBufferMemoryReservePercent * runtime.maxMemory(); + if (reserveMemory > freeMemory) { + // We don't have enough memory in reserve so we will + Log.w("ExoPlayer Warning", "Not enough reserve memory, setting back buffer to 0ms to reduce memory pressure!"); + this.backBufferDurationMs = 0; + return; + } this.backBufferDurationMs = backBufferDurationMs; } @@ -1679,12 +1700,14 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setHideShutterView(hideShutterView); } - public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent) { + public void setBufferConfig(int newMinBufferMs, int newMaxBufferMs, int newBufferForPlaybackMs, int newBufferForPlaybackAfterRebufferMs, double newMaxHeapAllocationPercent, double newMinBackBufferMemoryReservePercent, double newMinBufferMemoryReservePercent) { minBufferMs = newMinBufferMs; maxBufferMs = newMaxBufferMs; bufferForPlaybackMs = newBufferForPlaybackMs; bufferForPlaybackAfterRebufferMs = newBufferForPlaybackAfterRebufferMs; maxHeapAllocationPercent = newMaxHeapAllocationPercent; + minBackBufferMemoryReservePercent = newMinBackBufferMemoryReservePercent; + minBufferMemoryReservePercent = newMinBufferMemoryReservePercent; releasePlayer(); initializePlayer(); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index a923e050..5d8a84d4 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -56,6 +56,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Fri, 4 Feb 2022 12:32:34 -0800 Subject: [PATCH 042/194] Fix lint --- DRMType.js | 2 +- Video.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DRMType.js b/DRMType.js index 473536b2..d873bd69 100644 --- a/DRMType.js +++ b/DRMType.js @@ -2,5 +2,5 @@ export default { WIDEVINE: 'widevine', PLAYREADY: 'playready', CLEARKEY: 'clearkey', - FAIRPLAY: 'fairplay' + FAIRPLAY: 'fairplay', }; diff --git a/Video.js b/Video.js index b492d48b..e1eb6b3b 100644 --- a/Video.js +++ b/Video.js @@ -249,7 +249,7 @@ export default class Video extends Component { NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError(error, findNodeHandle(this._root)); }); } else { - NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError("No spc received", findNodeHandle(this._root)); + NativeModules.VideoManager.setLicenseError && NativeModules.VideoManager.setLicenseError('No spc received', findNodeHandle(this._root)); } } } @@ -403,7 +403,7 @@ Video.propTypes = { ]), drm: PropTypes.shape({ type: PropTypes.oneOf([ - DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY + DRMType.CLEARKEY, DRMType.FAIRPLAY, DRMType.WIDEVINE, DRMType.PLAYREADY, ]), licenseServer: PropTypes.string, headers: PropTypes.shape({}), From 9398a15fc6976803b8664dead13e74f333d027da Mon Sep 17 00:00:00 2001 From: Owen Hart Date: Fri, 4 Feb 2022 16:29:58 -0800 Subject: [PATCH 043/194] Update shaka-player --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb8d146e..b7652584 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "dependencies": { "keymirror": "^0.1.1", "prop-types": "^15.7.2", - "shaka-player": "^2.5.9" + "shaka-player": "^3.3.1" }, "scripts": { "lint": "yarn eslint *.js" From 2af360a7bb0462582a05f20c9f5f7fdf508b0ade Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 14 Feb 2022 21:17:22 -0400 Subject: [PATCH 044/194] add useSecureView prop to prevent screen recording --- README.md | 13 +++++++++++++ Video.js | 1 + .../brentvatne/exoplayer/ExoPlayerView.java | 18 +++++++++++++++++- .../exoplayer/ReactExoplayerView.java | 4 ++++ .../exoplayer/ReactExoplayerViewManager.java | 6 ++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c0a34da6..2881926f 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,7 @@ var styles = StyleSheet.create({ * [textTracks](#texttracks) * [trackId](#trackId) * [useTextureView](#usetextureview) +* [useSecureView](#useSecureView) * [volume](#volume) * [localSourceEncryptionKeyScheme](#localSourceEncryptionKeyScheme) @@ -911,6 +912,18 @@ useTextureView can only be set at same time you're setting the source. Platforms: Android ExoPlayer +#### useSecureView +Force the output to a SurfaceView and enables the secure surface. + +This will override useTextureView flag. + +SurfaceView is is the only one that can be labeled as secure. + +* **true** - Use security +* **false (default)** - Do not use security + +Platforms: Android ExoPlayer + #### volume Adjust the volume. * **1.0 (default)** - Play at full volume diff --git a/Video.js b/Video.js index 0a97be1d..db88752f 100644 --- a/Video.js +++ b/Video.js @@ -488,6 +488,7 @@ Video.propTypes = { fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']), progressUpdateInterval: PropTypes.number, useTextureView: PropTypes.bool, + useSecureView: PropTypes.bool, hideShutterView: PropTypes.bool, onLoadStart: PropTypes.func, onLoad: PropTypes.func, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 61cb0dd6..a91618d5 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -41,6 +41,7 @@ public final class ExoPlayerView extends FrameLayout { private ViewGroup.LayoutParams layoutParams; private boolean useTextureView = true; + private boolean useSecureView = false; private boolean hideShutterView = false; public ExoPlayerView(Context context) { @@ -103,7 +104,15 @@ public final class ExoPlayerView extends FrameLayout { } private void updateSurfaceView() { - View view = useTextureView ? new TextureView(context) : new SurfaceView(context); + View view; + if (!useTextureView || useSecureView) { + view = new SurfaceView(context); + if (useSecureView) { + ((SurfaceView)view).setSecure(true); + } + } else { + view = new TextureView(context); + } view.setLayoutParams(layoutParams); surfaceView = view; @@ -178,6 +187,13 @@ public final class ExoPlayerView extends FrameLayout { } } + public void useSecureView(boolean useSecureView) { + if (useSecureView != this.useSecureView) { + this.useSecureView = useSecureView; + updateSurfaceView(); + } + } + public void setHideShutterView(boolean hideShutterView) { this.hideShutterView = hideShutterView; updateShutterViewVisibility(); diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 5dcb5e0d..e4000116 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1696,6 +1696,10 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setUseTextureView(finallyUseTextureView); } + public void useSecureView(boolean useSecureView) { + exoPlayerView.useSecureView(useSecureView); + } + public void setHideShutterView(boolean hideShutterView) { exoPlayerView.setHideShutterView(hideShutterView); } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 5d8a84d4..3744fcae 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -72,6 +72,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager Date: Thu, 3 Mar 2022 15:57:21 -0800 Subject: [PATCH 045/194] Always check for hideShutterView before showing shutterView --- .../src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 61cb0dd6..f8881631 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -139,7 +139,7 @@ public final class ExoPlayerView extends FrameLayout { clearVideoView(); } this.player = player; - shutterView.setVisibility(VISIBLE); + shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE); if (player != null) { setVideoView(); player.addVideoListener(componentListener); @@ -206,7 +206,7 @@ public final class ExoPlayerView extends FrameLayout { } } // Video disabled so the shutter must be closed. - shutterView.setVisibility(VISIBLE); + shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE); } public void invalidateAspectRatio() { From 4eed72f22462086fc9c5cce0ecf60e498b0548a0 Mon Sep 17 00:00:00 2001 From: Igor Tironi Date: Thu, 3 Mar 2022 16:03:59 -0800 Subject: [PATCH 046/194] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7f5943..aac2d41e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### Version 5.2.2 + +- Ensure we always use `hideShutterView` before showing the `shutterView` on Android + ### Version 5.2.1 - Add Google's maven repository to avoid build error [#2552] (https://github.com/react-native-video/react-native-video/pull/2552) From b6f365263681bbc7c7e6a6fba1608292211433b1 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Tue, 29 Mar 2022 15:21:39 +0300 Subject: [PATCH 047/194] Capture more errors from DRM session manager --- .../exoplayer/ReactExoplayerView.java | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e4000116..3dbc0e20 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -564,6 +564,10 @@ class ReactExoplayerView extends FrameLayout implements return; } } + if (drmSessionManager == null) { + // DRM Session Manager failed to instantiate, no other work can be done - error has been handled by the instantiating method + return; + } // End DRM ArrayList mediaSourceList = buildTextSources(); @@ -608,26 +612,36 @@ class ReactExoplayerView extends FrameLayout implements } - private DrmSessionManager buildDrmSessionManager(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, int retryCount) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; } - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, - buildHttpDataSourceFactory(false)); - if (keyRequestPropertiesArray != null) { - for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { - drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], - keyRequestPropertiesArray[i + 1]); + try { + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, + buildHttpDataSourceFactory(false)); + if (keyRequestPropertiesArray != null) { + for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) { + drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); + } } + FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); + if (hasDrmFailed) { + // When DRM fails using L1 we want to switch to L3 + mediaDrm.setPropertyString("securityLevel", "L3"); + } + return new DefaultDrmSessionManager(uuid, mediaDrm, drmCallback, null, false, 3); + } catch(UnsupportedDrmException e) { + // Unsupported DRM exceptions are handled by the calling method + throw e; + } catch (Exception e) { + if (retryCount < 3) { + // Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason + return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, ++retryCount); + } + // Handle the unknow exception and emit to JS + eventEmitter.error(ex.toString(), ex, "3006"); + return null; } - FrameworkMediaDrm mediaDrm = FrameworkMediaDrm.newInstance(uuid); - if (hasDrmFailed) { - // When DRM fails using L1 we want to switch to L3 - mediaDrm.setPropertyString("securityLevel", "L3"); - } - return new DefaultDrmSessionManager(uuid, - mediaDrm, drmCallback, null, false, 3); } private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { From 39dee21efab52c5d868abe09c3feb63cd036f3e7 Mon Sep 17 00:00:00 2001 From: Nick Fujita Date: Wed, 30 Mar 2022 18:17:56 +0900 Subject: [PATCH 048/194] Updates disableDisconnectError retry condition to be more specific (#22) Updates disableDisconnectError retry condition to be more specific. Jira: VEX-6518 Reviews Major reviewer (domain expert): @armadilio3 Minor reviewer: @gabriel-rivero --- .../exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java index c701310f..d0d68de1 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerLoadErrorHandlingPolicy.java @@ -16,7 +16,10 @@ public final class ReactExoplayerLoadErrorHandlingPolicy extends DefaultLoadErro @Override public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) { - if (loadErrorInfo.exception instanceof HttpDataSourceException) { + if ( + loadErrorInfo.exception instanceof HttpDataSourceException && + (loadErrorInfo.exception.getMessage() == "Unable to connect" || loadErrorInfo.exception.getMessage() == "Software caused connection abort") + ) { // Capture the error we get when there is no network connectivity and keep retrying it return 1000; // Retry every second } else if(loadErrorInfo.errorCount < this.minLoadRetryCount) { From 88f32ae3c8e8d21a24d67d39042016fdcf77b775 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 14:29:08 +0300 Subject: [PATCH 049/194] Move DRM init to a separate thread --- .../exoplayer/ReactExoplayerView.java | 127 +++++++++++------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 3dbc0e20..13eaca83 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -7,6 +7,7 @@ import android.content.Context; import android.media.AudioManager; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.text.TextUtils; import android.util.Log; @@ -547,59 +548,33 @@ class ReactExoplayerView extends FrameLayout implements PlaybackParameters params = new PlaybackParameters(rate, 1f); player.setPlaybackParameters(params); } + if (playerNeedsSource && srcUri != null) { exoPlayerView.invalidateAspectRatio(); + // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread + ExecutorService es = Executors.newSingleThreadExecutor(); + es.execute(new Runnable() { + @Override + public void run() { + + // DRM initialization must run on a different thread + initializePlayerDrm(); + + // Initialize handler to run on the main thread + new Handler(Looper.getMainLooper()).post(new Runnable () { + @Override + public void run () { + // Source initialization must run on the main thread + initializePlayerSource(); + } + }); - // DRM - DrmSessionManager drmSessionManager = null; - if (self.drmUUID != null) { - try { - drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, - self.drmLicenseHeader); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); - eventEmitter.error(getResources().getString(errorStringId), e, "3003"); - return; } - } - if (drmSessionManager == null) { - // DRM Session Manager failed to instantiate, no other work can be done - error has been handled by the instantiating method - return; - } - // End DRM - - ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); - 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; - - reLayout(exoPlayerView); - eventEmitter.loadStart(); - loadVideoStarted = true; + }); + } else { + finishPlayerInitialization(); } - - // Initializing the playerControlView - initializePlayerControl(); - setControls(controls); - applyModifiers(); - startBufferCheckTimer(); + } catch (Exception ex) { self.playerNeedsSource = true; @@ -612,6 +587,62 @@ class ReactExoplayerView extends FrameLayout implements } + private void initializePlayerDrm() { + DrmSessionManager drmSessionManager = null; + if (self.drmUUID != null) { + try { + drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, + self.drmLicenseHeader); + } catch (UnsupportedDrmException e) { + int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported + : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + eventEmitter.error(getResources().getString(errorStringId), e, "3003"); + return; + } + } + if (drmSessionManager == null) { + // DRM Session Manager failed to instantiate, no other work can be done - error has been handled by the instantiating method + return; + } + } + + private void initializePlayerSource() { + ArrayList mediaSourceList = buildTextSources(); + MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); + 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; + + reLayout(exoPlayerView); + eventEmitter.loadStart(); + loadVideoStarted = true; + + finishPlayerInitialization(); + } + + private void finishPlayerInitialization() { + // Initializing the playerControlView + initializePlayerControl(); + setControls(controls); + applyModifiers(); + startBufferCheckTimer(); + } + private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, int retryCount) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; From 2f77765fe2ce4fbd33540596358cba625cd94b1a Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 14:35:48 +0300 Subject: [PATCH 050/194] Clean up --- .../exoplayer/ReactExoplayerView.java | 85 ++++++++++--------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 13eaca83..98413ef5 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -512,41 +512,8 @@ class ReactExoplayerView extends FrameLayout implements public void run() { try { if (player == null) { - ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelector.setParameters(trackSelector.buildUponParameters() - .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); - - DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - RNVLoadControl loadControl = new RNVLoadControl( - allocator, - minBufferMs, - maxBufferMs, - bufferForPlaybackMs, - bufferForPlaybackAfterRebufferMs, - -1, - true, - backBufferDurationMs, - DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME - ); - DefaultRenderersFactory renderersFactory = - new DefaultRenderersFactory(getContext()) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) - .setTrackSelector​(trackSelector) - .setBandwidthMeter(bandwidthMeter) - .setLoadControl(loadControl) - .build(); - player.addListener(self); - player.addMetadataOutput(self); - exoPlayerView.setPlayer(player); - audioBecomingNoisyReceiver.setListener(self); - bandwidthMeter.addEventListener(new Handler(), self); - setPlayWhenReady(!isPaused); - playerNeedsSource = true; - - PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); + // Initialize core configuration and listeners + initializePlayerCore(self); } if (playerNeedsSource && srcUri != null) { @@ -558,21 +525,21 @@ class ReactExoplayerView extends FrameLayout implements public void run() { // DRM initialization must run on a different thread - initializePlayerDrm(); + initializePlayerDrm(self); // Initialize handler to run on the main thread new Handler(Looper.getMainLooper()).post(new Runnable () { @Override public void run () { // Source initialization must run on the main thread - initializePlayerSource(); + initializePlayerSource(self); } }); } }); } else { - finishPlayerInitialization(); + initializePlayerSource(self); } @@ -587,7 +554,45 @@ class ReactExoplayerView extends FrameLayout implements } - private void initializePlayerDrm() { + private void initializePlayerCore(ReactExoplayerView self) { + ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); + trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + trackSelector.setParameters(trackSelector.buildUponParameters() + .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); + + DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + RNVLoadControl loadControl = new RNVLoadControl( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + -1, + true, + backBufferDurationMs, + DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME + ); + DefaultRenderersFactory renderersFactory = + new DefaultRenderersFactory(getContext()) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); + player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) + .setTrackSelector​(trackSelector) + .setBandwidthMeter(bandwidthMeter) + .setLoadControl(loadControl) + .build(); + player.addListener(self); + player.addMetadataOutput(self); + exoPlayerView.setPlayer(player); + audioBecomingNoisyReceiver.setListener(self); + bandwidthMeter.addEventListener(new Handler(), self); + setPlayWhenReady(!isPaused); + playerNeedsSource = true; + + PlaybackParameters params = new PlaybackParameters(rate, 1f); + player.setPlaybackParameters(params); + } + + private void initializePlayerDrm(ReactExoplayerView self) { DrmSessionManager drmSessionManager = null; if (self.drmUUID != null) { try { @@ -607,7 +612,7 @@ class ReactExoplayerView extends FrameLayout implements } } - private void initializePlayerSource() { + private void initializePlayerSource(ReactExoplayerView self) { ArrayList mediaSourceList = buildTextSources(); MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); MediaSource mediaSource; From 142521bc0ec1e37b92cb2e2a8a8402c41f6aed8e Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 14:43:04 +0300 Subject: [PATCH 051/194] Fix build issues --- .../exoplayer/ReactExoplayerView.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 98413ef5..2b0ee236 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -523,23 +523,24 @@ class ReactExoplayerView extends FrameLayout implements es.execute(new Runnable() { @Override public void run() { - // DRM initialization must run on a different thread - initializePlayerDrm(self); - + DRMSessionManager drmSessionManager = initializePlayerDrm(self); + if (drmSessionManager == null) { + // Failed to intialize DRM session manager - cannot continue + return; + } // Initialize handler to run on the main thread new Handler(Looper.getMainLooper()).post(new Runnable () { @Override public void run () { // Source initialization must run on the main thread - initializePlayerSource(self); + initializePlayerSource(self, drmSessionManager); } }); - } }); } else { - initializePlayerSource(self); + initializePlayerSource(self, null); } @@ -592,27 +593,24 @@ class ReactExoplayerView extends FrameLayout implements player.setPlaybackParameters(params); } - private void initializePlayerDrm(ReactExoplayerView self) { + private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { DrmSessionManager drmSessionManager = null; if (self.drmUUID != null) { try { - drmSessionManager = buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, + drmSessionManager = self.buildDrmSessionManager(self.drmUUID, self.drmLicenseUrl, self.drmLicenseHeader); } catch (UnsupportedDrmException e) { int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); eventEmitter.error(getResources().getString(errorStringId), e, "3003"); - return; + return null; } } - if (drmSessionManager == null) { - // DRM Session Manager failed to instantiate, no other work can be done - error has been handled by the instantiating method - return; - } + return drmSessionManager; } - private void initializePlayerSource(ReactExoplayerView self) { + private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); MediaSource mediaSource; @@ -666,10 +664,10 @@ class ReactExoplayerView extends FrameLayout implements mediaDrm.setPropertyString("securityLevel", "L3"); } return new DefaultDrmSessionManager(uuid, mediaDrm, drmCallback, null, false, 3); - } catch(UnsupportedDrmException e) { + } catch(UnsupportedDrmException ex) { // Unsupported DRM exceptions are handled by the calling method - throw e; - } catch (Exception e) { + throw ex; + } catch (Exception ex) { if (retryCount < 3) { // Attempt retry 3 times in case where the OS Media DRM Framework fails for whatever reason return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, ++retryCount); From cca274454269828d29352fa9526fa7983eeaf2a3 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 14:49:40 +0300 Subject: [PATCH 052/194] Fix typo --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 2b0ee236..ea003623 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -524,7 +524,7 @@ class ReactExoplayerView extends FrameLayout implements @Override public void run() { // DRM initialization must run on a different thread - DRMSessionManager drmSessionManager = initializePlayerDrm(self); + DrmSessionManager drmSessionManager = initializePlayerDrm(self); if (drmSessionManager == null) { // Failed to intialize DRM session manager - cannot continue return; @@ -646,6 +646,10 @@ class ReactExoplayerView extends FrameLayout implements startBufferCheckTimer(); } + private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + return buildDrmSessionManager(uuid, licenseUrl, keyRequestPropertiesArray, 0); + } + private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, int retryCount) throws UnsupportedDrmException { if (Util.SDK_INT < 18) { return null; From 7067afa609e6d7c4e801d0be446d06f082cf1f1c Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 15:04:49 +0300 Subject: [PATCH 053/194] Fix runtime crash --- .../main/java/com/brentvatne/exoplayer/ReactExoplayerView.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index ea003623..18174c41 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -500,6 +500,9 @@ class ReactExoplayerView extends FrameLayout implements } private void stopBufferCheckTimer() { + if (this.bufferCheckTimer == null) { + return; + } this.bufferCheckTimer.cancel(); this.bufferCheckTimer = null; } From bd1f7f50cb48ebba526bdf6c2fad1e7230650d32 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 15:19:50 +0300 Subject: [PATCH 054/194] Fix runtime issues --- .../exoplayer/ReactExoplayerView.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 18174c41..4d8fd980 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -560,8 +560,8 @@ class ReactExoplayerView extends FrameLayout implements private void initializePlayerCore(ReactExoplayerView self) { ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelector.setParameters(trackSelector.buildUponParameters() + self.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + self.trackSelector.setParameters(self.trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); @@ -579,21 +579,21 @@ class ReactExoplayerView extends FrameLayout implements DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) + self.player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) .setTrackSelector​(trackSelector) .setBandwidthMeter(bandwidthMeter) .setLoadControl(loadControl) .build(); - player.addListener(self); - player.addMetadataOutput(self); - exoPlayerView.setPlayer(player); - audioBecomingNoisyReceiver.setListener(self); - bandwidthMeter.addEventListener(new Handler(), self); + self.player.addListener(self); + self.player.addMetadataOutput(self); + self.exoPlayerView.setPlayer(player); + self.audioBecomingNoisyReceiver.setListener(self); + self.bandwidthMeter.addEventListener(new Handler(), self); setPlayWhenReady(!isPaused); - playerNeedsSource = true; + self.playerNeedsSource = true; PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); + self.player.setPlaybackParameters(params); } private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { @@ -615,7 +615,7 @@ class ReactExoplayerView extends FrameLayout implements private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); + MediaSource videoSource = self.buildMediaSource(self.srcUri, self.extension, drmSessionManager); MediaSource mediaSource; if (mediaSourceList.size() == 0) { mediaSource = videoSource; From edd3cc169819c7ce8c457fececd569fd52977d08 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 15:27:09 +0300 Subject: [PATCH 055/194] Fix runtime issues --- .../java/com/brentvatne/exoplayer/ReactExoplayerView.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 4d8fd980..759989ef 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -537,13 +537,13 @@ class ReactExoplayerView extends FrameLayout implements @Override public void run () { // Source initialization must run on the main thread - initializePlayerSource(self, drmSessionManager); + initializePlayerSource(self, drmSessionManager, srcUri); } }); } }); } else { - initializePlayerSource(self, null); + initializePlayerSource(self, null, srcUri); } @@ -613,9 +613,9 @@ class ReactExoplayerView extends FrameLayout implements return drmSessionManager; } - private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { + private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager, Uri srcUri) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = self.buildMediaSource(self.srcUri, self.extension, drmSessionManager); + MediaSource videoSource = self.buildMediaSource(srcUri, self.extension, drmSessionManager); MediaSource mediaSource; if (mediaSourceList.size() == 0) { mediaSource = videoSource; From 0e3fe11b7885ee2ab63dfa86c3b2caac7ff40c73 Mon Sep 17 00:00:00 2001 From: Armands Malejevs Date: Wed, 30 Mar 2022 15:33:17 +0300 Subject: [PATCH 056/194] Fix runtime issues --- .../exoplayer/ReactExoplayerView.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 759989ef..2022c108 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -537,13 +537,13 @@ class ReactExoplayerView extends FrameLayout implements @Override public void run () { // Source initialization must run on the main thread - initializePlayerSource(self, drmSessionManager, srcUri); + initializePlayerSource(self, drmSessionManager); } }); } }); - } else { - initializePlayerSource(self, null, srcUri); + } else if (srcUri != null) { + initializePlayerSource(self, null); } @@ -560,8 +560,8 @@ class ReactExoplayerView extends FrameLayout implements private void initializePlayerCore(ReactExoplayerView self) { ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - self.trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - self.trackSelector.setParameters(self.trackSelector.buildUponParameters() + trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + trackSelector.setParameters(trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); @@ -579,21 +579,21 @@ class ReactExoplayerView extends FrameLayout implements DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF); - self.player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) + player = new SimpleExoPlayer.Builder(getContext(), renderersFactory) .setTrackSelector​(trackSelector) .setBandwidthMeter(bandwidthMeter) .setLoadControl(loadControl) .build(); - self.player.addListener(self); - self.player.addMetadataOutput(self); - self.exoPlayerView.setPlayer(player); - self.audioBecomingNoisyReceiver.setListener(self); - self.bandwidthMeter.addEventListener(new Handler(), self); + player.addListener(self); + player.addMetadataOutput(self); + exoPlayerView.setPlayer(player); + audioBecomingNoisyReceiver.setListener(self); + bandwidthMeter.addEventListener(new Handler(), self); setPlayWhenReady(!isPaused); - self.playerNeedsSource = true; + playerNeedsSource = true; PlaybackParameters params = new PlaybackParameters(rate, 1f); - self.player.setPlaybackParameters(params); + player.setPlaybackParameters(params); } private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { @@ -613,9 +613,9 @@ class ReactExoplayerView extends FrameLayout implements return drmSessionManager; } - private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager, Uri srcUri) { + private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = self.buildMediaSource(srcUri, self.extension, drmSessionManager); + MediaSource videoSource = buildMediaSource(srcUri, extension, drmSessionManager); MediaSource mediaSource; if (mediaSourceList.size() == 0) { mediaSource = videoSource; From 4bf4ee7392454edcc5d302c91ed7e42bb031cbf0 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Wed, 30 Mar 2022 22:36:21 +0200 Subject: [PATCH 057/194] Use prop types from `deprecated-react-native-prop-types` Starting with React Native 0.68, using Prop Types from `react-native` emits a warning. --- Video.js | 3 ++- package.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Video.js b/Video.js index b492d48b..959d0fe7 100644 --- a/Video.js +++ b/Video.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle } from 'react-native'; +import { StyleSheet, requireNativeComponent, NativeModules, View, Image, Platform, findNodeHandle } from 'react-native'; +import { ViewPropTypes } from "deprecated-react-native-prop-types"; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import TextTrackType from './TextTrackType'; import FilterType from './FilterType'; diff --git a/package.json b/package.json index cb8d146e..9dabd081 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "react-native-windows": "^0.61.0-0" }, "dependencies": { + "deprecated-react-native-prop-types": "^2.2.0", "keymirror": "^0.1.1", "prop-types": "^15.7.2", "shaka-player": "^2.5.9" From e39d87edae3dc66e2da852d6a742e29a280ee685 Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Thu, 31 Mar 2022 15:37:53 +0300 Subject: [PATCH 058/194] VEX-6778: Android: React Native video getVideoTrackInfoFromManifest crash (#20) JIRA: VEX-6778 Move video track info request logic entirely on a different thread, completely preventing ANRs in this section of code. --- .../exoplayer/ReactExoplayerView.java | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index e4000116..f764e2c0 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -100,6 +100,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Callable; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.lang.Integer; @SuppressLint("ViewConstructor") @@ -943,8 +944,24 @@ class ReactExoplayerView extends FrameLayout implements int width = videoFormat != null ? videoFormat.width : 0; int height = videoFormat != null ? videoFormat.height : 0; String trackId = videoFormat != null ? videoFormat.id : "-1"; - eventEmitter.load(player.getDuration(), player.getCurrentPosition(), width, height, - getAudioTrackInfo(), getTextTrackInfo(), getVideoTrackInfo(), trackId); + + // Properties that must be accessed on the main thread + long duration = player.getDuration(); + long currentPosition = player.getCurrentPosition(); + WritableArray audioTrackInfo = getAudioTrackInfo(); + WritableArray textTrackInfo = getTextTrackInfo(); + Timeline timelineRef = player.getCurrentTimeline(); + int trackRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_VIDEO); + + ExecutorService es = Executors.newSingleThreadExecutor(); + es.execute(new Runnable() { + @Override + public void run() { + // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done + eventEmitter.load(duration, currentPosition, width, height, + audioTrackInfo, textTrackInfo, getVideoTrackInfo(timelineRef, trackRendererIndex), trackId); + } + }); } } @@ -971,9 +988,9 @@ class ReactExoplayerView extends FrameLayout implements } return audioTracks; } - private WritableArray getVideoTrackInfo() { + private WritableArray getVideoTrackInfo(Timeline timelineRef, int trackRendererIndex) { - WritableArray contentVideoTracks = this.getVideoTrackInfoFromManifest(); + WritableArray contentVideoTracks = this.getVideoTrackInfoFromManifest(timelineRef); if (contentVideoTracks != null) { isUsingContentResolution = true; return contentVideoTracks; @@ -982,12 +999,12 @@ class ReactExoplayerView extends FrameLayout implements WritableArray videoTracks = Arguments.createArray(); MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo(); - int index = getTrackRendererIndex(C.TRACK_TYPE_VIDEO); - if (info == null || index == C.INDEX_UNSET) { + + if (info == null || trackRendererIndex == C.INDEX_UNSET) { return videoTracks; } - TrackGroupArray groups = info.getTrackGroups(index); + TrackGroupArray groups = info.getTrackGroups(trackRendererIndex); for (int i = 0; i < groups.length; ++i) { TrackGroup group = groups.get(i); @@ -998,8 +1015,7 @@ class ReactExoplayerView extends FrameLayout implements videoTrack.putInt("height",format.height == Format.NO_VALUE ? 0 : format.height); videoTrack.putInt("bitrate", format.bitrate == Format.NO_VALUE ? 0 : format.bitrate); videoTrack.putString("codecs", format.codecs != null ? format.codecs : ""); - videoTrack.putString("trackId", - format.id == null ? String.valueOf(trackIndex) : format.id); + videoTrack.putString("trackId", format.id == null ? String.valueOf(trackIndex) : format.id); if (isFormatSupported(format)) { videoTracks.pushMap(videoTrack); } @@ -1009,11 +1025,14 @@ class ReactExoplayerView extends FrameLayout implements return videoTracks; } - private WritableArray getVideoTrackInfoFromManifest() { + private WritableArray getVideoTrackInfoFromManifest(Timeline timeline) { + return this.getVideoTrackInfoFromManifest(timeline, 0); + } + + private WritableArray getVideoTrackInfoFromManifest(Timeline timelineRef, int retryCount) { ExecutorService es = Executors.newSingleThreadExecutor(); final DataSource dataSource = this.mediaDataSourceFactory.createDataSource(); final Uri sourceUri = this.srcUri; - final Timeline timelineRef = this.player.getCurrentTimeline(); final long startTime = this.contentStartTime * 1000 - 100; // s -> ms with 100ms offset Future result = es.submit(new Callable() { @@ -1064,7 +1083,10 @@ class ReactExoplayerView extends FrameLayout implements }); try { - WritableArray results = result.get(); + WritableArray results = result.get(3000, TimeUnit.MILLISECONDS); + if (results == null && retryCount < 1) { + return this.getVideoTrackInfoFromManifest(timelineRef, ++retryCount); + } es.shutdown(); return results; } catch (Exception e) {} From ccd9894910e01ccd59d4bad8384843f27db0e057 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 5 Apr 2022 12:34:41 -0700 Subject: [PATCH 059/194] Fix iOS 15.4 race condition --- ios/Video/RCTVideo.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index a757c08a..cecc5fb3 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -1064,8 +1064,8 @@ static int const RCTVideoUnset = -1; [self setSelectedTextTrack:_selectedTextTrack]; [self setResizeMode:_resizeMode]; [self setRepeat:_repeat]; - [self setPaused:_paused]; [self setControls:_controls]; + [self setPaused:_paused]; [self setAllowsExternalPlayback:_allowsExternalPlayback]; } From 0e3de8497278b03c0d1581fc1e433e82f939cc0c Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 5 Apr 2022 12:45:53 -0700 Subject: [PATCH 060/194] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7f5943..2b500675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ### Version 5.2.1 -- Add Google's maven repository to avoid build error [#2552] (https://github.com/react-native-video/react-native-video/pull/2552) +- Add Google's maven repository to avoid build error [#2552] (https://github.com/react-native-video/react-native-video/pull/2552) +- Fix iOS 15.4 HLS playback race condition [#2633](#https://github.com/react-native-video/react-native-video/pull/2633) ### Version 5.2.0 From f78c623df47e3d1a37a74088b2ef5016e1db6b52 Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Thu, 7 Apr 2022 19:36:50 +0300 Subject: [PATCH 061/194] VEX-6779: React Native Video buildDrmSessionManager crash followup (#24) Fix infinite loading on some devices caused by earlier fix to drm session manager crash --- .../exoplayer/ReactExoplayerView.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 42974360..d8ab3ac2 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -510,6 +510,7 @@ class ReactExoplayerView extends FrameLayout implements private void initializePlayer() { ReactExoplayerView self = this; + Activity activity = themedReactContext.getCurrentActivity(); // This ensures all props have been settled, to avoid async racing conditions. new Handler().postDelayed(new Runnable() { @Override @@ -519,7 +520,6 @@ class ReactExoplayerView extends FrameLayout implements // Initialize core configuration and listeners initializePlayerCore(self); } - if (playerNeedsSource && srcUri != null) { exoPlayerView.invalidateAspectRatio(); // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread @@ -529,14 +529,16 @@ class ReactExoplayerView extends FrameLayout implements public void run() { // DRM initialization must run on a different thread DrmSessionManager drmSessionManager = initializePlayerDrm(self); - if (drmSessionManager == null) { + if (drmSessionManager == null && self.drmUUID != null) { // Failed to intialize DRM session manager - cannot continue + Log.e("ExoPlayer Exception", "Failed to initialize DRM Session Manager Framework!"); + eventEmitter.error("Failed to initialize DRM Session Manager Framework!", new Exception("DRM Session Manager Framework failure!"), "3003"); return; } + // Initialize handler to run on the main thread - new Handler(Looper.getMainLooper()).post(new Runnable () { - @Override - public void run () { + activity.runOnUiThread(new Runnable() { + public void run() { // Source initialization must run on the main thread initializePlayerSource(self, drmSessionManager); } @@ -595,6 +597,7 @@ class ReactExoplayerView extends FrameLayout implements PlaybackParameters params = new PlaybackParameters(rate, 1f); player.setPlaybackParameters(params); + } private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { @@ -1283,6 +1286,14 @@ class ReactExoplayerView extends FrameLayout implements } else if(cause instanceof MediaDrmCallbackException) { errorCode = "3005"; errorString = getResources().getString(R.string.unrecognized_media_format); + if (!hasDrmFailed) { + // When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time + hasDrmFailed = true; + playerNeedsSource = true; + updateResumePosition(); + initializePlayer(); + return; + } } else { errorCode = "2021"; errorString = getResources().getString(R.string.unrecognized_media_format); From 6da215ccadbbdf5e3c4df9855c9ed0d012c34804 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 19 Apr 2022 00:40:15 -0700 Subject: [PATCH 062/194] Create stale.yml --- .github/stale.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..17e71884 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,19 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. If you are having a similar problem, please open a + new issue and reference this one instead of commenting on a stale or closed + issue. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false From 03f77495fc7ea3cb1984fae9a749adf063bfe462 Mon Sep 17 00:00:00 2001 From: Gabriel Rivero Date: Tue, 19 Apr 2022 12:12:47 -0400 Subject: [PATCH 063/194] VEX-6350: add onPlaybackStateChanged prop (#25) * Adds the new prop onPlaybackStateChanged --- README.md | 19 +++++++++++++++++++ Video.js | 8 ++++++++ .../exoplayer/ReactExoplayerView.java | 5 +++++ .../exoplayer/VideoEventEmitter.java | 12 +++++++++++- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2881926f..80577e2a 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,7 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) * [onLoad](#onload) * [onLoadStart](#onloadstart) +* [onPlaybackStateChanged]($onPlaybackStateChanged) * [onReadyForDisplay](#onreadyfordisplay) * [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) * [onPlaybackRateChange](#onplaybackratechange) @@ -1110,6 +1111,24 @@ Example: Platforms: all +#### onPlaybackStateChanged +Callback function that is called when the playback state changes. + +Payload: + +Property | Description +--- | --- +isPlaying | boolean | Boolean indicating if the media is playing or not + +Example: +``` +{ + isPlaying: true, +} +``` + +Platforms: Android ExoPlayer + #### onReadyForDisplay Callback function that is called when the first video frame is ready for display. This is when the poster is removed. diff --git a/Video.js b/Video.js index db88752f..13d141f0 100644 --- a/Video.js +++ b/Video.js @@ -99,6 +99,12 @@ export default class Video extends Component { } }; + _onPlaybackStateChanged = (event) => { + if (this.props.onPlaybackStateChanged) { + this.props.onPlaybackStateChanged(event.nativeEvent); + } + }; + _onLoad = (event) => { // Need to hide poster here for windows as onReadyForDisplay is not implemented if (Platform.OS === 'windows') { @@ -311,6 +317,7 @@ export default class Video extends Component { requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, }, onVideoLoadStart: this._onLoadStart, + onVideoPlaybackStateChanged: this._onPlaybackStateChanged, onVideoLoad: this._onLoad, onVideoError: this._onError, onVideoProgress: this._onProgress, @@ -491,6 +498,7 @@ Video.propTypes = { useSecureView: PropTypes.bool, hideShutterView: PropTypes.bool, onLoadStart: PropTypes.func, + onPlaybackStateChanged: PropTypes.func, onLoad: PropTypes.func, onBuffer: PropTypes.func, onError: PropTypes.func, diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index d8ab3ac2..5a818e32 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1243,6 +1243,11 @@ class ReactExoplayerView extends FrameLayout implements eventEmitter.playbackRateChange(params.speed); } + @Override + public void onIsPlayingChanged(boolean isPlaying) { + eventEmitter.playbackStateChanged(isPlaying); + } + @Override public void onPlayerError(ExoPlaybackException e) { String errorString = "ExoPlaybackException type : " + e.type; diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index 266d2538..5aeb09ce 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -44,6 +44,7 @@ class VideoEventEmitter { 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_PLAYBACK_STATE_CHANGED = "onVideoPlaybackStateChanged"; private static final String EVENT_BUFFER_PROGRESS = "onVideoBufferProgress"; private static final String EVENT_IDLE = "onVideoIdle"; private static final String EVENT_TIMED_METADATA = "onTimedMetadata"; @@ -66,6 +67,7 @@ class VideoEventEmitter { EVENT_RESUME, EVENT_READY, EVENT_BUFFER, + EVENT_PLAYBACK_STATE_CHANGED, EVENT_BUFFER_PROGRESS, EVENT_IDLE, EVENT_TIMED_METADATA, @@ -91,6 +93,7 @@ class VideoEventEmitter { EVENT_RESUME, EVENT_READY, EVENT_BUFFER, + EVENT_PLAYBACK_STATE_CHANGED, EVENT_BUFFER_PROGRESS, EVENT_IDLE, EVENT_TIMED_METADATA, @@ -137,8 +140,9 @@ class VideoEventEmitter { private static final String EVENT_PROP_TIMED_METADATA = "metadata"; - private static final String EVENT_PROP_BITRATE = "bitrate"; + private static final String EVENT_PROP_BITRATE = "bitrate"; + private static final String EVENT_PROP_IS_PLAYING = "isPlaying"; void setViewId(int viewId) { this.viewId = viewId; @@ -215,6 +219,12 @@ class VideoEventEmitter { receiveEvent(EVENT_BUFFER, map); } + void playbackStateChanged(boolean isPlaying) { + WritableMap map = Arguments.createMap(); + map.putBoolean(EVENT_PROP_IS_PLAYING, isPlaying); + receiveEvent(EVENT_PLAYBACK_STATE_CHANGED, map); + } + void bufferProgress(double start, double end) { WritableMap map = Arguments.createMap(); map.putDouble(EVENT_PROP_BUFFER_START, start); From 9ba249507b4196a971231c60b90b930868aeda08 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 19 Apr 2022 10:00:41 -0700 Subject: [PATCH 064/194] Update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b500675..db0b4f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ ## Changelog -### Version 5.2.1 +### Version 6.0.0 - Add Google's maven repository to avoid build error [#2552] (https://github.com/react-native-video/react-native-video/pull/2552) -- Fix iOS 15.4 HLS playback race condition [#2633](#https://github.com/react-native-video/react-native-video/pull/2633) +- Fix iOS 15.4 HLS playback race condition [#2633](https://github.com/react-native-video/react-native-video/pull/2633) +- Add fullscreen support on Android [#2073](https://github.com/react-native-video/react-native-video/pull/2073) ### Version 5.2.0 From ba53bb743de6ce5d9b77d8782282aed173e09bd0 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 19 Apr 2022 10:01:44 -0700 Subject: [PATCH 065/194] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db0b4f9f..636d4870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Changelog -### Version 6.0.0 +### Version 6.0.0-alpha1 - Add Google's maven repository to avoid build error [#2552] (https://github.com/react-native-video/react-native-video/pull/2552) - Fix iOS 15.4 HLS playback race condition [#2633](https://github.com/react-native-video/react-native-video/pull/2633) From 90810fdbf58460963ece70e82db6429b98a9f1e4 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 19 Apr 2022 15:36:02 -0700 Subject: [PATCH 066/194] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c01bf0..dd544c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add Google's maven repository to avoid build error [#2552] (https://github.com/react-native-video/react-native-video/pull/2552) - Fix iOS 15.4 HLS playback race condition [#2633](https://github.com/react-native-video/react-native-video/pull/2633) - Add fullscreen support on Android [#2073](https://github.com/react-native-video/react-native-video/pull/2073) +- Fix app crash from NPE in Exoplayer error handler [#2575](https://github.com/react-native-video/react-native-video/pull/2575) ### Version 5.2.0 From 54ca713bde505288ae662532402a67cf362bb9ec Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 19 Apr 2022 15:40:10 -0700 Subject: [PATCH 067/194] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd544c55..1b445790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fix iOS 15.4 HLS playback race condition [#2633](https://github.com/react-native-video/react-native-video/pull/2633) - Add fullscreen support on Android [#2073](https://github.com/react-native-video/react-native-video/pull/2073) - Fix app crash from NPE in Exoplayer error handler [#2575](https://github.com/react-native-video/react-native-video/pull/2575) +- Fix default closed captioning behavior for Android ExoPlayer [#2181](https://github.com/react-native-video/react-native-video/pull/2181) ### Version 5.2.0 @@ -18,7 +19,6 @@ ### Version 5.1.0-alpha9 - Add ARM64 support for windows [#2137](https://github.com/react-native-community/react-native-video/pull/2137) -- Fix default closed captioning behavior for Android ExoPlayer [#2181](https://github.com/react-native-video/react-native-video/pull/2181) - Fix deprecated API bug for windows [#2119](https://github.com/react-native-video/react-native-video/pull/2119) - Added `rate` property and autolinking support for windows [#2206](https://github.com/react-native-video/react-native-video/pull/2206) From 1000788764ad403ebe5382a029bf3ff993b1a9c7 Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Tue, 19 Apr 2022 16:01:44 -0700 Subject: [PATCH 068/194] Update stale.yml --- .github/stale.yml | 53 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 17e71884..0262e33a 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,19 +1,60 @@ -# Number of days of inactivity before an issue becomes stale +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 7 -# Issues with these labels will never be considered stale + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - pinned - security -# Label to use when marking an issue as stale + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: true + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: true + +# Label to use when marking as stale staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable + +# Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. If you are having a similar problem, please open a new issue and reference this one instead of commenting on a stale or closed issue. -# Comment to post when closing a stale issue. Set to `false` to disable + +# Comment to post when removing the stale label. +unmarkComment: false + +# Comment to post when closing a stale Issue or Pull Request. closeComment: false + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 50 + +# Limit to only `issues` or `pulls` +only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed From 6815044013efeeed5fc73e818d658e2ed4a5bd9d Mon Sep 17 00:00:00 2001 From: Armands Malejev Date: Wed, 20 Apr 2022 03:05:32 +0300 Subject: [PATCH 069/194] VEX-6099: Android: Progress knob jumps backward after tapping Skip Forwards multiple times (#26) Move to the new way of detecting seek end and remove onSeekProcessed which was deprecated. --- .../exoplayer/ReactExoplayerView.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 5a818e32..b86deacb 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -1206,6 +1206,19 @@ class ReactExoplayerView extends FrameLayout implements && player.getRepeatMode() == Player.REPEAT_MODE_ONE) { eventEmitter.end(); } + + } + + @Override + public void onPlaybackStateChanged(int playbackState) { + if (playbackState == Player.STATE_READY && seekTime != C.TIME_UNSET) { + eventEmitter.seek(player.getCurrentPosition(), seekTime); + seekTime = C.TIME_UNSET; + if (isUsingContentResolution) { + // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period + setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); + } + } } @Override @@ -1213,16 +1226,6 @@ class ReactExoplayerView extends FrameLayout implements // Do nothing. } - @Override - public void onSeekProcessed() { - eventEmitter.seek(player.getCurrentPosition(), seekTime); - seekTime = C.TIME_UNSET; - if (isUsingContentResolution) { - // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period - setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue); - } - } - @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { // Do nothing. From 1aa4e36b854765074666c66dcd034bfb53c374cd Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Wed, 20 Apr 2022 00:54:39 -0700 Subject: [PATCH 070/194] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b445790..01e65fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add fullscreen support on Android [#2073](https://github.com/react-native-video/react-native-video/pull/2073) - Fix app crash from NPE in Exoplayer error handler [#2575](https://github.com/react-native-video/react-native-video/pull/2575) - Fix default closed captioning behavior for Android ExoPlayer [#2181](https://github.com/react-native-video/react-native-video/pull/2181) +- Disable pipController init if pictureInPicture is false [#2645](https://github.com/react-native-video/react-native-video/pull/2645) ### Version 5.2.0 From 0883f0a9be7f3e42d730b0c82fc5371006d2aa6a Mon Sep 17 00:00:00 2001 From: Eran Hammer Date: Wed, 20 Apr 2022 01:16:40 -0700 Subject: [PATCH 071/194] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e65fdb..ec4fec45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Fix app crash from NPE in Exoplayer error handler [#2575](https://github.com/react-native-video/react-native-video/pull/2575) - Fix default closed captioning behavior for Android ExoPlayer [#2181](https://github.com/react-native-video/react-native-video/pull/2181) - Disable pipController init if pictureInPicture is false [#2645](https://github.com/react-native-video/react-native-video/pull/2645) +- Make sure modifiers are applied before playing [#2395](https://github.com/react-native-video/react-native-video/pull/2395) ### Version 5.2.0 From c027879bd7714f5f9a22481e1f0044319ac89698 Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Thu, 9 Jul 2020 19:52:24 -0700 Subject: [PATCH 072/194] Remove duplicate LICENSE --- dom/LICENSE.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 dom/LICENSE.md diff --git a/dom/LICENSE.md b/dom/LICENSE.md deleted file mode 100644 index aa185c82..00000000 --- a/dom/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -MIT License - -Copyright (c) 2018 Vincent Riemer - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 6c86713aff1b4686f1a7418c6cda33324775ea9d Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Thu, 9 Jul 2020 19:57:34 -0700 Subject: [PATCH 073/194] Fix react-native peer deps --- package.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cb8d146e..e145a3b0 100644 --- a/package.json +++ b/package.json @@ -28,13 +28,12 @@ "url": "git@github.com:react-native-community/react-native-video.git" }, "devDependencies": { - "@react-native-community/eslint-config": "0.0.7", - "babel-eslint": "10.0.3", - "eslint": "6.8.0", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "@react-native-community/eslint-config": "^0.0.5", + "eslint": "^6.5.1", + "react": "16.9.0", + "react-dom": "16.9.0", "react-hot-loader": "^4.12.19", - "react-native": "^0.61.5", + "react-native": "0.61.5", "react-native-windows": "^0.61.0-0" }, "dependencies": { From ad98bfa8784411861ed28514acdf44f408233b15 Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Thu, 9 Jul 2020 19:53:34 -0700 Subject: [PATCH 074/194] Fix ESLint setup and errors --- .eslintignore | 1 + dom/RCTVideo.js | 96 +++++++++++++++++++++--------------------- dom/RCTVideoEvent.js | 8 ++-- dom/RCTVideoManager.js | 42 +++++++++--------- dom/index.js | 2 +- package.json | 3 +- 6 files changed, 78 insertions(+), 74 deletions(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..d838da98 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +examples/ diff --git a/dom/RCTVideo.js b/dom/RCTVideo.js index 32128973..77e9dc88 100644 --- a/dom/RCTVideo.js +++ b/dom/RCTVideo.js @@ -1,11 +1,13 @@ // @flow -import { RCTEvent, RCTView, type RCTBridge } from "react-native-dom"; -import shaka from "shaka-player"; +/* eslint-env browser */ -import resizeModes from "./resizeModes"; -import type { VideoSource } from "./types"; -import RCTVideoEvent from "./RCTVideoEvent"; +import { RCTView, type RCTBridge } from 'react-native-dom'; +import shaka from 'shaka-player'; + +import resizeModes from './resizeModes'; +import type { VideoSource } from './types'; +import RCTVideoEvent from './RCTVideoEvent'; class RCTVideo extends RCTView { playPromise: Promise = Promise.resolve(); @@ -24,7 +26,7 @@ class RCTVideo extends RCTView { constructor(bridge: RCTBridge) { super(bridge); - this.eventDispatcher = bridge.getModuleByName("EventDispatcher"); + this.eventDispatcher = bridge.getModuleByName('EventDispatcher'); shaka.polyfill.installAll(); @@ -35,12 +37,12 @@ class RCTVideo extends RCTView { this.onProgress = this.onProgress.bind(this); this.videoElement = this.initializeVideoElement(); - this.videoElement.addEventListener("ended", this.onEnd); - this.videoElement.addEventListener("loadeddata", this.onLoad); - this.videoElement.addEventListener("canplay", this.onReadyForDisplay); - this.videoElement.addEventListener("loadstart", this.onLoadStart); - this.videoElement.addEventListener("pause", this.onPause); - this.videoElement.addEventListener("play", this.onPlay); + this.videoElement.addEventListener('ended', this.onEnd); + this.videoElement.addEventListener('loadeddata', this.onLoad); + this.videoElement.addEventListener('canplay', this.onReadyForDisplay); + this.videoElement.addEventListener('loadstart', this.onLoadStart); + this.videoElement.addEventListener('pause', this.onPause); + this.videoElement.addEventListener('play', this.onPlay); this.player = new shaka.Player(this.videoElement); this.muted = false; @@ -50,26 +52,26 @@ class RCTVideo extends RCTView { } detachFromView(view: UIView) { - this.videoElement.removeEventListener("ended", this.onEnd); - this.videoElement.removeEventListener("loadeddata", this.onLoad); - this.videoElement.removeEventListener("canplay", this.onReadyForDisplay); - this.videoElement.removeEventListener("loadstart", this.onLoadStart); - this.videoElement.removeEventListener("pause", this.onPause); - this.videoElement.removeEventListener("play", this.onPlay); + this.videoElement.removeEventListener('ended', this.onEnd); + this.videoElement.removeEventListener('loadeddata', this.onLoad); + this.videoElement.removeEventListener('canplay', this.onReadyForDisplay); + this.videoElement.removeEventListener('loadstart', this.onLoadStart); + this.videoElement.removeEventListener('pause', this.onPause); + this.videoElement.removeEventListener('play', this.onPlay); this.stopProgressTimer(); } initializeVideoElement() { - const elem = document.createElement("video"); + const elem = document.createElement('video'); Object.assign(elem.style, { - display: "block", - position: "absolute", - top: "0", - left: "0", - width: "100%", - height: "100%" + display: 'block', + position: 'absolute', + top: '0', + left: '0', + width: '100%', + height: '100%', }); return elem; @@ -81,7 +83,7 @@ class RCTVideo extends RCTView { set controls(value: boolean) { this.videoElement.controls = value; - this.videoElement.style.pointerEvents = value ? "auto" : ""; + this.videoElement.style.pointerEvents = value ? 'auto' : ''; } set id(value: string) { @@ -121,19 +123,19 @@ class RCTVideo extends RCTView { set resizeMode(value: number) { switch (value) { case resizeModes.ScaleNone: { - this.videoElement.style.objectFit = "none"; + this.videoElement.style.objectFit = 'none'; break; } case resizeModes.ScaleToFill: { - this.videoElement.style.objectFit = "fill"; + this.videoElement.style.objectFit = 'fill'; break; } case resizeModes.ScaleAspectFit: { - this.videoElement.style.objectFit = "contain"; + this.videoElement.style.objectFit = 'contain'; break; } case resizeModes.ScaleAspectFill: { - this.videoElement.style.objectFit = "cover"; + this.videoElement.style.objectFit = 'cover'; break; } } @@ -146,16 +148,16 @@ class RCTVideo extends RCTView { set source(value: VideoSource) { let uri = value.uri; - if (uri.startsWith("blob:")) { + if (uri.startsWith('blob:')) { let blob = this.bridge.blobManager.resolveURL(uri); - if (blob.type === "text/xml") { - blob = new Blob([blob], { type: "video/mp4" }); + if (blob.type === 'text/xml') { + blob = new Blob([blob], { type: 'video/mp4' }); } uri = URL.createObjectURL(blob); } if (!shaka.Player.isBrowserSupported()) { // primarily iOS WebKit - this.videoElement.setAttribute("src", uri); + this.videoElement.setAttribute('src', uri); if (!this._paused) { this.requestPlay(); } @@ -181,12 +183,12 @@ class RCTVideo extends RCTView { onEnd = () => { this.onProgress(); - this.sendEvent("topVideoEnd", null); - this.stopProgressTimer(); + this.sendEvent('topVideoEnd', null); + this.stopProgressTimer(); } onError = error => { - console.warn("topVideoError", error); + console.warn('topVideoError', error); } onLoad = () => { @@ -199,23 +201,23 @@ class RCTVideo extends RCTView { naturalSize: { width, height, - orientation: width >= height ? "landscape" : "portrait" - } + orientation: width >= height ? 'landscape' : 'portrait', + }, }; - this.sendEvent("topVideoLoad", payload); + this.sendEvent('topVideoLoad', payload); } onReadyForDisplay = () => { - this.sendEvent("onReadyForDisplay"); + this.sendEvent('onReadyForDisplay'); } onLoadStart = () => { const src = this.videoElement.currentSrc; const payload = { isNetwork: !src.match(/^https?:\/\/localhost/), // require is served from localhost - uri: this.videoElement.currentSrc + uri: this.videoElement.currentSrc, }; - this.sendEvent("topVideoLoadStart", payload); + this.sendEvent('topVideoLoadStart', payload); } onPause = () => { @@ -229,13 +231,13 @@ class RCTVideo extends RCTView { onProgress = () => { const payload = { currentTime: this.videoElement.currentTime, - seekableDuration: this.videoElement.duration + seekableDuration: this.videoElement.duration, }; - this.sendEvent("topVideoProgress", payload); + this.sendEvent('topVideoProgress', payload); } onRejectedAutoplay = () => { - this.sendEvent("topVideoRejectedAutoplay", null); + this.sendEvent('topVideoRejectedAutoplay', null); } requestPlay() { @@ -273,6 +275,6 @@ class RCTVideo extends RCTView { } } -customElements.define("rct-video", RCTVideo); +customElements.define('rct-video', RCTVideo); export default RCTVideo; diff --git a/dom/RCTVideoEvent.js b/dom/RCTVideoEvent.js index 699a88c4..4a772cf5 100644 --- a/dom/RCTVideoEvent.js +++ b/dom/RCTVideoEvent.js @@ -4,10 +4,10 @@ interface RCTEvent { viewTag: number; eventName: string; coalescingKey: number; - + canCoalesce(): boolean; coalesceWithEvent(event: RCTEvent): RCTEvent; - + moduleDotMethod(): string; arguments(): Array; } @@ -38,14 +38,14 @@ export default class RCTVideoEvent implements RCTEvent { } moduleDotMethod(): string { - return "RCTEventEmitter.receiveEvent"; + return 'RCTEventEmitter.receiveEvent'; } arguments(): Array { const args = [ this.viewTag, this.eventName, - this.data + this.data, ]; return args; } diff --git a/dom/RCTVideoManager.js b/dom/RCTVideoManager.js index 2eb5309b..efeb4ae0 100644 --- a/dom/RCTVideoManager.js +++ b/dom/RCTVideoManager.js @@ -1,14 +1,14 @@ // @flow -import { RCTViewManager } from "react-native-dom"; +import { RCTViewManager } from 'react-native-dom'; -import RCTVideo from "./RCTVideo"; -import resizeModes from "./resizeModes"; +import RCTVideo from './RCTVideo'; +import resizeModes from './resizeModes'; -import type { VideoSource } from "./types"; +import type { VideoSource } from './types'; class RCTVideoManager extends RCTViewManager { - static moduleName = "RCTVideoManager"; + static moduleName = 'RCTVideoManager'; view() { return new RCTVideo(this.bridge); @@ -17,22 +17,22 @@ class RCTVideoManager extends RCTViewManager { describeProps() { return super .describeProps() - .addBooleanProp("controls", this.setControls) - .addStringProp("id", this.setId) - .addBooleanProp("muted", this.setMuted) - .addBooleanProp("paused", this.setPaused) - .addNumberProp("progressUpdateInterval", this.setProgressUpdateInterval) - .addBooleanProp("rate", this.setRate) - .addBooleanProp("repeat", this.setRepeat) - .addNumberProp("resizeMode", this.setResizeMode) - .addNumberProp("seek", this.setSeek) - .addObjectProp("src", this.setSource) - .addNumberProp("volume", this.setVolume) - .addDirectEvent("onVideoEnd") - .addDirectEvent("onVideoError") - .addDirectEvent("onVideoLoad") - .addDirectEvent("onVideoLoadStart") - .addDirectEvent("onVideoProgress"); + .addBooleanProp('controls', this.setControls) + .addStringProp('id', this.setId) + .addBooleanProp('muted', this.setMuted) + .addBooleanProp('paused', this.setPaused) + .addNumberProp('progressUpdateInterval', this.setProgressUpdateInterval) + .addBooleanProp('rate', this.setRate) + .addBooleanProp('repeat', this.setRepeat) + .addNumberProp('resizeMode', this.setResizeMode) + .addNumberProp('seek', this.setSeek) + .addObjectProp('src', this.setSource) + .addNumberProp('volume', this.setVolume) + .addDirectEvent('onVideoEnd') + .addDirectEvent('onVideoError') + .addDirectEvent('onVideoLoad') + .addDirectEvent('onVideoLoadStart') + .addDirectEvent('onVideoProgress'); } dismissFullscreenPlayer() { diff --git a/dom/index.js b/dom/index.js index d68d5bba..e097d5fb 100644 --- a/dom/index.js +++ b/dom/index.js @@ -1,3 +1,3 @@ // @flow -module.exports = require("./RCTVideoManager"); +module.exports = require('./RCTVideoManager'); diff --git a/package.json b/package.json index e145a3b0..29507d8b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-dom": "16.9.0", "react-hot-loader": "^4.12.19", "react-native": "0.61.5", + "react-native-dom": "^0.5.0", "react-native-windows": "^0.61.0-0" }, "dependencies": { @@ -42,7 +43,7 @@ "shaka-player": "^2.5.9" }, "scripts": { - "lint": "yarn eslint *.js" + "lint": "eslint ." }, "files": [ "android-exoplayer", From 18f1c0d7605dac5d944e207119c2ee38e2c8018e Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Thu, 9 Jul 2020 20:38:54 -0700 Subject: [PATCH 075/194] Enable basic GitHub Actions CI --- .github/workflows/ci.yml | 10 ++++++++++ README.md | 4 ++++ 2 files changed, 14 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..efe65552 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,10 @@ +name: ci +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + - run: yarn --no-lockfile + - run: yarn lint diff --git a/README.md b/README.md index 9c396a2f..04f56375 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## react-native-video +[![ci][4]][5] + A `