OneByte_
OneByte_

Reputation: 81

JPEG Compression causes GDI+ Exception

So, I'm currently working on a LAN-Video-Streaming program, which records single images and sends them over. Because it would be too much to send 30 1920x1080 pictures per second, to get 30FPS, I did some research and found JPEG-Compression. The problem is, that when I try to save the compressed JPEG, it throws an System.Runtime.InteropServices.ExternalException, with the additional information: General error in GDI+.

Here's my code:

Private Sub Stream() Handles StreamTimer.Tick

    If Streaming = True Then

        Try
            ScreenCap = New Bitmap(Bounds.Width, Bounds.Height)

            GFX = Graphics.FromImage(ScreenCap)
            GFX.CopyFromScreen(0, 0, 0, 0, ScreenCap.Size)

            Dim Frame As New Bitmap(ScreenCap, Resolution.Split(";")(0), Resolution.Split(";")(1))

            Dim jpgEncoder As ImageCodecInfo = GetEncoder(ImageFormat.Jpeg)
            Dim myEncoder As Encoder = Encoder.Quality
            Dim myEncoderParameters As New EncoderParameters(1)
            Dim myEncoderParameter As New EncoderParameter(myEncoder, Compression)
            myEncoderParameters.Param(0) = myEncoderParameter

            Frame.Save(My.Computer.FileSystem.SpecialDirectories.Temp & "\LSSFrame.jpg", jpgEncoder, myEncoderParameters) 'Error occurs in this line

            Using FS As New FileStream(My.Computer.FileSystem.SpecialDirectories.Temp & "\LSSFrame.jpg", FileMode.Open)
                Frame = Image.FromStream(FS)
                FrameSizeStatus.Text = Math.Round(FS.Length / 1000) & "KB"
                FS.Close()
            End Using

            PreviewBox.Image = Frame
            FPSStat += 1

            FlushMemory()

            If ViewerIPs.Count > 0 Then

                For i = 0 To ViewerIPs.Count - 1
                    SendFrame(ViewerIPs(i), Frame)
                Next

            End If
        Catch ex As Exception
            LostFrames += 1
        End Try
    End If

End Sub

Any help is appreciated!

Upvotes: 0

Views: 235

Answers (1)

In part, you are not disposing of the Graphics or Bitmap objects you create. The error message is not very helpful, but not disposing of those will leave resources unrecovered.

There is also a lot going on in that procedure. If it were broken into parts it might be easier to fine-tune for performance and such.

' form level objects
Private jEncParams As EncoderParameters
Private jpgEncoder As ImageCodecInfo
...
' inititalize somewhere when the process starts:
Dim quality As Int64 = 95

jpgEncoder = GetEncoder(ImageFormat.Jpeg)
Dim myjEnc As Imaging.Encoder = Imaging.Encoder.Quality

jEncParams = New EncoderParameters(1)
' quality is inverse to compression
jEncParams.Param(0) = New EncoderParameter(myjEnc, quality)

Since the encoder and quality elements are not going to change for each screen snap, create them once and resuse them. Then your timer event:

Dim scrBytes = GetScreenSnap(1280, 720)
' do something to send them....maybe queue them?
Console.WriteLine("image size: {0}k", (scrBytes.Length / 1024).ToString)

Optimizing SendFrame is outside the scope of this Q/A, but getting the screen shot is separate from sending.

Private Function GetScreenSnap(w As Int32, h As Int32) As Byte()

    Using bmpScrn As New Bitmap(My.Computer.Screen.Bounds.Width, My.Computer.Screen.Bounds.Height)
        Using g As Graphics = Graphics.FromImage(bmpScrn)
            g.CopyFromScreen(0, 0, 0, 0, bmpScrn.Size)
        End Using       ' done with graphics

        Using bmpThumb As New Bitmap(bmpScrn, w, h),
            ms As New MemoryStream
            bmpThumb.Save(ms, jpgEncoder, jEncParams)
            Return ms.ToArray
        End Using        ' dispose of bmp
    End Using            ' dispose of bmpScrn

End Function

For no particular reason, I am thumbnailing the entire screen. Yours seems off using Bounds.Width, Bounds.Height since that would refer to the form. It would only work as a screen snapshot if the form is maximized. keypoints:

  • This is predicated on the idea that you can/will be sending the byte array in a stream. As such, I leave it as a byte array rather than creating a BMP only to (presumably) convert it back later.
  • There is no need to create a disk file to get the size. If the array contains encoded bytes, it will be the same size.
  • I've never used the COMPRESSION param, but I know Quality is inverse to compression.

Resulting sizes for various Quality factors:

100 = 462
95 = 254
90 = 195
80 = 147

Strictly speaking you do not need the encoder, bmpThumb.Save(ms, Imaging.ImageFormat.Jpeg) will also work but a single encoder object offers more fine tuning.


For the sending part, you might want a Stack, Queue or LinkedList to store the byte array. This would further isolate getting images from sending them. The collection would be a sort of ToDo/ToSend list.

Then, if there are multiple recipients, I'd look into perhaps doing SendFrame as a Task, perhaps sending 2-3 at a time. There might be a point where the number of recievers interferes with how fast you can grab new ones.

Upvotes: 1

Related Questions