kekyo / FlashCap

Independent video frame capture library on .NET/.NET Core and .NET Framework.
Apache License 2.0
198 stars 28 forks source link

Need support with Avalonia Sample #113

Closed ManuelKetisch closed 10 months ago

ManuelKetisch commented 12 months ago

Hello there,

i am currently trying to get FlashCap running in an Avalonia 11 Application using MVVM Community Toolkit. I am using the provided Sample to get something going, but i am somewhat confused about what to do with the provided sample methods in the ViewModel like: OnDeviceListChangedAsync, OnCharacteristicsChangedAsync, OnPixelBufferArrivedAsync... Are these methods supposed to be used as commands for the ComboBoxes? From the sample code, it is not clear to me where these Methods get called.

Could someone explain a little what is going on in that sample?

kekyo commented 12 months ago

Thanks reached out!

This sample code uses an MVVM framework called Epoxy that I maintain. Methods with PropertyChanged attributes as follows: OnDeviceListChangedAsync()

    [PropertyChanged(nameof(Device))]
    private ValueTask OnDeviceListChangedAsync(CaptureDeviceDescriptor? descriptor)
    { ... }

is called by Epoxy when a property of that name is changed. The argument is passed a modified value, which can be used to perform additional operations.

The Device property is updated with data binding when the device is changed from the combo box, so this method is called at that time to update the list of characteristics.

The method signature must return a ValueTask as in this example; Epoxy generates code at build time for all property changes, including code to properly supplement unhandled exceptions.

Is there anything else you don't understand?

ManuelKetisch commented 12 months ago

Ok thanks for clarifying this for me. I thought it had something to do with the Epoxy MVVM Framework.

Since i am using Microsofts MVVM Community Toolkit for my application, this Framework handles things a little bit diffrently. For Example: I can only assing the NotifyProperyChangedFor attribute to fields, not methods. Therefore i need to figure out a different way to trigger these methods when the device or characteristic is changing.

Assigning the RelayCommand attribute to the methods and NotifyCanExecuteChangedFor to the Device property doesn't do the trick.

I will see if i can get some help to make this work with MVVM Community Toolkit, since this issue has nothing to do with your FlashCap library.

ManuelKetisch commented 12 months ago

Update: I figured out how i can trigger my methods using MVVM Community Toolkit. I had to declare partial methods for the auto generated observables, like this:

[ObservableProperty] private CaptureDeviceDescriptor? _deviceDescriptor;
[ObservableProperty] private VideoCharacteristics? _characteristic;

partial void OnDeviceDescriptorChanged(CaptureDeviceDescriptor? descriptor)
{
    OnDeviceListChangedAsync(descriptor);
}

partial void OnCharacteristicChanged(VideoCharacteristics? characteristic)
{
     OnCharacteristicsChangedAsync(characteristic);
}

Now the sample code is working and i get images streamed to my SKImageView.

Now i have another question. The Window which hosts the SKImageView has a button, which when pressed, should take a snapshot from the current stream and store it in the parent windows image control.

What would be the best approach to take a single shot from the current image stream? Should i use the already available stored Bitmap from the Image property or should i handle this seperately, for example by using the TakeOneShotAsync method?

Edit: The image does not need to be stored as a file on the system when captured. This will be handled seperately in a save data method.

kekyo commented 12 months ago

Should i use the already available stored Bitmap from the Image property

This is a good idea. There is no special function in FlashCap that holds continuous image data.

One thing to consider is the possibility that a handler OnPixelBufferArrivedAsync() is called in a different thread than the UI thread, so there may be a race condition in saving and retrieving the SKBitmap.

However, since SKBitmap is a reference object (objref), holding it in a field or property would not be a problem if it were referenced by another handler (e.g., a button click handler).

Perhaps you are storing the decoded SKbitmap in a property to bind it to the SkiaImageView. You could save it using that value like:

// Save SKBitmap into png.
using (var fs = new FileStream(...))
{
    this.Image.Encode(fs, SKEncodedImageFormat.Png, 100);
    await fs.FlushAsync();
}

