001package com.github.sarxos.webcam.ds.jmf;
002
003import java.awt.Dimension;
004import java.awt.Graphics2D;
005import java.awt.Image;
006import java.awt.image.BufferedImage;
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.Comparator;
010import java.util.List;
011import java.util.concurrent.Semaphore;
012
013import javax.media.Buffer;
014import javax.media.CaptureDeviceInfo;
015import javax.media.Control;
016import javax.media.Controller;
017import javax.media.ControllerEvent;
018import javax.media.ControllerListener;
019import javax.media.Format;
020import javax.media.Manager;
021import javax.media.MediaLocator;
022import javax.media.Player;
023import javax.media.ResourceUnavailableEvent;
024import javax.media.StartEvent;
025import javax.media.control.FormatControl;
026import javax.media.control.FrameGrabbingControl;
027import javax.media.format.VideoFormat;
028import javax.media.util.BufferToImage;
029
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033import com.github.sarxos.webcam.WebcamDevice;
034
035
036/**
037 * Webcam device - JMF and FMJ implementation.
038 * 
039 * @author Bartosz Firyn (SarXos)
040 */
041public class JmfDevice implements WebcamDevice {
042
043        private static final Logger LOG = LoggerFactory.getLogger(JmfDevice.class);
044
045        /**
046         * Control to control format.
047         */
048        private final static String FORMAT_CTRL = "javax.media.control.FormatControl";
049
050        /**
051         * Control to grab frames.
052         */
053        private final static String GRABBING_CTRL = "javax.media.control.FrameGrabbingControl";
054
055        /**
056         * Player starter.
057         */
058        private class PlayerStarter extends Thread implements ControllerListener {
059
060                public PlayerStarter(Player player) {
061                        setDaemon(true);
062                        player.addControllerListener(this);
063                        player.start();
064                }
065
066                @Override
067                public void run() {
068
069                        // wait for start
070                        while (!started) {
071                                try {
072                                        Thread.sleep(10);
073                                } catch (InterruptedException e) {
074                                        e.printStackTrace();
075                                }
076                                started = player.getState() == Controller.Started;
077                                if (started) {
078                                        break;
079                                }
080                        }
081
082                        // try to grab single image (wait 10 seconds)
083                        for (int i = 0; i < 100; i++) {
084                                Buffer buffer = grabber.grabFrame();
085                                if (buffer.getLength() == 0) {
086                                        try {
087                                                Thread.sleep(100);
088                                        } catch (InterruptedException e) {
089                                                e.printStackTrace();
090                                        }
091                                }
092                        }
093
094                        semaphore.release();
095                }
096
097                @Override
098                public void controllerUpdate(ControllerEvent ce) {
099                        if (ce instanceof StartEvent) {
100                                available = true;
101                                semaphore.release();
102                        }
103                        if (ce instanceof ResourceUnavailableEvent) {
104                                available = false;
105                                semaphore.release();
106                        }
107                }
108        }
109
110        /**
111         * Is webcam open.
112         */
113        private volatile boolean open = false;
114
115        /**
116         * Is player available.
117         */
118        private volatile boolean available = false;
119
120        /**
121         * Is player started.
122         */
123        private volatile boolean started = false;
124
125        private volatile boolean disposed = false;
126
127        private PlayerStarter starter = null;
128        private Semaphore semaphore = new Semaphore(0, true);
129
130        private CaptureDeviceInfo cdi = null;
131        private List<Dimension> dimensions = null;
132        private Dimension dimension = null;
133
134        private CaptureDeviceInfo device = null;
135        private MediaLocator locator = null;
136        private Player player = null;
137        private VideoFormat format = null;
138        private FormatControl control = null;
139        private FrameGrabbingControl grabber = null;
140        private BufferToImage converter = null;
141
142        private Dimension viewSize = null;
143
144        public JmfDevice(CaptureDeviceInfo cdi) {
145                this.cdi = cdi;
146        }
147
148        @Override
149        public String getName() {
150                return cdi.getName();
151        }
152
153        private VideoFormat createFormat(Dimension size) {
154                if (size == null) {
155                        return getLargestVideoFormat();
156                } else {
157                        return getSizedVideoFormat(size);
158                }
159        }
160
161        /**
162         * Get player control.
163         * 
164         * @param name control name
165         * @return Player control
166         */
167        private Control getControl(String name) {
168                Control control = player.getControl(name);
169                if (control == null) {
170                        throw new RuntimeException("Cannot find format control " + name);
171                }
172                return control;
173        }
174
175        /**
176         * Get video format for size.
177         * 
178         * @param device device to get format from
179         * @param size specific size to search
180         * @return VideoFormat
181         */
182        private VideoFormat getSizedVideoFormat(Dimension size) {
183
184                Format[] formats = device.getFormats();
185                VideoFormat format = null;
186
187                for (Format f : formats) {
188
189                        if (!"RGB".equalsIgnoreCase(f.getEncoding()) || !(f instanceof VideoFormat)) {
190                                continue;
191                        }
192
193                        Dimension d = ((VideoFormat) f).getSize();
194                        if (d.width == size.width && d.height == size.height) {
195                                format = (VideoFormat) f;
196                                break;
197                        }
198                }
199
200                return format;
201        }
202
203        /**
204         * Get suitable video format to use (the largest one by default, but this
205         * can be easily changed).
206         * 
207         * @param device device to get video format for
208         * @return Suitable video format
209         */
210        private VideoFormat getLargestVideoFormat() {
211
212                Format[] formats = device.getFormats();
213                VideoFormat format = null;
214                int area = 0;
215
216                // find the largest picture format
217                for (Format f : formats) {
218
219                        if (!(f instanceof VideoFormat) || !"RGB".equalsIgnoreCase(f.getEncoding())) {
220                                continue;
221                        }
222
223                        VideoFormat vf = (VideoFormat) f;
224                        Dimension dim = vf.getSize();
225
226                        int a = dim.width * dim.height;
227                        if (a > area) {
228                                area = a;
229                                format = vf;
230                        }
231                }
232
233                return format;
234        }
235
236        @Override
237        public Dimension[] getResolutions() {
238
239                if (dimensions == null) {
240                        dimensions = new ArrayList<Dimension>();
241
242                        Format[] formats = cdi.getFormats();
243                        for (Format format : formats) {
244                                if ("RGB".equalsIgnoreCase(format.getEncoding())) {
245                                        dimensions.add(((VideoFormat) format).getSize());
246                                }
247                        }
248
249                        Collections.sort(dimensions, new Comparator<Dimension>() {
250
251                                @Override
252                                public int compare(Dimension a, Dimension b) {
253                                        int apx = a.width * a.height;
254                                        int bpx = b.width * b.height;
255                                        if (apx > bpx) {
256                                                return 1;
257                                        } else if (apx < bpx) {
258                                                return -1;
259                                        } else {
260                                                return 0;
261                                        }
262                                }
263                        });
264                }
265
266                return dimensions.toArray(new Dimension[dimensions.size()]);
267        }
268
269        @Override
270        public Dimension getResolution() {
271                return dimension;
272        }
273
274        @Override
275        public void setResolution(Dimension size) {
276                this.dimension = size;
277        }
278
279        @Override
280        public BufferedImage getImage() {
281
282                if (!open) {
283                        throw new RuntimeException("Webcam has to be open to get image");
284                }
285
286                Buffer buffer = grabber.grabFrame();
287                Image image = converter.createImage(buffer);
288
289                if (image == null) {
290                        throw new RuntimeException("Cannot get image");
291                }
292
293                int width = image.getWidth(null);
294                int height = image.getHeight(null);
295                int type = BufferedImage.TYPE_INT_RGB;
296
297                BufferedImage buffered = new BufferedImage(width, height, type);
298
299                Graphics2D g2 = buffered.createGraphics();
300                g2.drawImage(image, null, null);
301                g2.dispose();
302                buffered.flush();
303
304                return buffered;
305        }
306
307        @Override
308        public void open() {
309
310                if (disposed) {
311                        LOG.warn("Cannot open device because it's already disposed");
312                        return;
313                }
314
315                if (open) {
316                        LOG.debug("Opening webcam - already open!");
317                        return;
318                }
319
320                LOG.debug("Opening webcam");
321
322                locator = device.getLocator();
323                format = createFormat(viewSize);
324                converter = new BufferToImage(format);
325
326                try {
327                        player = Manager.createRealizedPlayer(locator);
328                } catch (Exception e) {
329                        throw new RuntimeException(e);
330                }
331
332                control = (FormatControl) getControl(FORMAT_CTRL);
333                grabber = (FrameGrabbingControl) getControl(GRABBING_CTRL);
334
335                if (control.setFormat(format) == null) {
336                        throw new RuntimeException("Cannot change video format");
337                }
338
339                starter = new PlayerStarter(player);
340                starter.start();
341
342                try {
343                        semaphore.acquire(2);
344                        starter.join();
345                } catch (InterruptedException e) {
346                }
347
348                do {
349                        BufferedImage image = getImage();
350                        if (image != null) {
351                                break;
352                        }
353                        try {
354                                Thread.sleep(100);
355                        } catch (InterruptedException e) {
356                        }
357                } while (true);
358
359                open = started && available;
360        }
361
362        @Override
363        public void close() {
364
365                if (!started && !available && !open) {
366                        LOG.debug("Cannot close webcam");
367                        return;
368                }
369
370                LOG.debug("Closing webcam");
371
372                open = false;
373
374                if (started || available) {
375
376                        started = false;
377                        available = false;
378
379                        player.stop();
380                        player.close();
381                        player.deallocate();
382                }
383        }
384
385        @Override
386        public void dispose() {
387                disposed = true;
388        }
389
390        @Override
391        public boolean isOpen() {
392                return open;
393        }
394
395}