2222import java .nio .ByteBuffer ;
2323import java .nio .ByteOrder ;
2424import javafx .application .Platform ;
25- import javafx .beans .property .ObjectProperty ;
26- import javafx .beans .property .SimpleObjectProperty ;
25+ import javafx .beans .property .ReadOnlyObjectProperty ;
26+ import javafx .beans .property .ReadOnlyObjectWrapper ;
2727import javafx .scene .image .Image ;
2828import javafx .scene .image .PixelBuffer ;
2929import javafx .scene .image .PixelFormat ;
3636import org .freedesktop .gstreamer .elements .AppSink ;
3737
3838/**
39- *
39+ * A wrapper connecting a GStreamer AppSink and a JavaFX Image, making use of
40+ * {@link PixelBuffer} to directly access the native GStreamer pixel data.
41+ * <p>
42+ * Use {@link #imageProperty()} to access the JavaFX image. The Image should
43+ * only be used on the JavaFX application thread, and is only valid while it is
44+ * the current property value. Using the Image when it is no longer the current
45+ * property value may cause errors or crashes.
4046 */
4147public class FXImageSink {
42-
48+
4349 private final static String DEFAULT_CAPS ;
44-
50+
4551 static {
4652 if (ByteOrder .nativeOrder () == ByteOrder .LITTLE_ENDIAN ) {
4753 DEFAULT_CAPS = "video/x-raw, format=BGRx" ;
@@ -51,18 +57,31 @@ public class FXImageSink {
5157 }
5258
5359 private final AppSink sink ;
54- private final ObjectProperty <Image > image ;
60+ private final ReadOnlyObjectWrapper <Image > image ;
5561 private final AtomicReference <Sample > pending ;
5662 private final NewSampleListener newSampleListener ;
5763 private final NewPrerollListener newPrerollListener ;
58-
64+
5965 private Sample activeSample ;
6066 private Buffer activeBuffer ;
61-
67+
68+ private int requestWidth ;
69+ private int requestHeight ;
70+ private int requestRate ;
71+
72+ /**
73+ * Create an FXImageSink. A new AppSink element will be created that can be
74+ * accessed using {@link #getSinkElement()}.
75+ */
6276 public FXImageSink () {
6377 this (new AppSink ("FXImageSink" ));
6478 }
65-
79+
80+ /**
81+ * Create and FXImageSink wrapping the provided AppSink element.
82+ *
83+ * @param sink AppSink element
84+ */
6685 public FXImageSink (AppSink sink ) {
6786 this .sink = sink ;
6887 sink .set ("emit-signals" , true );
@@ -71,10 +90,95 @@ public FXImageSink(AppSink sink) {
7190 sink .connect (newSampleListener );
7291 sink .connect (newPrerollListener );
7392 sink .setCaps (Caps .fromString (DEFAULT_CAPS ));
74- image = new SimpleObjectProperty <>();
93+ image = new ReadOnlyObjectWrapper <>();
7594 pending = new AtomicReference <>();
7695 }
77-
96+
97+ /**
98+ * Property wrapping the current video frame as a JavaFX {@link Image}. The
99+ * Image should only be accessed on the JavaFX application thread. Use of
100+ * the Image when it is no longer the current value of this property may
101+ * cause errors or crashes.
102+ *
103+ * @return image property for current video frame
104+ */
105+ public ReadOnlyObjectProperty <Image > imageProperty () {
106+ return image .getReadOnlyProperty ();
107+ }
108+
109+ /**
110+ * Get access to the AppSink element this class wraps.
111+ *
112+ * @return AppSink element
113+ */
114+ public AppSink getSinkElement () {
115+ return sink ;
116+ }
117+
118+ /**
119+ * Clear any image and dispose of underlying native buffers. Can be called
120+ * from any thread, but clearing will happen asynchronously if not called on
121+ * JavaFX application thread.
122+ */
123+ public void clear () {
124+ if (Platform .isFxApplicationThread ()) {
125+ clearImage ();
126+ } else {
127+ Platform .runLater (() -> clearImage ());
128+ }
129+ }
130+
131+ /**
132+ * Request the given frame size for each video frame. This will set up the
133+ * Caps on the wrapped AppSink. Values are in pixels. A value of zero or
134+ * less will result in the value being omitted from the Caps.
135+ *
136+ * @param width pixel width
137+ * @param height pixel height
138+ * @return this for chaining
139+ */
140+ public FXImageSink requestFrameSize (int width , int height ) {
141+ this .requestWidth = width ;
142+ this .requestHeight = height ;
143+ sink .setCaps (Caps .fromString (buildCapsString ()));
144+ return this ;
145+ }
146+
147+ /**
148+ * Request the given frame rate. This will set up the Caps on the wrapped
149+ * AppSink. Value is in frames per second. A value of zero or less will
150+ * result in the value being omitted from the Caps.
151+ *
152+ * @param rate frame rate in frames per second
153+ * @return this for chaining
154+ */
155+ public FXImageSink requestFrameRate (double rate ) {
156+ requestRate = (int ) Math .round (rate );
157+ sink .setCaps (Caps .fromString (buildCapsString ()));
158+ return this ;
159+ }
160+
161+ private String buildCapsString () {
162+ if (requestWidth < 1 && requestHeight < 1 && requestRate < 1 ) {
163+ return DEFAULT_CAPS ;
164+ }
165+ StringBuilder sb = new StringBuilder (DEFAULT_CAPS );
166+ if (requestWidth > 0 ) {
167+ sb .append (",width=" );
168+ sb .append (requestWidth );
169+ }
170+ if (requestHeight > 0 ) {
171+ sb .append (",height=" );
172+ sb .append (requestHeight );
173+ }
174+ if (requestRate > 0 ) {
175+ sb .append (",framerate=" );
176+ sb .append (requestRate );
177+ sb .append ("/1" );
178+ }
179+ return sb .toString ();
180+ }
181+
78182 private void updateImage () {
79183 if (!Platform .isFxApplicationThread ()) {
80184 throw new IllegalStateException ("Not on FX application thread" );
@@ -85,13 +189,13 @@ private void updateImage() {
85189 }
86190 Sample oldSample = activeSample ;
87191 Buffer oldBuffer = activeBuffer ;
88-
192+
89193 activeSample = newSample ;
90194 Structure capsStruct = newSample .getCaps ().getStructure (0 );
91195 int width = capsStruct .getInteger ("width" );
92196 int height = capsStruct .getInteger ("height" );
93197 activeBuffer = newSample .getBuffer ();
94-
198+
95199 PixelBuffer <ByteBuffer > pixelBuffer = new PixelBuffer (width , height ,
96200 activeBuffer .map (false ), PixelFormat .getByteBgraPreInstance ());
97201 WritableImage img = new WritableImage (pixelBuffer );
@@ -103,46 +207,58 @@ private void updateImage() {
103207 if (oldSample != null ) {
104208 oldSample .dispose ();
105209 }
106-
107- }
108-
109- public ObjectProperty <Image > imageProperty () {
110- return image ;
210+
111211 }
112-
113- public AppSink getElement () {
114- return sink ;
212+
213+ private void clearImage () {
214+ if (!Platform .isFxApplicationThread ()) {
215+ throw new IllegalStateException ("Not on FX application thread" );
216+ }
217+ Sample newSample = pending .getAndSet (null );
218+ if (newSample != null ) {
219+ newSample .dispose ();
220+ }
221+ image .set (null );
222+ if (activeBuffer != null ) {
223+ activeBuffer .unmap ();
224+ activeBuffer = null ;
225+ }
226+ if (activeSample != null ) {
227+ activeSample .dispose ();
228+ activeSample = null ;
229+ }
115230 }
116-
117-
231+
118232 private class NewSampleListener implements AppSink .NEW_SAMPLE {
119233
120234 @ Override
121235 public FlowReturn newSample (AppSink appsink ) {
122236 Sample s = appsink .pullSample ();
123237 s = pending .getAndSet (s );
124238 if (s != null ) {
239+ // if not null the Sample has not been taken by the application thread so dispose
125240 s .dispose ();
126241 }
127242 Platform .runLater (() -> updateImage ());
128243 return FlowReturn .OK ;
129244 }
130-
245+
131246 }
132-
247+
133248 private class NewPrerollListener implements AppSink .NEW_PREROLL {
134249
135250 @ Override
136251 public FlowReturn newPreroll (AppSink appsink ) {
137252 Sample s = appsink .pullPreroll ();
138253 s = pending .getAndSet (s );
139254 if (s != null ) {
255+ // if not null the Sample has not been taken by the application thread so dispose
140256 s .dispose ();
141257 }
142258 Platform .runLater (() -> updateImage ());
143259 return FlowReturn .OK ;
144260 }
145-
261+
146262 }
147-
263+
148264}
0 commit comments