Or, if you really want to output the raw data captured by FlashCap to a file as it is, you can keep it as byte[] in a private field and refer to it. In this case, do not forget to use PixelBuffer.CopyImage() instead of PixelBuffer.ExtractImage() and PixelBuffer.ReferImage() to retrieve it.

private byte[]? imageData;

// Get image data binary and save
this.imageData = bufferScope.Buffer.CopyImage();
ManuelKetisch commented 11 months ago

Thanks for the advice. I am currently trying to make use of the SKBitmap directly by storing it in another ViewModels SKBitmap property. However, despite the fact, that the property is an ObservableProperty, the changing value does not trigger the View to update. Do you have any ideas why that is the case?

I have a MainView with its corresponding MainViewModel. The MainView has an Image control and a button. When the button is pressed, a secondary Window opens, which is the capture window. This capture window has a button, when pressed, stores the image in a property of the capture windows ViewModel. I then hand over the SKBitmap data from the ImageCaptureViewModel to the MainViewModel.

MainViewModel:

        [ObservableProperty] private SKBitmap? _currentImage11;

        [RelayCommand]
        private async Task CaptureImageButtonPressed(MainWindow ownerWindow)
        {
            await new ImageCaptureWindow().ShowDialog(ownerWindow);
        }

ImageCaptureWindowViewModel:

        [ObservableProperty] private SKBitmap? _image;

        [RelayCommand]
        private async Task CaptureImageButtonPressed(ImageCaptureWindow captureWindow)
        {
            _currentWindow = captureWindow;
            MainView mainView = new();

            var captureDevice = _captureDevice;
            captureDevice.StopAsync();

            _mainViewModel.CurrentImage11 = Image;
            _currentWindow.Close();
        }

Now i would expect the CurrentImage11 ObservableProperty to update the MainView Image Control:

                        <siv:SKImageView Width="600" Height="600"
                                         x:Name="SkImageView1"
                                         HorizontalAlignment="Center" VerticalAlignment="Center"
                                         Source="{Binding CurrentImage11, Mode=TwoWay}"
                                         Stretch="Uniform" />

But for some reason i does not update the view. I am missing something... but can't figure our what it is. How would you do it?

kekyo commented 11 months ago

I have no experience with the MVVM Community Toolkit, so I can't give you solid advice, but I am curious about something.

ManuelKetisch commented 11 months ago

Where does the ImageCaptureWindowViewModel._mainViewModel come from?

I instantiated it in the class constructor, but for some reason it does not trigger the notify property changed when the value of the property changes.

Also, the MainView mainView = new(); line doesn't seem to make sense.

Correct, that was experimental, and is no longer present in my code.

Anyways, i figured out, that MVVM Community Toolkit features messaging (sender, recipients), which allows for communication between different elements of an application (Models, Views, ViewModels). Now the property triggers notify property changed correctly and i am getting closer to my goal.

Thanks for the assistance.

kekyo commented 11 months ago

If I am not mistaken, When you create an instance of MainViewModel in the constructor of ImageCaptureWindowViewModel, no changes are transmitted to the "original" MainViewModel.

In this state, two instances of MainViewModel exist, and a new instance of MainViewModel is created and held separately from the instance of MainViewModel that is attached to the View on the display.

Assuming that ImageCaptureWindowViewModel._mainViewModel holds this second instance, any changes to this instance will not (automatically) be notified to the original instance.

There is no generally established way for the view model of a dialog-like view and the original view model to communicate with each other. For example, the party displaying the dialog would need to provide an explicit way to transfer:

// Construct new dialog view.
var dialog = new SubDialogWindow();

// HACK: Transfer this (main) view model reference into dialog view model.
((SubDialogViewModel)dialog.DataContext)._mainViewModel = this;

// Show.
dialog.Show();

As you can see this is dirty code, and some MVVM frameworks may have smart ways to assist with this.

(Epoxy does not, because Epoxy assumes an SPA-like style as in Web UI, and does not intend to take advantage of dialogs.)

kekyo commented 10 months ago

If there seem to be further problems, please reopen.