From 1cb5070e3edd145c55939d889586abb8d2fb8b55 Mon Sep 17 00:00:00 2001 From: Andy Chentsov Date: Thu, 12 May 2022 20:05:43 +0300 Subject: [PATCH 01/55] Multi-window support (#318) * Multi-window support Part of the Android player is rewritten to support the multi-window mode. Now, when you turn on the multi-window mode and changing the size of the player window, the video and sound continues without breaks. The example application is updated - support for the multi-window is added to the AndroidManifest. * The VLCTextureView class is moved to a separate file. * Code Formatting * Added in the README how to set up multi-window mode in Android & v 7.1.3 * README update --- flutter_vlc_player/CHANGELOG.md | 4 + flutter_vlc_player/README.md | 19 ++ .../fluttervlcplayer/FlutterVlcPlayer.java | 166 +++++++------- .../FlutterVlcPlayerPlugin.java | 26 ++- .../fluttervlcplayer/VLCTextureView.java | 213 ++++++++++++++++++ .../example/android/app/build.gradle | 4 +- .../android/app/src/main/AndroidManifest.xml | 7 +- flutter_vlc_player/pubspec.yaml | 2 +- 8 files changed, 346 insertions(+), 95 deletions(-) create mode 100644 flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/VLCTextureView.java diff --git a/flutter_vlc_player/CHANGELOG.md b/flutter_vlc_player/CHANGELOG.md index 610c2575..fe9b80f2 100644 --- a/flutter_vlc_player/CHANGELOG.md +++ b/flutter_vlc_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.1.3 +* Added support for multi-window mode in Android. +Credits to Andy Chentsov (https://github.com/andyduke). + ## 7.1.2 * Add Hybrid composition support for Android. diff --git a/flutter_vlc_player/README.md b/flutter_vlc_player/README.md index 86c442d6..a58dc5a3 100644 --- a/flutter_vlc_player/README.md +++ b/flutter_vlc_player/README.md @@ -100,6 +100,25 @@ android { ```proguard -keep class org.videolan.libvlc.** { *; } ``` +
+ +#### Android multi-window support + +To enable multi-window support in your Android application, you need to make changes to `AndroidManifest.xml`, add the `android:resizeableActivity` key for the main activity, as well as the `android.allow_multiple_resumed_activities` metadata for application: +```xml + + + + ... + + ... + + + +```
diff --git a/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayer.java b/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayer.java index 60ecaaa3..b05c0c42 100644 --- a/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayer.java +++ b/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayer.java @@ -6,18 +6,16 @@ import org.videolan.libvlc.RendererDiscoverer; import org.videolan.libvlc.RendererItem; import org.videolan.libvlc.interfaces.IMedia; +import org.videolan.libvlc.interfaces.IVLCVout; import android.content.Context; import android.graphics.Bitmap; import android.graphics.SurfaceTexture; import android.net.Uri; -import android.os.Handler; -import android.os.Looper; import android.util.Base64; import android.util.Log; import android.view.Surface; import android.view.SurfaceView; -import android.view.TextureView; import android.view.View; import io.flutter.plugin.common.BinaryMessenger; @@ -39,7 +37,7 @@ final class FlutterVlcPlayer implements PlatformView { private final boolean debug = false; // private final Context context; - private final TextureView textureView; + private final VLCTextureView textureView; private final TextureRegistry.SurfaceTextureEntry textureEntry; // private final QueuingEventSink mediaEventSink = new QueuingEventSink(); @@ -66,6 +64,7 @@ public void dispose() { if (isDisposed) return; // + textureView.dispose(); textureEntry.release(); mediaEventChannel.setStreamHandler(null); rendererEventChannel.setStreamHandler(null); @@ -115,7 +114,7 @@ public void onCancel(Object o) { }); // textureEntry = textureRegistry.createSurfaceTexture(); - textureView = new TextureView(context); + textureView = new VLCTextureView(context); textureView.setSurfaceTexture(textureEntry.surfaceTexture()); textureView.forceLayout(); textureView.setFitsSystemWindows(true); @@ -134,81 +133,11 @@ public void initialize(List options) { private void setupVlcMediaPlayer() { - // method 1 - textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { - - boolean wasPlaying = false; - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - log("onSurfaceTextureAvailable"); - - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (mediaPlayer == null) - return; - mediaPlayer.getVLCVout().setWindowSize(width, height); - mediaPlayer.getVLCVout().setVideoSurface(surface); - if (!mediaPlayer.getVLCVout().areViewsAttached()) - mediaPlayer.getVLCVout().attachViews(); - mediaPlayer.setVideoTrackEnabled(true); - if (wasPlaying) - mediaPlayer.play(); - wasPlaying = false; - }, 100L); - - } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - if (mediaPlayer != null) - mediaPlayer.getVLCVout().setWindowSize(width, height); - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - log("onSurfaceTextureDestroyed"); - - if (mediaPlayer != null) { - wasPlaying = mediaPlayer.isPlaying(); - mediaPlayer.pause(); - mediaPlayer.setVideoTrackEnabled(false); - mediaPlayer.getVLCVout().detachViews(); - } - return false; //do not return true if you reuse it. - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - } - - }); - -// method 2 - textureView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { - @Override - public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { - log("onLayoutChange"); - // - if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { - mediaPlayer.pause(); - mediaPlayer.setVideoTrackEnabled(false); - mediaPlayer.getVLCVout().detachViews(); - mediaPlayer.getVLCVout().setWindowSize(view.getWidth(), view.getHeight()); - mediaPlayer.getVLCVout().setVideoView((TextureView) view); - mediaPlayer.getVLCVout().attachViews(); - mediaPlayer.setVideoTrackEnabled(true); - // hacky way to prevent video pixeling, it might be larger than buffer size - long tmpTime = mediaPlayer.getTime() - 500; - if (tmpTime > 0) - mediaPlayer.setTime(tmpTime); - mediaPlayer.play(); - } - } - }); // mediaPlayer.getVLCVout().setWindowSize(textureView.getWidth(), textureView.getHeight()); mediaPlayer.getVLCVout().setVideoSurface(textureView.getSurfaceTexture()); - mediaPlayer.getVLCVout().attachViews(); + textureView.setTextureEntry(textureEntry); + textureView.setMediaPlayer(mediaPlayer); mediaPlayer.setVideoTrackEnabled(true); // mediaPlayer.setEventListener( @@ -313,26 +242,36 @@ public void onEvent(MediaPlayer.Event event) { } void play() { - mediaPlayer.play(); + if (mediaPlayer != null) { + mediaPlayer.play(); + } } void pause() { - mediaPlayer.pause(); + if (mediaPlayer != null) { + mediaPlayer.pause(); + } } void stop() { - mediaPlayer.stop(); + if (mediaPlayer != null) { + mediaPlayer.stop(); + } } boolean isPlaying() { + if (mediaPlayer == null) return false; return mediaPlayer.isPlaying(); } boolean isSeekable() { + if (mediaPlayer == null) return false; return mediaPlayer.isSeekable(); } void setStreamUrl(String url, boolean isAssetUrl, boolean autoPlay, long hwAcc) { + if (mediaPlayer == null) return; + try { mediaPlayer.stop(); // @@ -379,39 +318,57 @@ void setLooping(boolean value) { } void setVolume(long value) { + if (mediaPlayer == null) return; + long bracketedValue = Math.max(0, Math.min(100, value)); mediaPlayer.setVolume((int) bracketedValue); } int getVolume() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getVolume(); } void setPlaybackSpeed(double value) { + if (mediaPlayer == null) return; + mediaPlayer.setRate((float) value); } float getPlaybackSpeed() { + if (mediaPlayer == null) return -1.0f; + return mediaPlayer.getRate(); } void seekTo(int location) { + if (mediaPlayer == null) return; + mediaPlayer.setTime(location); } long getPosition() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getTime(); } long getDuration() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getLength(); } int getSpuTracksCount() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getSpuTracksCount(); } HashMap getSpuTracks() { + if (mediaPlayer == null) return new HashMap(); + MediaPlayer.TrackDescription[] spuTracks = mediaPlayer.getSpuTracks(); HashMap subtitles = new HashMap<>(); if (spuTracks != null) @@ -423,30 +380,44 @@ HashMap getSpuTracks() { } void setSpuTrack(int index) { + if (mediaPlayer == null) return; + mediaPlayer.setSpuTrack(index); } int getSpuTrack() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getSpuTrack(); } void setSpuDelay(long delay) { + if (mediaPlayer == null) return; + mediaPlayer.setSpuDelay(delay); } long getSpuDelay() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getSpuDelay(); } void addSubtitleTrack(String url, boolean isSelected) { + if (mediaPlayer == null) return; + mediaPlayer.addSlave(Media.Slave.Type.Subtitle, Uri.parse(url), isSelected); } int getAudioTracksCount() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getAudioTracksCount(); } HashMap getAudioTracks() { + if (mediaPlayer == null) return new HashMap(); + MediaPlayer.TrackDescription[] audioTracks = mediaPlayer.getAudioTracks(); HashMap audios = new HashMap<>(); if (audioTracks != null) @@ -458,30 +429,44 @@ HashMap getAudioTracks() { } void setAudioTrack(int index) { + if (mediaPlayer == null) return; + mediaPlayer.setAudioTrack(index); } int getAudioTrack() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getAudioTrack(); } void setAudioDelay(long delay) { + if (mediaPlayer == null) return; + mediaPlayer.setAudioDelay(delay); } long getAudioDelay() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getAudioDelay(); } void addAudioTrack(String url, boolean isSelected) { + if (mediaPlayer == null) return; + mediaPlayer.addSlave(Media.Slave.Type.Audio, Uri.parse(url), isSelected); } int getVideoTracksCount() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getVideoTracksCount(); } HashMap getVideoTracks() { + if (mediaPlayer == null) return new HashMap(); + MediaPlayer.TrackDescription[] videoTracks = mediaPlayer.getVideoTracks(); HashMap videos = new HashMap<>(); if (videoTracks != null) @@ -493,30 +478,43 @@ HashMap getVideoTracks() { } void setVideoTrack(int index) { + if (mediaPlayer == null) return; + mediaPlayer.setVideoTrack(index); } int getVideoTrack() { + if (mediaPlayer == null) return -1; + return mediaPlayer.getVideoTrack(); } void setVideoScale(float scale) { + if (mediaPlayer == null) return; + mediaPlayer.setScale(scale); } float getVideoScale() { + if (mediaPlayer == null) return -1.0f; + return mediaPlayer.getScale(); } void setVideoAspectRatio(String aspectRatio) { + if (mediaPlayer == null) return; + mediaPlayer.setAspectRatio(aspectRatio); } String getVideoAspectRatio() { + if (mediaPlayer == null) return ""; + return mediaPlayer.getAspectRatio(); } void startRendererScanning(String rendererService) { + if (libVLC == null) return; // // android -> chromecast -> "microdns" @@ -568,6 +566,8 @@ public void onEvent(RendererDiscoverer.Event event) { } void stopRendererScanning() { + if (mediaPlayer == null) return; + if (isDisposed) return; // @@ -587,6 +587,8 @@ void stopRendererScanning() { } ArrayList getAvailableRendererServices() { + if (libVLC == null) return new ArrayList(); + RendererDiscoverer.Description[] renderers = RendererDiscoverer.list(libVLC); ArrayList availableRendererServices = new ArrayList<>(); for (RendererDiscoverer.Description renderer : renderers) { @@ -605,6 +607,8 @@ HashMap getRendererDevices() { } void castToRenderer(String rendererDevice) { + if (mediaPlayer == null) return; + if (isDisposed) { return; } @@ -627,6 +631,8 @@ void castToRenderer(String rendererDevice) { } String getSnapshot() { + if (textureView == null) return ""; + Bitmap bitmap = textureView.getBitmap(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); diff --git a/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayerPlugin.java b/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayerPlugin.java index e2f7e944..acea8b90 100644 --- a/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayerPlugin.java +++ b/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/FlutterVlcPlayerPlugin.java @@ -48,16 +48,8 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { flutterPluginBinding = binding; - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - flutterPluginBinding = null; - } - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + // if (flutterVlcPlayerFactory == null) { final FlutterInjector injector = FlutterInjector.instance(); // @@ -79,20 +71,30 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { startListening(); } + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + stopListening(); + // + + flutterPluginBinding = null; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + } + @Override public void onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity(); } @RequiresApi(api = Build.VERSION_CODES.N) @Override public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - onAttachedToActivity(binding); } @Override public void onDetachedFromActivity() { - stopListening(); } // extra methods diff --git a/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/VLCTextureView.java b/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/VLCTextureView.java new file mode 100644 index 00000000..520aa011 --- /dev/null +++ b/flutter_vlc_player/android/src/main/java/software/solid/fluttervlcplayer/VLCTextureView.java @@ -0,0 +1,213 @@ +package software.solid.fluttervlcplayer; + +import org.videolan.libvlc.MediaPlayer; +import org.videolan.libvlc.interfaces.IVLCVout; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.view.TextureView; +import android.view.View; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import io.flutter.view.TextureRegistry; + +public class VLCTextureView extends TextureView implements TextureView.SurfaceTextureListener, View.OnLayoutChangeListener, IVLCVout.OnNewVideoLayoutListener { + + private MediaPlayer mMediaPlayer = null; + private TextureRegistry.SurfaceTextureEntry mTextureEntry = null; + protected Context mContext; + private SurfaceTexture mSurfaceTexture = null; + private boolean wasPlaying = false; + + private Handler mHandler; + private Runnable mLayoutChangeRunnable = null; + + public VLCTextureView(final Context context) { + super(context); + mContext = context; + initVideoView(); + } + + public VLCTextureView(final Context context, final AttributeSet attrs) { + super(context, attrs); + mContext = context; + initVideoView(); + } + + public VLCTextureView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mContext = context; + initVideoView(); + } + + public void dispose() { + setSurfaceTextureListener(null); + removeOnLayoutChangeListener(this); + + if (mLayoutChangeRunnable != null) { + mHandler.removeCallbacks(mLayoutChangeRunnable); + mLayoutChangeRunnable = null; + } + + if (mSurfaceTexture != null) { + if (!mSurfaceTexture.isReleased()) { + mSurfaceTexture.release(); + } + mSurfaceTexture = null; + } + mTextureEntry = null; + mMediaPlayer = null; + mContext = null; + } + + private void initVideoView() { + mHandler = new Handler(Looper.getMainLooper()); + + setFocusable(false); + setSurfaceTextureListener(this); + addOnLayoutChangeListener(this); + } + + public void setMediaPlayer(MediaPlayer mediaPlayer) { + if (mediaPlayer == null) { + mMediaPlayer.getVLCVout().detachViews(); + } + + mMediaPlayer = mediaPlayer; + + if (mMediaPlayer != null) { + mMediaPlayer.getVLCVout().attachViews(this); + } + } + + public void setTextureEntry(TextureRegistry.SurfaceTextureEntry textureEntry) { + this.mTextureEntry = textureEntry; + this.updateSurfaceTexture(); + } + + private void updateSurfaceTexture() { + if (this.mTextureEntry != null) { + final SurfaceTexture texture = this.mTextureEntry.surfaceTexture(); + if (!texture.isReleased() && (getSurfaceTexture() != texture)) { + setSurfaceTexture(texture); + } + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + if (mSurfaceTexture == null || mSurfaceTexture.isReleased()) { + mSurfaceTexture = surface; + + if (mMediaPlayer != null) { + mMediaPlayer.getVLCVout().setWindowSize(width, height); + if (!mMediaPlayer.getVLCVout().areViewsAttached()) { + mMediaPlayer.getVLCVout().setVideoSurface(mSurfaceTexture); + if (!mMediaPlayer.getVLCVout().areViewsAttached()) { + mMediaPlayer.getVLCVout().attachViews(this); + } + mMediaPlayer.setVideoTrackEnabled(true); + if (wasPlaying) { + mMediaPlayer.play(); + } + } + } + + wasPlaying = false; + + } else { + if (getSurfaceTexture() != mSurfaceTexture) { + setSurfaceTexture(mSurfaceTexture); + } + } + + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + setSize(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (mMediaPlayer != null) { + wasPlaying = mMediaPlayer.isPlaying(); + } + + if (mSurfaceTexture != surface) { + if (mSurfaceTexture != null) { + if (!mSurfaceTexture.isReleased()) { + mSurfaceTexture.release(); + } + } + mSurfaceTexture = surface; + } + + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + + @Override + public void onNewVideoLayout(IVLCVout vlcVout, int width, int height, int visibleWidth, int visibleHeight, int sarNum, int sarDen) { + if (width * height == 0) return; + + setSize(width, height); + } + + @Override + public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { + updateLayoutSize(view); + } + } + + public void updateLayoutSize(View view) { + if (mMediaPlayer != null) { + mMediaPlayer.getVLCVout().setWindowSize(view.getWidth(), view.getHeight()); + updateSurfaceTexture(); + } + } + + private void setSize(int width, int height) { + int mVideoWidth = 0; + int mVideoHeight = 0; + mVideoWidth = width; + mVideoHeight = height; + if (mVideoWidth * mVideoHeight <= 1) return; + + // Screen size + int w = this.getWidth(); + int h = this.getHeight(); + + // Size + if (w > h && w < h) { + int i = w; + w = h; + h = i; + } + + float videoAR = (float) mVideoWidth / (float) mVideoHeight; + float screenAR = (float) w / (float) h; + + if (screenAR < videoAR) { + h = (int) (w / videoAR); + } else { + w = (int) (h * videoAR); + } + + // Layout fit + ViewGroup.LayoutParams lp = this.getLayoutParams(); + lp.width = ViewGroup.LayoutParams.MATCH_PARENT; + lp.height = h; + this.setLayoutParams(lp); + this.invalidate(); + } + +} \ No newline at end of file diff --git a/flutter_vlc_player/example/android/app/build.gradle b/flutter_vlc_player/example/android/app/build.gradle index f862e07a..105d7fa2 100644 --- a/flutter_vlc_player/example/android/app/build.gradle +++ b/flutter_vlc_player/example/android/app/build.gradle @@ -33,7 +33,7 @@ android { defaultConfig { applicationId "software.solid.fluttervlcplayerexample" - minSdkVersion 17 + minSdkVersion 20 targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -44,6 +44,8 @@ android { release { // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug + minifyEnabled false + shrinkResources false } } } diff --git a/flutter_vlc_player/example/android/app/src/main/AndroidManifest.xml b/flutter_vlc_player/example/android/app/src/main/AndroidManifest.xml index e8ae7d35..b1838a22 100644 --- a/flutter_vlc_player/example/android/app/src/main/AndroidManifest.xml +++ b/flutter_vlc_player/example/android/app/src/main/AndroidManifest.xml @@ -23,8 +23,9 @@ android:name=".MainActivity" android:launchMode="singleTop" android:theme="@style/LaunchTheme" - android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" + android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode|smallestScreenSize" android:hardwareAccelerated="true" + android:resizeableActivity="true" android:windowSoftInputMode="adjustResize">