Twój pierwszy i najpoważniejszy błąd, to nieprawdopodobnie czasochłonna obsługa zdarzenia. Powinna ona zająć maksymalnie ułamek milisekundy, a zajmuje czas, który może być liczony w sekundach. Obsługa zdarzeń jest jednowątkowa. Oznacza to, że dopóki nie zakończysz swojej obsługi zdarzenia, żadne inne zdarzenie, ani nawet odrysowanie gui nie zostanie obsłużone.
Są tylko dwie zasady dotyczące obsługi zdarzeń:
- Żadnych czasochłonnych operacji!
- Jeżeli korzystasz ze sleep, operacji plikowej lub tworzenia obiektów (new/fabryka), to jeszcze raz wszystko przeczytaj, skasuj ten kod i wróć do punktu 1.
U Ciebie obsługa zdarzenia kończy się dopiero po tym jak zmarnujesz 0,8s na pierwsze sleep oraz nieokreślony czas na utworzenie obiektu VideoCapture, a następnie w pętli kolejny raz marnujesz 10 razy po 10 ms oraz nieokreślony czas na zapis obrazu do pliku (prawdopodobnie). W efekcie tracisz co najmniej sekundę na obsługę jednego zdarzenia. W tym czasie całe GUI obsługiwane przez Javę jest kompletnie zamrożone i czeka na zakończenie Twojej procedury obsługi zdarzenia.
W jednym przebiegu pętli obsługi zdarzeń wszystkie polecenia repaint
dotyczące tego samego obszaru obrazu są scalane w jedno wywołanie. Obsługa pojedynczego zdarzenia jest jednym z elementów jednego przebiegu takiej pętli. Samej pętli nie widzisz ponieważ jest ona schowana w klasach Swinga i w jej trakcie przebiegane są wszystkie procedury:
- obsługi zdarzeń, wszystkich obiektów, które mogą je wygenerować
- aktualizacji obiektów (w tym żądania
repaint
)
- odmalowania zaktualizowanych obiektów
Krótko mówiąc z punktu widzenia obsługi GUI Twoja procedura jest niczym innym jak nieświadomym wandalizmem. :)
Teraz jak to naprawić.
Twoja obsługa przycisku ma za zadanie jedynie włączyć przełącznik pozwalający Twojemu programowi na samodzielne odrysowywanie kolejnych klatek pobieranych z jakiegoś źródła. I na tym ma się zakończyć. Ponieważ jednak potrzebne jest przygotowanie źródła, otwarcie plików i ewentualna obsługa błędów (obsługa pliku zawsze może wygenerować błąd i/o) - to musisz to zrobić najlepiej przed momentem w którym będziesz mógł otrzymać pierwsze żądanie obsługi zdarzenia. I tu właśnie wchodzi obsługa wątków.
Pierwszym wątkiem z jakim masz do czynienia jest wywołanie metody main
w przypadku aplikacji lub init
w przypadku apletu. Najlepiej więc z tego miejsca przygotować wszystko do przeprowadzania płynnej animacji, czytania z plików, strumieni itp.
Drugim wątkiem z jakim masz do czynienia jest wywołanie obsługi zdarzenia. Zazwyczaj wszystkie procedury obsługi wywoływane są w jednym wątku w jakiejś kolejności - dlatego mówi się, że obsługa GUI jest jednowątkowa.
Wbrew pozorom wątek odpalający metodę main
nie musi się kończyć, choć w przypadku apletu init
powinna zostać zakończona bo nie ma żadnej gwarancji, ze init, start, stop i destroy muszą być koniecznie osobnymi wątkami.
Wątek kręcący pętlą obsługi GUI uruchamia kiedy zostanie wykonana pierwsza operacja, która będzie wymagać jej działania - np. setVisible(true)
lub addXXXListener
.
Nie jesteś jednak ograniczony tymi zdefiniowanymi wątkami. Wszędzie gdzie potrzeba możesz utworzyć oraz następnie uruchomić nowy wątek. Tworzenie i uruchamianie nie musi być w jednym miejscu (nawet lepiej, żeby nie było). Jeżeli Twoja procedura obsługi potrzebuje więcej czasu na operacje, to zawsze może uruchomić inny wątek i tam mogą być te operacje kontynuowane. Aktualnie nowe wątki nie powinno odpalać się przez tworzenie obiektu Thread (to jest bardzo niski poziom wymagający drobiazgowej obsługi), lecz przez wysokopoziomowe klasy jak SwingWorker czy obiekty zadań (Runnable, Callable) wykonywane przez różne typy egzekutorów oraz timery.
W Twoim wypadku procedura obsługi naciśnięcia przycisku powinna uruchomić wątek timera, który w wywołaniu zegarowym co najmniej 25 razy na sekundę będzie odczytywał kolejny obraz z kamery, przygotowywał go w formacie nadającym się do odmalowania w wybranym oknie/komponencie, a następnie wyrzucał żądanie repaint
dla tego okna/komponentu.
To żądanie wyzwoli wywołanie metody paint
/paintComponent
(zależnie na jak wysokim poziomie chcesz odmalowywać kontrolkę), w której będziesz mógł po prostu odpalić metodę draw
na już przygotowanym obiekcie obrazka. Ważne, że czas wykonania wywołania zegarowego też jest ograniczony do maksymalnie 1/25 sekundy ponieważ jeżeli wykona się dłużej, to rozpocznie się już kolejne wywołanie zegarowe, które zacznie wczytywać kolejną klatkę itd. Wtedy jeżeli deficyt czasu będzie narastał doprowadzi to do awarii danych i katastrofy.
Zamiast timera można też użyć osobnego wątku, który będzie wykonywał pętlę aktywnego renderingu (tak jak robią to profesjonalne gry). Dzięki temu można uniknąć katastrofy w przypadku chwilowych opóźnień (tzw. laga), który może wyniknąć z przyczyn nad którymi nie masz żadnej kontroli (np. defragmentacji pamięci, kalibracji kamery, opóźnień sygnału itp.). Aktywny rendering pozwala bowiem na bieżący pomiar czasu i pominięcie niektórych najbardziej czasochłonnych elementów w sytuacji deficytu czasu, którego nie ma jak nadgonić. Jednak na Twoim poziomie jest to zbyt trudne, więc masz tylko informację, że istnieje lepsza alternatywa dla Timera.
Jeżeli więc ograniczysz się tylko do Timera, to musisz kontrolować czas wykonania każdego wywołania zegarowego i ograniczyć go do okresu czasu wynikającego z podziału sekundy na ilość klatek, którą chcesz/musisz uzyskać. Najlepiej jednak podziel go jeszcze na 2 lub 4 bo sam timer i reszta systemu też ma swój narzut czasowy, a aplikację możesz przecież wykonywać na systemie jednoprocesorowym. Musisz też utrzymywać flagę informującą czy jesteś w trakcie wykonywania wywołania zegarowego tak aby każde następne wywołanie zegarowe mogło ją sprawdzić i w przypadku gdy poprzednie wywołanie się nie zakończyło natychmiast zakończyć obsługę bieżącego wywołania (lub w czasie testów generować wyjątek oraz przerywać działanie aplikacji). To pozwoli uniknąć katastrofy - gdyby taka miała nastąpić.
Krótko podsumowując:
- main/init - przygotowujesz pliki, kamerę, obiekty potrzebne do płynnego odczytu takie jak timer itp., na komponencie, który ma wyświetlać obraz odpalasz
setIgnoreRepaint(true)
, żeby nie było niekontrolowanych odrysowań gdy Twoje obrazy nie są jeszcze gotowe.
- Metoda
actionPerformed
- ustawiasz flagę aktywności animacji i uruchamiasz timer do generowania wywołań zegarowych
- Metoda wywołania zegarowego sprawdza czy aktualizacja jest w trakcie (wtedy wychodzi), ustawia flagę aktualizacji, odczytuje obraz z kamery, przygotowuje go do wyświetlenia i wyłącza flagę aktualizacji przed wyjściem.
- Metoda
paint
(dla JPanel) lub paintComponent
(dla innych) odrysowuje przygotowany obraz i natychmiast kończy się.
- O
sleep
powinieneś zapomnieć, że w ogóle istnieje. De facto sleep
powoduje utworzenie nowego wątku, który nic nie robi, tylko czeka, a po jego zakończeniu robi join
z wątkiem, który sleep
wywołał. Dlatego można ją przerwać przed czasem i dlatego to przerwanie jest obligatoryjnie do złapania.
[aktualizacja]
W międzyczasie sobie poradziłeś, ale to nadal jest tylko działająca prowizorka, w której ręcznie sterujesz. Działa to głównie dzięki zbiegowi okoliczności i dobrej jakości kodu bibliotek, które wywołujesz. :)