1

I am working on an application that shows two videos side-by-side (unfortunately with custom format).

When I try to display the video frames, I get the "cannot access this object because a different thread owns it"-exception, even though I thought I calling the dispatcher would fix that.

It seems people have had similar problems when using BitmapImages or similar, and that they could solve the problem via freezing the image, but in my case, I receive a byte array, which does not seem to have a freeze-method.

This is the problematic code (which is called from an async method):

private void ShowVideo(string fileName)
{
    ColorVideo video = new ColorVideo(fileName); // ColorVideo reads the frames from disk

    WriteableBitmap image = new WriteableBitmap(video.Width, video.Height, 96, 96, PixelFormats.Bgr32, null);

    // set the image as source for the image frame in the UI
    this.Dispatcher.BeginInvoke((Action)delegate
    {
    this.ImageFrame.Source = image;
    });

    for (int i=0; i < video.Length, i++)
    {
        byte[] frameBytes = video.GetFrame(i);

        // The following line throws the exception.
        // wrapping the line inside a call to Dispatcher doesn't help
        // I get the cross-thread exception regardless

        image.WritePixels(new Int32Rect(0,0,video.Width,video.Height),frameBytes, video.Width*4,0);
    }
}

Now, I suspect that I my approach may not actually be ideal, anyway. All I want is to be able to run a showVideo method in the background and have it update the image shown 30 times a second. How can I do this better?

Jonas
  • 74,690
  • 10
  • 137
  • 177
  • You are using 96 as the X dpi, and 06 as the Y dpi. Shouldn't it be 96 for both X and Y? – Ove Mar 10 '14 at 22:08
  • @Ove: yes, of course. Typo. – Jonas Mar 10 '14 at 22:18
  • You should use `WriteableBitmap.BackBuffer` from the background thread: http://stackoverflow.com/questions/9868929/how-to-edit-a-writablebitmap-backbuffer-in-non-ui-thread – noseratio Mar 12 '14 at 06:55

3 Answers3

1

You must not create the WriteableBitmap in the background thread, but instead include this in the Invoke call or freeze it (the bitmap, not the array!)

Also, you probably shouldn't use BeginInvoke here, since it defers execution. Later on you continue to access the image from the background thread.

I get the impression you're a bit confused about accessing GUI elements with regards to threads - you should sort out the basics first.

JeffRSon
  • 10,404
  • 4
  • 26
  • 51
  • I am indeed a bit confused. I would love to have the time to sit down and figure out the basics first. Unfortunately I don't. Would you happen to be able (and have time) to kindly explain me just enough so that I will get by this time? – Jonas Mar 10 '14 at 22:25
  • Well, as mentioned above - if you create the WriteableBitmap in the background thread, you also need to do the processing in this thread's context (write pixels). After that, freeze the bitmap, and only then `Invoke` the assignment. – JeffRSon Mar 10 '14 at 22:29
  • When you want to keep your method alive as long as the video runs you can't freeze the bitmap, though. It could work like this: Create the bitmap with `Invoke` and assign it to the `source` property. Then, in your loop, `Invoke` the `WritePixel` method. However, I doubt that you'll have much fun with this. Normally, every frame should stay for, say, 40ms. But this is in no way fulfilled in your code. – JeffRSon Mar 10 '14 at 22:36
  • I left out the part where I use the stopwatch to ensure that the frame stays on for long enough, but I left it out to avoid cluttering the question - fml. Anyway, I ended up freezing a clone of the image and passing that to the UI thread. Thanks very much for your time! – Jonas Mar 14 '14 at 15:42
1

Note that you are first creating the bitmap, then you are issuing the BeginInvoke+setSource operations to the Dispatcher, and then you write the pixels.

This leaves a race: the dispatcher can first set the bitmap as the source, and then your loop may run and try to modify the pixels. Since the loop runs in a background thread, the WritePixels may throw. I have not checked, just a guess.

So, first try:

your-for-loop
{
   create-a-bitmap
   get-frame
   write-pixels

   // XXX

   this.Dispatcher.BeginInvoke((Action)delegate
   {
       this.ImageFrame.Source = image;
   });
}

So just moving the start of the dispatcher block may remove the problem, as it will be ensure that the loop will not write anything to a bitmap that was already pushed to the UI.

In this setup, you can still add "Freeze" at XXX marks, it will allow the WPF to skip some intermediate copies of the bitmaps, probably not a single copy will be done during rendering. However, adding Freeze will not help at all with the fact that the loop will constantly allocate and throw out new Bitmaps.

The fact of creating them will create some pressure on the memory and GC. Not only the bitmaps will be constantly created, it also must be periodically found and removed by GC. You can circumvent it a little by creating two bitmaps and reusing them by flipping odd-even:

create-bitmap-1
create-bitmap-2
your-for-loop
{
   get-frame

   if frame-number is ODD
      write-pixels to bitmap-1
   else
      write-pixels to bitmap-2

   this.Dispatcher.BeginInvoke((Action)delegate
   {
       if frame-number is ODD
           this.ImageFrame.Source = image1;
       else
           this.ImageFrame.Source = image2;
   });
}

Note important thing: when using this way, you cannot Freeze the bitmaps to help WPF in caching and cut the number of intermediate copies. You do not want to make them cacheable, and you need the bitmaps to be modifiable. It will slow down the renderer and/or compositor thread, but will lower the pressure on GC and CPU in overall.

quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
  • Thank you for your explanation. It is indeed on the WritePixels that the exception gets thrown. However, if I loop through all the images before I attach the image to the source, wouldn't I be unable to show a video? – Jonas Mar 10 '14 at 22:24
  • Freezing the image allows me to show the first image. Yay! However, I cannot seem to unfreeze it. – Jonas Mar 10 '14 at 22:36
  • Corrected. As I misread your loop, my answer was previously a bit of nonsense. See the current explanation. Try both ways to get a fell of what the "pressure" is. The second example with "less pressure" will have smaller FPS rating than that with "Freeze", but usually it's better option than the first. – quetzalcoatl Mar 10 '14 at 22:39
  • Btw. of course with the 'loop' thing, you will have a hard time of getting any stable FPS. You should use some timer or render/paint callback to "pull" the frames everytime a time of next frame passes. but that's another thing. If you get the loop running, rewriting it to "pull" or timer is easy. Especially if you use the "odd-even" way, extend it to, say, 3 or 4 buffers, write to them from the background thread, and in the timer/renderer only "pull" the oldest buffer from the writers queue.. But I think I'm getting to far away :) – quetzalcoatl Mar 10 '14 at 22:41
  • Thanks again for your answer and your time. I ended up cloning and freezing the image before passing it to the UI thread. I have set up the ColorVideo class so that I could easily add a ConcurrentQueue for buffering (note that at least in my case, with a spinning disk, running multiple file operations in parallel don't help), but the operation ended up being fast enough, anyway. – Jonas Mar 14 '14 at 15:40
0

Assuming ImageFrame is a UI control, you need to do ImageFrame.Invoke to set it's property .Source. Only thread that created that control instance has a right to access and manipulate it.

LB2
  • 4,802
  • 19
  • 35