Screen capture läuft nicht stabil

  • VB.NET

Es gibt 40 Antworten in diesem Thema. Der letzte Beitrag () ist von Peter329.

    Screen capture läuft nicht stabil

    Hi,

    ich versuche einen Screen Recorder zu programmieren. Im Schritt 1 werden dazu in einer Schleife Screenshots gezogen und diese in .png Dateien gespeichert. Im Schritt 2 werden diese Dateien dann in ein Video Format konvertiert

    Den Schritt 1, das Sammeln von Screenshots, habe ich wie folgt kodiert:

    VB.NET-Quellcode

    1. Public Sub TakeScreenshot()
    2. VideoFno += 1
    3. Debug.Print("VideoFno=" & VideoFno.ToString)
    4. 'Capture picture
    5. Dim errInd As Integer
    6. Dim capture As System.Drawing.Bitmap
    7. Dim graph As Graphics
    8. Try
    9. errInd = 1
    10. Debug.Print("Action: capture=....")
    11. capture = New Bitmap(area.Width, area.Height, PixelFormat.Format32bppArgb)
    12. errInd = 2
    13. Debug.Print("Action: graph=...")
    14. graph = Graphics.FromImage(capture)
    15. errInd = 3
    16. Debug.Print("Action: graph.CopyFromScreen ...")
    17. graph.CopyFromScreen(area.X, area.Y, 0, 0, area.Size, CopyPixelOperation.SourceCopy)
    18. Catch ex As Exception
    19. Timer1.Enabled = False
    20. Label1.Visible = False
    21. Debug.Print("Error" & errInd.ToString & ": Fno=" & VideoFno.ToString & " " & ex.Message)
    22. Debug.Print("area=" & area.ToString)
    23. Debug.Print("capture=" & If(capture Is Nothing, "NOTHING", capture.Size.ToString))
    24. Debug.Print("graph=" & If(graph Is Nothing, "NOTHING", graph.ToString))
    25. Exit Sub
    26. End Try
    27. 'Save file
    28. Dim ScreenShotFileName As String = "img" & VideoFno.ToString.PadLeft(8, "0"c) & ".png"
    29. Dim ScreenShotObject As String = Path.Combine(VIDEODIRECTORY, ScreenShotFileName)
    30. capture.Save(ScreenShotObject, ImageFormat.Png) 'Save picture as png
    31. Debug.Print("Fno=" & VideoFno.ToString & ", OK")
    32. End Sub


    Pro Sekunde ziehe ich mit einem Timer zwei Screenshots ... und das klappt auch zunächst problemlos. Die erstellten .png Dateien sehen blendend aus. Aber irgendwann ist Schluss mit lustig:


    Quellcode

    1. ...
    2. Fno=372, OK
    3. VideoFno=373
    4. Action: capture=....
    5. Action: graph=...
    6. Action: graph.CopyFromScreen ...
    7. Fno=373, OK
    8. VideoFno=374
    9. Action: capture=....
    10. Ausnahme ausgelöst: "System.ArgumentException" in System.Drawing.dll
    11. Error1: Fno=374 Parameter is not valid.
    12. area={X=10,Y=10,Width=1000,Height=600}
    13. capture=NOTHING
    14. graph=NOTHING


    373 files werden fehlerfrei geschrieben ... aber dann beim File numero 374 kracht es im capture ... Die Meldung "Parameter is not valid" hilft nicht weiter, denn wie man sieht ist da nichts falsch dran !

    Nach diesem Fehler scheitern auch alle weiteren Screenshots .... also mit "einfach ignorieren" kann ich das Problem nicht umgehen. Erst nach einem Neustart des Programms funktioniert erst mal wieder alles .. bis zum nächsten Crash.

    Meine Fragen:

    Weiß jemand wieso dieser Fehler auftritt .... und wie man das ggfs. abwenden kann ? Ich könnt mir vorstellen, dass da irgendwo Arbeitsbereiche überlaufen, die man nach jedem Screenshot leeren müsste ...

    Oder gibt es alternative Möglichkeiten im Visual Basic eine Area zu überwachen und als .avi oder besser als .mp4 Datei abzuspeichern ?

    LG
    Peter

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Peter329“ ()

    Hi erstmal,

    hast du mal einen haltepunkt an der betreffenden stelle gesetzt und geschaut welche werte dein

    Quellcode

    1. ​area
    hat?

    VB.NET-Quellcode

    1. capture = New Bitmap(area.Width, area.Height, PixelFormat.Format32bppArgb)


    der Fehler in bezug auf Graphics wird im normalfall bei Bitmaps ausgegeben wenn einer der werte 0 annimmt.

    LG Mausekeks
    Brain is Loading: 35%
    @Peter329 Wahrscheinlich läuft der Cache voll und dann knallt es,
    ich denke, es liegt an Dim graph As Graphics.
    Arbeite mit Using und Finally und räume in jeder Runde den Speicher sauber auf.
    ====
    MP4 und AVI sind Film-Formate, das passt nicht zu ScrennShots.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Vielen Dank erst mal für eure Ratschläge 1

    mausekeks schrieb:

    der Fehler in bezug auf Graphics wird im normalfall bei Bitmaps ausgegeben wenn einer der werte 0 annimmt.


    An der Debug Ausgabe erkennt man, dass die Werte für area ok sind. Die bleiben während der gesamten Verarbeitung unverändert. Daran liegt es also nicht.

    RodFromGermany schrieb:

    Wahrscheinlich läuft der Cache voll und dann knallt es,


    Ich habe jetzt eingefügt:

    VB.NET-Quellcode

    1. If capture IsNot Nothing Then capture.Dispose()
    2. If graph IsNot Nothing Then graph.Dispose()


    Damit tritt der Abbruch bisher nicht mehr auf ! Das Problem ist damit wohl gelöst. :)

    RodFromGermany schrieb:

    MP4 und AVI sind Film-Formate, das passt nicht zu ScrennShots.


    Jau ... das weiß ich schon. Dafür möchte ich dann das Freeware Program ffmpeg zum Einsatz bringen ... das soll aus einer Sammlung von .png Dateien ein .avi zusammen basteln. Das scheint ein wenig hakelig zu sein ... doch das sollte sich lösen lassen.

    Aber ...

    ... wenn ich mir ansehe, welche Datenmengen da allein bei einer Sampling Rate von 2 frames pro Sekunde zusammen kommen, dann stellt sich mir schon die Frage, ob das wirklilch das richtige Vorgehen ist ! Denn für einen flüssigen Film brauche ich ja eine Sampling Rate von mindestens 20 frames pro Sekunde ... Eine einstündige Aufnahme braucht damit 72.000 frames zu jeweils ca 1 MB ... das ist dann vielleicht doch ein bissl viel Holz ... :)

    Womit ich wieder bei meiner zweiten Frage wäre: gibt es alternative Möglichkeiten im Visual Basic eine Area zu überwachen und als .avi oder besser als .mp4 Datei abzuspeichern ?

    LG
    Peter
    @Peter329 Lass mal Deine ScreenShot-Maschine in einem anderen Thread arbeiten.
    Und:
    Sieh mal hier rein, ich hab mal damit rumgespielt, das funktioniert.
    Konverter der Bilder zu einem Video umwandelt
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    Super ! Na, das probiere ich aus ! :)

    Jetzt habe ich aber erst noch ein Problem mit dem Speicher. Ich habe jetzt mit 20 Frames pro Sekunde 15.000 .png Dateien gesammelt und würde die einfach mal als Preview mit einem Timer in einer picBox ansehen wollen

    VB.NET-Quellcode

    1. Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
    2. currentFno += 1
    3. If currentFno >= myFrames.Length Then
    4. StopTimer()
    5. MessageBox.Show("Preview ended.")
    6. Exit Sub
    7. End If
    8. Try
    9. picFrame.Image = Image.FromFile(myFrames(currentFno))
    10. Catch ex As Exception
    11. StopTimer()
    12. MessageBox.Show("Timer1" & NewLine2 & ex.Message)
    13. End Try
    14. End Sub


    Das Timer Inteval steht gemäß der FrameRate auf Timer1.Interval = 50 (50 ms = 20 Tics pro Sekunde).

    Das klappt auch prima ... das sieht aus als würde man das Dingens als Video abspielen ... hehehe !

    Aber leider ist nach 450 Bildchen (also nach einer knappen halben Minute) wieder Schluss mit lustig.

    Ich erhalte vom Befehl

    VB.NET-Quellcode

    1. picFrame.Image = Image.FromFile(myFrames(currentFno))


    eine Out of Memory Exception

    Irgendwas wird also nicht aufgeräumt.

    Wenn ich ein

    Quellcode

    1. picFrame.Dispose()
    vor die Anweisung setze, sehe ich gar nichts mehr ...

    Irgendwelche Ideen, was ich da machen kann ?

    LG
    Peter

    Neu

    @Peter329 Arbeite nicht mit .Dispose()[tt] sondern mit [tt]Using.
    Kannst Du mal das bereinigte Projekt posten?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    Peter329 schrieb:

    Wenn ich ein picFrame.Dispose() vor die Anweisung setze, sehe ich gar nichts mehr ...
    Du willst ja auch nicht die PicBox disposen, sondern das enthaltene Bild. So läuft der Speicher nicht voll:

    VB.NET-Quellcode

    1. If picFrame.Image IsNot Nothing Then picFrame.Image.Dispose()
    2. picFrame.Image = Drawing.Image.FromFile(…)

    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VaporiZed“ ()

    Neu

    @Peter329 Ich hab aus Deinem Code mal ne Variante gemacht, die bei mir locker bis 1000 Bilder läuft, ich hab die Bildgröße auf 640, 480 festgelegt.
    Bei einer Bildgröße von 1800, 1080 hab ich bis 630 Bilder laufen lassen, problemlos.
    Wenn Du das Bild nur abspeicherst, genügt ein Using innerhalb des Try-Blocks.
    Wenn Du das Bild darstellst, musst Du das PictureBox1.Image vor dem Zuweisen des neuen Bildes disposen, wie @VaporiZed scheibt.
    Interessamt wird dann allerdings die Laufzeit.
    Ich liege bei 1800, 1080 Pixeln im DEBUG-Mode bei 140 ms, d.h. das Capturen dauert mit einem Intervall von 100 ms länger als der Abstand zwischen zwei Ticks.
    Auch in der RELEASE bin ich da nicht schneller.
    Das heißt, dass Du irgendwann überholt wirst mit dem OutOfMemory, weil Du die Bilder nicht schnell genug abspeichern kannst, es sei denn, Du hast ne SSD-Platte.
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Imports System.IO
    2. Imports System.Drawing.Imaging
    3. Public Class Form1
    4. Private VideoFno As Integer = 0
    5. Private area As Rectangle = New Rectangle(0, 0, 1800, 1080)
    6. Private VIDEODIRECTORY As String = "C:\Temp\Screenshot\"
    7. Public Sub TakeScreenshot()
    8. VideoFno += 1
    9. 'Debug.Print("VideoFno=" & VideoFno.ToString)
    10. 'Capture picture
    11. Dim errInd As Integer
    12. Try
    13. errInd = 1
    14. 'Debug.Print("Action: capture=....")
    15. Dim capture = New Bitmap(area.Width, area.Height)
    16. 'Using capture = New Bitmap(area.Width, area.Height)
    17. errInd = 2
    18. 'Debug.Print("Action: graph=...")
    19. Dim graph = Graphics.FromImage(capture)
    20. errInd = 3
    21. 'Debug.Print("Action: graph.CopyFromScreen ...")
    22. graph.CopyFromScreen(area.X, area.Y, 0, 0, area.Size, CopyPixelOperation.SourceCopy)
    23. 'Save file
    24. Dim ScreenShotFileName As String = "img" & VideoFno.ToString.PadLeft(8, "0"c) & ".png"
    25. Dim ScreenShotObject As String = Path.Combine(VIDEODIRECTORY, ScreenShotFileName)
    26. capture.Save(ScreenShotObject, ImageFormat.Png) 'Save picture as png
    27. Debug.Print(" Fno=" & VideoFno.ToString & ", OK")
    28. If PictureBox1.Image IsNot Nothing Then
    29. PictureBox1.Image.Dispose()
    30. End If
    31. PictureBox1.Image = capture
    32. 'End Using
    33. Catch ex As Exception
    34. CheckBox1.Checked = False
    35. Timer1.Enabled = False
    36. Label1.Visible = False
    37. Debug.Print(" Error" & errInd.ToString & ": Fno=" & VideoFno.ToString & " " & ex.Message)
    38. End Try
    39. End Sub
    40. Private Sub CheckBox1_CheckedChanged(sender As Object, e As EventArgs) Handles CheckBox1.CheckedChanged
    41. Timer1.Enabled = CheckBox1.Checked
    42. End Sub
    43. Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
    44. Dim sw = Stopwatch.StartNew()
    45. TakeScreenshot()
    46. sw.Stop()
    47. Label1.Text = String.Format("Duration = {0} ms", sw.ElapsedMilliseconds)
    48. End Sub
    49. End Class
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    ok, erst mal Danke für eure Ratschläge !

    Jau, mit picFrameImage.Dispose funktioniert das dann .. die Preview über die Frames läuft jetzt hervorragend.

    Allein die Datenmenge ist halt prohibitiv !

    Jetzt habe ich angefangen die Konvertierung in eine .avi Datei zu basteln ! Da habe ich den Hinweis von RFG aufgegriffen und die Klassen Avi und AviWriter aus dem Spoiler kopiert. Das funzt ohne Probleme. Den Aufruf realisiere ich wie folgt:

    VB.NET-Quellcode

    1. Private Sub Convert_To_Video2()
    2. 'Initialize display
    3. Dim myStartTime = DateTime.Now
    4. txtElapsed.Text = ""
    5. cmdConvert.ForeColor = Color.Red
    6. cmdConvert.Update()
    7. Dim myFolder As String = VIDEODIRECTORY & "Hold2" '***TEST***
    8. Dim myFiles As String()
    9. Try
    10. myFiles = Directory.GetFiles(myFolder)
    11. Catch ex As Exception
    12. MessageBox.Show("LoadFrames" & NewLine2 & ex.Message)
    13. Exit Sub
    14. End Try
    15. Dim Writer As New AviWriter
    16. Writer.OpenAVI(Path.Combine(VIDEODIRECTORY, "output.avi"), 20) 'path, framerate
    17. Dim mincnt As Integer = CInt(txtStartFrame.Text)
    18. Dim maxcnt As Integer = CInt(txtEndFrame.Text)
    19. Dim cnt As Integer = 0
    20. For i = mincnt To maxcnt
    21. cnt += 1
    22. 'If cnt > 100 Then Exit For '***TEST****
    23. Using Frame As Bitmap = Image.FromFile(myFiles(i))
    24. Writer.AddFrame(Frame)
    25. 'Frame.Dispose()
    26. If cnt Mod 100 = 0 Then
    27. cmdConvert.Text = cnt.ToString("n0")
    28. cmdConvert.Update()
    29. txtElapsed.Text = (DateTime.Now - myStartTime).ToString.Substring(0, 8)
    30. txtElapsed.Update()
    31. End If
    32. End Using
    33. Next
    34. Writer.Close()
    35. txtElapsed.Text = (DateTime.Now - myStartTime).ToString.Substring(0, 8)
    36. cmdConvert.ForeColor = SystemColors.ControlText
    37. cmdConvert.Text = "Convert"
    38. MessageBox.Show("OK, AVI created" & NewLine & cnt.ToString("n0") & " files processed.")
    39. End Sub


    Bei kurzen Samplings klappt das sogar sehr gut !

    Aber wenn man mehr als 1.000 frames hat, dann ist die Routine nicht stabil.

    1. Das Ganze dauert Ewigkeiten

    2. Der .avi File ist nicht unbedingt fehlerfrei (Index broken) ... wenn man den Index neu aufbaut, dann kriegt man genau die gleiche Fehlermeldung. Wenn man das Dingens "as is" abspielt, dann bleibt er vor dem eigentlichen Ende "hängen" ...

    3. Bei sehr großen files (z.B. 10.000 samples) ist der .avi File nicht abspielbar, bzw. zeigt nur "Schrott" an.

    4. Die FileSize ist auch für kleine Samples über 3 GB ... mein Hex Editor schafft nur 2 GB ... deshalb kann ich gar nicht nachschauen was da hinein geschrieben wird.

    Meine Frage: hat jemand mit Avi / AviWriter Erfahrung und kann mir sagen, ob das Dingens brauchbar ist ?

    LG
    Peter
    Bilder
    • s 2020-05-21 17-23-568.jpg

      17,85 kB, 496×191, 14 mal angesehen

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Peter329“ ()

    Neu

    @Peter329 Probierma, die merkwürdige AVI mit FFMPEG zu konvertieren.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    Jau, das hab ich mir schon so gedacht. Da bleibt mir wohl nix anderes übrig als die hakelige ffmpeg Geschichte zu installieren. Hab halt vorher nachfragen wollen, ehe ich in den sauren Apfel beiße ... :)

    LG
    Peter

    Neu

    Peter329 schrieb:

    installieren
    Da wird nix installiert, das kannst Du einfach in ein Verzeichnis kopieren und mit dem entsprechenden Pfad aufrufen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    RodFromGermany schrieb:

    Da wird nix installiert


    Na ja, man entpackt den ZIP File und erweitert ggf. die %PATH% Umgebungsvariable ... das ist für mich auch ein Install. Iss halt eine Frage der Definition. :)

    Aber um das Thema abzurunden: ffmpeg funktioniert für meine Zwecke hervorragend. Den AVI_WRITER habe ich ad acta gelegt.

    Allerdings ist das eine Konsolanwendung und deshalb basiert die Syntax auf Command Line Arguments. Und die sind mehr als hakelig. Aber man kann damit leben.

    Die Laufzeit ist ordentlich .. die Konvertierung eines 10 Minuten Samplings in einen AVI File dauert etwa genau so lange ! Offensichtlich braucht das ffmpeg enorme Rechenleistung. Leider wird die CPU Nutzung vom ffmpeg wohl nicht (oder kaum) parallelisiert. Deshalb hat man bei 4 Kernen eine konstante CPU Last von ziemlich genau 25 %. Aber auch damit kann man leben.

    Vielen Dank noch mal an die Ratgeber, ohne die ich das so nicht hinbekommen hätte.

    LG
    Peter

    Neu

    Peter329 schrieb:

    und erweitert ggf. die %PATH% Umgebungsvariable
    Gegebenenfalls.
    Ich hab mir ein Progrämmchen geschrieben, mit dem ich TS-Dateien nach MP4 konvertiere, da wird dann der FFMPEG-Pfad reinkopiert.
    %PATH% fasse ich schon sehr lange nicht mehr an.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    Peter329 schrieb:

    Ist das einfach zu realisieren ?
    Sehr einfach.
    Du generierst per Code eine Batch-Datei und startest sie.
    Lässt Dir per Event das Beenden melden und startest die nächste Batch.
    Den Befehl muss ich heute Abend mal raussuchen.
    ====
    Du kannst Dir natürlich mit ffmpeg /? und Pause in einer Batch-Datei die Möglichkeiten mal ausgeben lassen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    RodFromGermany schrieb:

    Du generierst per Code eine Batch-Datei und startest sie.
    Hab mich jetzt noch nicht sooo häufig mit ffmpeg beschäftigt, aber bisher waren meine Befehle Einzeiler, die ich auch per Process.Start() umsetzen konnte. Spräche was dagegen?
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.

    Neu

    VaporiZed schrieb:

    Spräche was dagegen?
    Nö.
    Ich hab den Code nicht im Kopf, es kann sein, dass da nur noch ein Pause-Befehl drinne steht.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    Neu

    Auf den Dreh mit der Batch Datei bin ich auch schon verfallen.

    Das Problem ist, dass die Laufzeit des ffmpeg meist sehr lang ist. Und ungeduldige Menschen wie ich minimieren dann das Fenster !

    Wenn man ffmpeg über Process.Start aufruft, dann wird die DOS Box beim Programmende geschlossen. Und davon bekomme ich nix mit !

    Mit einer Batch Prozedur kann ich nach ffmpeg den Befehl "Pause" aufrufen und damit wird die DOS Box nicht geschlossen.

    Zitat RFG

    Du generierst per Code eine Batch-Datei und startest sie.
    Lässt Dir per Event das Beenden melden und startest die nächste Batch.

    Wie mache ich das denn ? Ich könnte aus der .bat Datei, ein Programm aufrufen, dass ein Messagebox.Show absetzt. Aber ob du das damit meinst ?

    LG
    Peter

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Peter329“ ()