Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update VideoViewRenderer.cs #242

Closed
wants to merge 1 commit into from

Conversation

AndreasReitberger
Copy link

@AndreasReitberger AndreasReitberger commented Sep 21, 2021

As requested, I also made the changes here.
https://code.videolan.org/videolan/LibVLCSharp/-/merge_requests/20

Description of Change

Added an override for the "Dispose" method and set a private boolean to true if the object is disposing.
Then, if it's disposing, no upadate of the MediaPlayer element is happening for the disposing object.
This avoids the "View is null" exception on Android.

at Java.Interop.JniEnvironment+InstanceMethods.CallVoidMethod (Java.Interop.JniObjectReference instance, Java.Interop.JniMethodInfo method, Java.Interop.JniArgumentValue* args) [0x0006e] in <bd6bd528a8784b7caf03e9f25c9f0d7b>:0 
  at Java.Interop.JniPeerMembers+JniInstanceMethods.InvokeVirtualVoidMethod (System.String encodedMember, Java.Interop.IJavaPeerable self, Java.Interop.JniArgumentValue* parameters) [0x0002a] in <bd6bd528a8784b7caf03e9f25c9f0d7b>:0 
  at Org.Videolan.Libvlc.AWindow.SetVideoView (Android.Views.SurfaceView p0) [0x00031] in <7b7511c11eb74e4a92928dc74104a17e>:0 
  at LibVLCSharp.Platforms.Android.VideoView.Attach () [0x0002b] in <b891362fb4a04724910f1ae3d57b6cc1>:0 
  at LibVLCSharp.Platforms.Android.VideoView.set_MediaPlayer (LibVLCSharp.Shared.MediaPlayer value) [0x0001e] in <b891362fb4a04724910f1ae3d57b6cc1>:0 
  at LibVLCSharp.Forms.Platforms.Android.VideoViewRenderer.OnMediaPlayerChanging (System.Object sender, LibVLCSharp.Shared.MediaPlayerChangingEventArgs e) [0x0000c] in <1c512d0e0e3c4e059c601f600fc7013f>:0 
  at (wrapper delegate-invoke) System.EventHandler`1[LibVLCSharp.Shared.MediaPlayerChangingEventArgs].invoke_void_object_TEventArgs(object,LibVLCSharp.Shared.MediaPlayerChangingEventArgs)
  at LibVLCSharp.Forms.Shared.VideoView.OnMediaPlayerChanging (Xamarin.Forms.BindableObject bindable, System.Object oldValue, System.Object newValue) [0x00024] in <1c512d0e0e3c4e059c601f600fc7013f>:0 
  at Xamarin.Forms.BindableObject.SetValueActual (Xamarin.Forms.BindableProperty property, Xamarin.Forms.BindableObject+BindablePropertyContext context, System.Object value, System.Boolean currentlyApplying, Xamarin.Forms.Internals.SetValueFlags attributes, System.Boolean silent) [0x00051] in D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:475 
  at Xamarin.Forms.BindableObject.SetValueCore (Xamarin.Forms.BindableProperty property, System.Object value, Xamarin.Forms.Internals.SetValueFlags attributes, Xamarin.Forms.BindableObject+SetValuePrivateFlags privateAttributes) [0x00173] in D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:446 
  at Xamarin.Forms.BindableObject.SetValue (Xamarin.Forms.BindableProperty property, System.Object value, System.Boolean fromStyle, System.Boolean checkAccess) [0x0004d] in D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:374 
  at Xamarin.Forms.BindableObject.SetValue (Xamarin.Forms.BindableProperty property, System.Object value) [0x00000] in D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:349 
  at LibVLCSharp.Forms.Shared.VideoView.set_MediaPlayer (LibVLCSharp.Shared.MediaPlayer value) [0x00000] in <1c512d0e0e3c4e059c601f600fc7013f>:0 
  at LibVLCSharp.Forms.Shared.MediaPlayerElement.OnMediaPlayerChanged (LibVLCSharp.Shared.MediaPlayer mediaPlayer) [0x0000a] in <1c512d0e0e3c4e059c601f600fc7013f>:0 
  at LibVLCSharp.Forms.Shared.MediaPlayerElement.MediaPlayerPropertyChanged (Xamarin.Forms.BindableObject bindable, System.Object oldValue, System.Object newValue) [0x00000] in <1c512d0e0e3c4e059c601f600fc7013f>:0 
  at Xamarin.Forms.BindableObject.SetValueActual (Xamarin.Forms.BindableProperty property, Xamarin.Forms.BindableObject+BindablePropertyContext context, System.Object value, System.Boolean currentlyApplying, Xamarin.Forms.Internals.SetValueFlags attributes, System.Boolean silent) [0x00120] in D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:512 
  at Xamarin.Forms.BindableObject.SetValueCore (Xamarin.Forms.BindableProperty property, System.Object value, Xamarin.Forms.Internals.SetValueFlags attributes, Xamarin.Forms.BindableObject+SetValuePrivateFlags privateAttributes) [0x00173] in D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:446 
  at Xamarin.Forms.Internals.TypedBinding`2[TSource,TProperty].ApplyCore (System.Object sourceObject, Xamarin.Forms.BindableObject target, Xamarin.Forms.BindableProperty property, System.Boolean fromTarget) [0x0011f] in D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:233 
  at Xamarin.Forms.Internals.TypedBinding`2[TSource,TProperty].Apply (System.Boolean fromTarget) [0x00029] in D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:115 
  at Xamarin.Forms.Internals.TypedBinding`2+PropertyChangedProxy[TSource,TProperty].<OnPropertyChanged>b__16_0 () [0x00000] in D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:297 
  at Xamarin.Forms.DispatcherExtensions.Dispatch (Xamarin.Forms.IDispatcher dispatcher, System.Action action) [0x00028] in D:\a\1\s\Xamarin.Forms.Core\DispatcherExtensions.cs:53 
  at Xamarin.Forms.Internals.TypedBinding`2+PropertyChangedProxy[TSource,TProperty].OnPropertyChanged (System.Object sender, System.ComponentModel.PropertyChangedEventArgs e) [0x00033] in D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:297 
  at Xamarin.Forms.BindingExpression+WeakPropertyChangedProxy.OnPropertyChanged (System.Object sender, System.ComponentModel.PropertyChangedEventArgs e) [0x00012] in D:\a\1\s\Xamarin.Forms.Core\BindingExpression.cs:666 
  at (wrapper delegate-invoke) <Module>.invoke_void_object_PropertyChangedEventArgs(object,System.ComponentModel.PropertyChangedEventArgs)
  at RemoteControlRepetierServer.ViewModels.BaseViewModel.OnPropertyChanged (System.String propertyName) [0x00012] in C:\RCP\Source\RCRepetierServer\RCRS\ViewModels\BaseViewModel.cs:445 
  at RemoteControlRepetierServer.ViewModels.DashboardViewModel.set_MediaPlayer (LibVLCSharp.Shared.MediaPlayer value) [0x00017] in C:\RCP\Source\RCRepetierServer\RCRS\ViewModels\DashboardViewModel.cs:1924 
  at RemoteControlRepetierServer.ViewModels.DashboardViewModel.RefreshWebCamViewAction () [0x00351] in C:\RCP\Source\RCRepetierServer\RCRS\ViewModels\DashboardViewModel.cs:4497 
  --- End of managed Java.Lang.NullPointerException stack trace ---
java.lang.NullPointerException: view is null
	at org.videolan.libvlc.AWindow.setView(AWindow.java:235)
	at org.videolan.libvlc.AWindow.setVideoView(AWindow.java:268)
	at mono.java.lang.RunnableImplementor.n_run(Native Method)
	at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:30)
	at android.os.Handler.handleCallback(Handler.java:907)
	at android.os.Handler.dispatchMessage(Handler.java:105)
	at android.os.Looper.loop(Looper.java:216)
	at android.app.ActivityThread.main(ActivityThread.java:7625)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:987)

Issues Resolved

API Changes

None

Platforms Affected

  • Android

Behavioral/Visual Changes

None

Before/After Screenshots

Not applicable

Testing Procedure

PR Checklist

  • Rebased on top of the target branch at time of PR
  • Changes adhere to coding standard

@@ -53,6 +68,11 @@ protected override void OnElementChanged(ElementChangedEventArgs<VideoView> e)

private void OnMediaPlayerChanging(object sender, MediaPlayerChangingEventArgs e)
{
// Avoid updating the MediaPlayer if the Context from the Control is already disposed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you actually encounter this case? The repro code and stacktrace you shared does not include these lines, so I wonder why the defensive check here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this happens each time, if you navigate using Shell. If you re-visit a page, the old render gets disposed and a new one is created.
You can check it here:
https://github.com/AndreasReitberger/LibVlcViewIsNullTest

Just run the test repo and switch from "About" to "About2" and back to "About". Then you see the exception. If you set a breakpoint on "OnMediaPlayerChanging" and "Dispose", you'll see when it's called.

base.Dispose(disposing);

Control!.MediaPlayer = null;
Control!.Dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is very rare for ViewRenderers to dispose of the Control. Not sure if this is a common leak or if it should not be done.

Copy link
Author

@AndreasReitberger AndreasReitberger Sep 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically it's enough to set the "_isDisposing = true" here. Just avoid to update the MediaPlayer in the OnMediaPlayerChanging event. I just thought to do also the cleanup here for the Control.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check whether Xamarin.Forms performs the Dispose internally on the native Control? It's not obvious from some glances, but I think it might not. In which case we have a leak, including in the Apple renderer. If so, feel free to update it as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to figure out where the Control gets disposed and report back. I never had such issues on iOS, only Android.
Also, all is working fine if you do not use Shell. So I guess it's the way how Xamarin.Forms handles the page re-creation if you are using Shell.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this answers the disposing behaviour of Shell. @PureWeen wrote this on a Xamarin.Forms bug.

It should be a lot better right now in MAUI. One of the issues here is that on XF shell aggressively disposes and recreates renderers every time you move between tabs/flyout items. With MAUI we aren't doing that and as you move around the native stacks are all maintained.

If anyone is up for testing this out a bit on MAUI let me know your results.

xamarin/Xamarin.Forms#7521 (comment)

So I guess, when the Page is disposing, the MediaPlayerElement on it gets disposed as well (so the native control).
Then, Shell recreates the page.

Copy link
Member

@mfkl mfkl Sep 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VideoView gets disposed properly already, from my own testing. I don't think this is necessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then it should be fine to just avoid updating the MediaPlayer element if the renderer is disposing? (Set the flag _isDisposing = true)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to avoid something if it does not happen in the first place. This really shouldn't happen in the first place, but it's too easy to place the blame on Xamarin Shell only.

Do you think it could be possible to reproduce without Shell?

if (_isDisposing)
{
return;
}
Control.MediaPlayer = e.NewMediaPlayer;
Copy link
Author

@AndreasReitberger AndreasReitberger Sep 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception, from what I posted the Stacktrace, gets thrown here (if the Control is already disposed)

@mfkl
Copy link
Member

mfkl commented Sep 27, 2021

Still reviewing this, please bear with me. Could not always repro with the simulator but on device the error is consistent.

The reason this fails is because Attach() is called twice, and the second faulty call is originating from your code (or Xamarin.Shell)

>	0x8 in LibVLCSharp.Forms.Platforms.Android.VideoViewRenderer.OnMediaPlayerChanging at C:\LibVLCSharp\src\LibVLCSharp.Forms\Platforms\Android\VideoViewRenderer.cs:68,13	C#
 	0x31 in LibVLCSharp.Forms.Shared.VideoView.OnMediaPlayerChanging at C:\LibVLCSharp\src\LibVLCSharp.Forms\Shared\VideoView.cs:46,13	C#
 	[External Code]	
 	0x8 in LibVLCSharp.Forms.Shared.VideoView.set_MediaPlayer at C:\LibVLCSharp\src\LibVLCSharp.Forms\Shared\VideoView.cs:39,19	C#
 	0x13 in LibVLCSharp.Forms.Shared.MediaPlayerElement.OnMediaPlayerChanged at C:\LibVLCSharp\src\LibVLCSharp.Forms\Shared\MediaPlayerElement.xaml.cs:117,17	C#
 	0xD in LibVLCSharp.Forms.Shared.MediaPlayerElement.MediaPlayerPropertyChanged at C:\LibVLCSharp\src\LibVLCSharp.Forms\Shared\MediaPlayerElement.xaml.cs:158,13	C#
 	0x1A in LibVlcTestShell.ViewModels.BaseViewModel.OnPropertyChanged at C:\LibVLCSharp\samples\LibVlcTestShell\ViewModels\BaseViewModel.cs:50,13	C#
 	0x31 in LibVlcTestShell.ViewModels.BaseViewModel.SetProperty<LibVLCSharp.Shared.MediaPlayer> at C:\LibVLCSharp\samples\LibVlcTestShell\ViewModels\BaseViewModel.cs:38,13	C#
 	0xE in LibVlcTestShell.ViewModels.AboutViewModel.set_MediaPlayer at C:\LibVLCSharp\samples\LibVlcTestShell\ViewModels\AboutViewModel.cs:38,28	C#
 	0x147 in LibVlcTestShell.ViewModels.AboutViewModel.RefreshWebCamViewAction at C:\LibVLCSharp\samples\LibVlcTestShell\ViewModels\AboutViewModel.cs:63,13	C#

I'm not entirely satisfied with your approach to fix this, which feels like a quick workaround for a target we don't support (Shell), and I would like to explore other alternatives. Is it possible for you to modify your app so that Attach is not called unnecessarily?

@AndreasReitberger
Copy link
Author

Sure, take the time you need. I provided a very simple test repo. I don't call the Attach() from there, so I guess it's how Xamarin.Form.Shell handles the page recreation and calls the Attach() method twice.
At the moment I do not know how to avoid this crash otherwise.
Thanks for looking into this, even if Shell is not official supported.

@AndreasReitberger
Copy link
Author

AndreasReitberger commented Sep 27, 2021

So I made some further test with my repo. I also added a "Restart" button to trigger the reloading of the MediaPlayer bound to the MediaPlayerElement. This is what I've figured out:

  1. First visit of AboutPage.xml
  • Video is playing fine
  • I can press the restart button as often as I want, the VideoViewRenderer gets not disposed.
  1. Visit AboutTwoPage.xml
  • Video is playing fine
  1. Go back to AboutPage.xml
  • If I do not refresh the MediaPlayer in OnAppearing(), I simply can start the MediaPlayer by calling Play()
  • If I press then the Restart button, the VideoViewRenderer gets disposed if the MediaPlayer property gets updated (OnPropertyChanged) and I do not know why or where this happens.
private MediaPlayer _mediaPlayer;
/// <summary>
/// Gets the <see cref="LibVLCSharp.Shared.MediaPlayer"/> instance.
/// </summary>
public MediaPlayer MediaPlayer
{
    get => _mediaPlayer;
    private set => SetProperty(ref _mediaPlayer, value, nameof(MediaPlayer));
}
protected bool SetProperty<T>(ref T backingStore, T value,
     [CallerMemberName] string propertyName = "",
    Action onChanged = null)
{
    if (EqualityComparer<T>.Default.Equals(backingStore, value))
        return false;

    backingStore = value;
    onChanged?.Invoke();
    OnPropertyChanged(propertyName);
    return true;
}

#region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var changed = PropertyChanged;
            if (changed == null)
                return;

            changed.Invoke(this, new PropertyChangedEventArgs(propertyName)); // <=== HERE
        }
        #endregion

Then, the VideoViewRenderer is disposed before updating the MediaPlayer.
image

Output:
Attached to VideoViewRenderer to window
[PlayerBase] basePause() piid=3647
[android.media.AudioTrack] [HSM] AudioTrace pause() uid: 10221, pid: 20175
[PlayerBase] baseStart() piid=3647
[android.media.AudioTrack] [HSM] AudioTrace play() uid: 10221, pid: 20175
[mali_winsys] EGLint new_window_surface(egl_winsys_display *, void *, EGLSurface, EGLConfig, egl_winsys_surface **, EGLBoolean) returns 0x3000
[Window] window clear KeepScreenOnFlag for com.companyname.libvlctestshell
[SurfaceView] delay destroy surface control
Disposing renderer VideoViewRenderer
[SurfaceView] surfacePositionLost_uiRtSync mSurfaceControl = null
[PlayerBase] basePause() piid=3647
[android.media.AudioTrack] [HSM] AudioTrace pause() uid: 10221, pid: 20175
[PlayerBase] baseStart() piid=3647
[android.media.AudioTrack] [HSM] AudioTrace play() uid: 10221, pid: 20175
[PlayerBase] basePause() piid=3647
[android.media.AudioTrack] [HSM] AudioTrace pause() uid: 10221, pid: 20175
[android.media.AudioTrack] -27 17:35:19.114 E/HsmCoreServiceImpl( 1747): onTransact in code is: 102
[PlayerBase] baseStart() piid=3647
[android.media.AudioTrack] [HSM] AudioTrace play() uid: 10221, pid: 20175
[AudioTrack] stop() called with 0 frames delivered
[android.media.AudioTrack] [HSM] AudioTrace stop() uid: 10221, pid: 20175
[Window] window clear KeepScreenOnFlag for com.companyname.libvlctestshell
[0:] OnMediaPlayerChanging

It seems not to be a general problem of the OnAppearing(). Once I revisit a page, the VideoViewRenderer gets disposed when the MediaPlayer property changes.

public class VideoViewRenderer : ViewRenderer<LibVLCSharp.Forms.Shared.VideoView, LibVLCSharp.Platforms.Android.VideoView>
    {

        private bool _isDisposing = false;
        /// <summary>
        /// Main constructor (empty)
        /// </summary>
        /// <param name="context">Android context</param>
        public VideoViewRenderer(Context context) : base(context)
        {
        }
        /// <summary>
        /// 
        /// </summary>
        protected override void OnAttachedToWindow()
        {
            Console.WriteLine($"Attached VideoViewRenderer to window");
            base.OnAttachedToWindow();
        }

        /// <summary>
        /// Gets triggered when the object gets disposed
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
            _isDisposing = true;
            Console.WriteLine($"Disposing renderer {nameof(VideoViewRenderer)}");
            base.Dispose(disposing);

            //Control!.MediaPlayer = null;
            //Control!.Dispose();
        }

        /// <summary>
        /// Native control management during lifecycle events
        /// </summary>
        /// <param name="e">lifecycle event</param>
        protected override void OnElementChanged(ElementChangedEventArgs<VideoView> e)
        {
            base.OnElementChanged(e);
            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    SetNativeControl(new LibVLCSharp.Platforms.Android.VideoView(Context!));

                    e.NewElement.MediaPlayerChanging += OnMediaPlayerChanging;
                    if (Control!.MediaPlayer != e.NewElement.MediaPlayer)
                    {
                        OnMediaPlayerChanging(this, new MediaPlayerChangingEventArgs(Control!.MediaPlayer, e.NewElement.MediaPlayer));
                    }
                }
            }
            if (e.OldElement != null)
            {
                e.OldElement.MediaPlayerChanging -= OnMediaPlayerChanging;
            }
        }

        private void OnMediaPlayerChanging(object sender, MediaPlayerChangingEventArgs e)
        {
            // Avoid updating the MediaPlayer if the Context from the Control is already disposed
            if (_isDisposing)
            {
                Console.WriteLine($"Accessing renderer while disposing");
                return;
            }
            Control.MediaPlayer = e.NewMediaPlayer;
            Control.TriggerLayoutChangeListener();
        }
    }

Test repo: https://github.com/AndreasReitberger/LibVlcViewIsNullTest

@jeremyVignelles
Copy link
Collaborator

What about calling SetNativeControl(null) in the dispose, and skipping the method if Control is null for some reason instead ?
Feels less hackish to me.

@AndreasReitberger
Copy link
Author

As long as the exception disappears, I'm fine with it. Have you tested it this way?

@jeremyVignelles
Copy link
Collaborator

No I didn't that's a suggestion for you to try if you want to unlock the PR. I don't personnaly have the issue, but agree with mfkl that the solution you provide seems a bit too specific.

Could you try to see if it fixes the issue?

@mfkl would you be ok to merge such fix?

@AndreasReitberger
Copy link
Author

@jeremyVignelles
Sadly it's not possible to pass null for SetNativeControl();

image

@mfkl
Copy link
Member

mfkl commented Oct 26, 2021

The Xamarin team seems to have acknowledged the bug, so I'll close this for now. Thanks.

@mfkl mfkl closed this Oct 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants