001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 *
032 */
033package org.openimaj.video.xuggle;
034
035import java.awt.image.BufferedImage;
036import java.io.DataInput;
037import java.io.DataInputStream;
038import java.io.File;
039import java.io.IOException;
040import java.io.InputStream;
041import java.net.MalformedURLException;
042import java.net.URL;
043import java.util.concurrent.atomic.AtomicReference;
044
045import org.apache.log4j.Logger;
046import org.openimaj.image.ImageUtilities;
047import org.openimaj.image.MBFImage;
048import org.openimaj.image.colour.ColourSpace;
049import org.openimaj.video.Video;
050import org.openimaj.video.VideoDisplay;
051import org.openimaj.video.timecode.HrsMinSecFrameTimecode;
052import org.openimaj.video.timecode.VideoTimecode;
053
054import com.xuggle.ferry.JNIReference;
055import com.xuggle.mediatool.IMediaReader;
056import com.xuggle.mediatool.MediaListenerAdapter;
057import com.xuggle.mediatool.ToolFactory;
058import com.xuggle.mediatool.event.IVideoPictureEvent;
059import com.xuggle.xuggler.Global;
060import com.xuggle.xuggler.ICodec;
061import com.xuggle.xuggler.IContainer;
062import com.xuggle.xuggler.IError;
063import com.xuggle.xuggler.IPixelFormat;
064import com.xuggle.xuggler.IStream;
065import com.xuggle.xuggler.IVideoPicture;
066import com.xuggle.xuggler.io.URLProtocolManager;
067import com.xuggle.xuggler.video.AConverter;
068import com.xuggle.xuggler.video.BgrConverter;
069import com.xuggle.xuggler.video.ConverterFactory;
070
071/**
072 * Wraps a Xuggle video reader into the OpenIMAJ {@link Video} interface.
073 * <p>
074 * <b>Some Notes:</b>
075 * <p>
076 * The {@link #hasNextFrame()} method must attempt to read the next packet in
077 * the stream to determine if there is a next frame. That means that it incurs a
078 * time penalty. It also means there's various logic in that method and the
079 * {@link #getNextFrame()} method to avoid reading frames that have already been
080 * read. It also means that, to avoid {@link #getCurrentFrame()} incorrectly
081 * returning a new frame after {@link #hasNextFrame()} has been called, the
082 * class may be holding two frames (the current frame and the next frame) after
083 * {@link #hasNextFrame()} has been called.
084 * <p>
085 * The constructors have signatures that allow the passing of a boolean that
086 * determines whether the video is looped or not. This has a different effect
087 * than looping using the {@link VideoDisplay}. When the video is set to loop it
088 * will loop indefinitely and the timestamp of frames will be consecutive. That
089 * is, when the video loops the timestamps will continue to increase. This is in
090 * contrast to setting the {@link VideoDisplay} end action (using
091 * {@link VideoDisplay#setEndAction(org.openimaj.video.VideoDisplay.EndAction)}
092 * where the looping will reset all timestamps when the video loops.
093 * 
094 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
095 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
096 * @author Sina Samangooei (ss@ecs.soton.ac.uk)
097 * 
098 * @created 1 Jun 2011
099 */
100public class XuggleVideo extends Video<MBFImage>
101{
102        private final static Logger logger = Logger.getLogger(XuggleVideo.class);
103
104        static {
105                // This allows us to read videos from jar: urls
106                URLProtocolManager.getManager().registerFactory("jar", new JarURLProtocolHandlerFactory());
107
108                // This converter converts the frames into MBFImages for us
109                ConverterFactory.registerConverter(new ConverterFactory.Type(
110                                ConverterFactory.XUGGLER_BGR_24, MBFImageConverter.class,
111                                IPixelFormat.Type.BGR24, BufferedImage.TYPE_3BYTE_BGR));
112        }
113
114        /** The reader used to read the video */
115        private IMediaReader reader = null;
116
117        /** Used to tell, when reading packets, if we got enough for a new frame */
118        private boolean currentFrameUpdated = false;
119
120        /** The current frame - only ever one object that's reused */
121        private MBFImage currentMBFImage;
122
123        /** Whether the current frame is a key frame or not */
124        private boolean currentFrameIsKeyFrame = false;
125
126        /** The stream index that we'll be reading from */
127        private int streamIndex = -1;
128
129        /** Width of the video frame */
130        private int width = -1;
131
132        /** Height of the video frame */
133        private int height = -1;
134
135        /** A cache of the calculation of he total number of frames in the video */
136        private long totalFrames = -1;
137
138        /** A cache of the url of the video */
139        private final String url;
140
141        /** A cache of whether the video should be looped or not */
142        private final boolean loop;
143
144        /** The timestamp of the frame currently being decoded */
145        private long timestamp;
146
147        /** The offset to add to all timestamps (used for looping) */
148        private long timestampOffset = 0;
149
150        /** The number of frames per second */
151        private double fps;
152
153        /** The next frame in the stream */
154        private MBFImage nextFrame = null;
155
156        /** The timestamp of the next frame */
157        public long nextFrameTimestamp = 0;
158
159        /** Whether the next frame is a key frame or not */
160        public boolean nextFrameIsKeyFrame = false;
161
162        /**
163         * This implements the Xuggle MediaTool listener that will be called every
164         * time a video picture has been decoded from the stream. This class creates
165         * a BufferedImage for each video frame and updates the currentFrameUpdated
166         * boolean when one arrives.
167         * 
168         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
169         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
170         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
171         * 
172         * @created 1 Jun 2011
173         */
174        protected class FrameGetter extends MediaListenerAdapter
175        {
176                /**
177                 * {@inheritDoc}
178                 * 
179                 * @see com.xuggle.mediatool.MediaToolAdapter#onVideoPicture(com.xuggle.mediatool.event.IVideoPictureEvent)
180                 */
181                @Override
182                public void onVideoPicture(final IVideoPictureEvent event)
183                {
184                        // event.getPicture().getTimeStamp();
185                        if (event.getStreamIndex() == XuggleVideo.this.streamIndex)
186                        {
187                                XuggleVideo.this.currentMBFImage = ((MBFImageWrapper) event.getImage()).img;
188                                XuggleVideo.this.currentFrameIsKeyFrame = event.getMediaData().isKeyFrame();
189                                XuggleVideo.this.timestamp = (long) ((event.getPicture().getTimeStamp()
190                                                * event.getPicture().getTimeBase().getDouble()) * 1000)
191                                                + XuggleVideo.this.timestampOffset;
192                                XuggleVideo.this.currentFrameUpdated = true;
193                        }
194                }
195        }
196
197        /**
198         * Wrapper that created an MBFImage from a BufferedImage.
199         * 
200         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
201         * 
202         * @created 1 Nov 2011
203         */
204        protected static final class MBFImageWrapper extends BufferedImage
205        {
206                MBFImage img;
207
208                public MBFImageWrapper(final MBFImage img)
209                {
210                        super(1, 1, BufferedImage.TYPE_INT_RGB);
211                        this.img = img;
212                }
213        }
214
215        /**
216         * Converter for converting IVideoPictures directly to MBFImages.
217         * 
218         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
219         * 
220         * @created 1 Nov 2011
221         */
222        protected static final class MBFImageConverter extends BgrConverter
223        {
224                private final MBFImageWrapper bimg = new MBFImageWrapper(null);
225                private final byte[] buffer;
226
227                public MBFImageConverter(
228                                final IPixelFormat.Type pictureType, final int pictureWidth,
229                                final int pictureHeight, final int imageWidth, final int imageHeight)
230                {
231                        super(pictureType, pictureWidth, pictureHeight, imageWidth, imageHeight);
232
233                        this.bimg.img = new MBFImage(imageWidth, imageHeight, ColourSpace.RGB);
234                        this.buffer = new byte[imageWidth * imageHeight * 3];
235                }
236
237                @Override
238                public BufferedImage toImage(IVideoPicture picture) {
239                        // test that the picture is valid
240                        this.validatePicture(picture);
241
242                        // resample as needed
243                        IVideoPicture resamplePicture = null;
244                        final AtomicReference<JNIReference> ref = new AtomicReference<JNIReference>(null);
245                        try
246                        {
247                                if (this.willResample())
248                                {
249                                        resamplePicture = AConverter.resample(picture, this.mToImageResampler);
250                                        picture = resamplePicture;
251                                }
252
253                                // get picture parameters
254                                final int w = picture.getWidth();
255                                final int h = picture.getHeight();
256
257                                final float[][] r = this.bimg.img.bands.get(0).pixels;
258                                final float[][] g = this.bimg.img.bands.get(1).pixels;
259                                final float[][] b = this.bimg.img.bands.get(2).pixels;
260
261                                picture.getDataCached().get(0, this.buffer, 0, this.buffer.length);
262                                for (int y = 0, i = 0; y < h; y++) {
263                                        for (int x = 0; x < w; x++, i += 3) {
264                                                b[y][x] = ImageUtilities.BYTE_TO_FLOAT_LUT[(this.buffer[i] & 0xFF)];
265                                                g[y][x] = ImageUtilities.BYTE_TO_FLOAT_LUT[(this.buffer[i + 1] & 0xFF)];
266                                                r[y][x] = ImageUtilities.BYTE_TO_FLOAT_LUT[(this.buffer[i + 2] & 0xFF)];
267                                        }
268                                }
269
270                                return this.bimg;
271                        } finally {
272                                if (resamplePicture != null)
273                                        resamplePicture.delete();
274                                if (ref.get() != null)
275                                        ref.get().delete();
276                        }
277                }
278        }
279
280        /**
281         * Default constructor that takes the video file to read.
282         * 
283         * @param videoFile
284         *            The video file to read.
285         */
286        public XuggleVideo(final File videoFile)
287        {
288                this(videoFile.toURI().toString());
289        }
290
291        /**
292         * Default constructor that takes the video file to read.
293         * 
294         * @param videoFile
295         *            The video file to read.
296         * @param loop
297         *            should the video loop
298         */
299        public XuggleVideo(final File videoFile, final boolean loop)
300        {
301                this(videoFile.toURI().toString(), loop);
302        }
303
304        /**
305         * Default constructor that takes the location of a video file to read. This
306         * can either be a filename or a URL.
307         * 
308         * @param url
309         *            The URL of the file to read
310         */
311        public XuggleVideo(final String url)
312        {
313                this(url, false);
314        }
315
316        /**
317         * Default constructor that takes the URL of a video file to read.
318         * 
319         * @param url
320         *            The URL of the file to read
321         */
322        public XuggleVideo(final URL url)
323        {
324                this(url.toString(), false);
325        }
326
327        /**
328         * Default constructor that takes the location of a video file to read. This
329         * can either be a filename or a URL. The second parameter determines
330         * whether the video will loop indefinitely. If so, {@link #getNextFrame()}
331         * will never return null; otherwise this method will return null at the end
332         * of the video.
333         * 
334         * @param url
335         *            The URL of the file to read
336         * @param loop
337         *            Whether to loop the video indefinitely
338         */
339        public XuggleVideo(final URL url, final boolean loop)
340        {
341                this(url.toString(), loop);
342        }
343
344        /**
345         * Default constructor that takes the location of a video file to read. This
346         * can either be a filename or a URL. The second parameter determines
347         * whether the video will loop indefinitely. If so, {@link #getNextFrame()}
348         * will never return null; otherwise this method will return null at the end
349         * of the video.
350         * 
351         * @param url
352         *            The URL of the file to read
353         * @param loop
354         *            Whether to loop the video indefinitely
355         */
356        public XuggleVideo(final String url, final boolean loop)
357        {
358                this.url = url;
359                this.loop = loop;
360                this.create(url);
361        }
362
363        /**
364         * Default constructor that takes an input stream. Note that only
365         * "streamable" video codecs can be used in this way.
366         * 
367         * @param stream
368         *            The video data stream
369         */
370        public XuggleVideo(final InputStream stream)
371        {
372                this.url = null;
373                this.loop = false;
374                this.create(stream);
375        }
376
377        /**
378         * Default constructor that takes a data input. Note that only "streamable"
379         * video codecs can be used in this way.
380         * 
381         * @param input
382         *            The video data
383         */
384        public XuggleVideo(final DataInput input)
385        {
386                this.url = null;
387                this.loop = false;
388                this.create(input);
389        }
390
391        /**
392         * {@inheritDoc}
393         * 
394         * @see org.openimaj.video.Video#countFrames()
395         */
396        @Override
397        public long countFrames()
398        {
399                return this.totalFrames;
400        }
401
402        /**
403         * {@inheritDoc}
404         * 
405         * @see org.openimaj.video.Video#getNextFrame()
406         */
407        @Override
408        public MBFImage getNextFrame()
409        {
410                if (this.nextFrame != null)
411                {
412                        // We've already read the next frame, so we simply move on.
413                        this.currentMBFImage = this.nextFrame;
414                        this.timestamp = this.nextFrameTimestamp;
415                        this.currentFrameIsKeyFrame = this.nextFrameIsKeyFrame;
416                        this.nextFrame = null;
417                }
418                else
419                {
420                        // Read a frame from the stream.
421                        this.currentMBFImage = this.readFrame(false);
422                }
423
424                if (this.currentMBFImage != null)
425                {
426                        // Increment frame counter
427                        this.currentFrame++;
428                }
429
430                return this.currentMBFImage;
431        }
432
433        /**
434         * Reads a frame from the stream, or returns null if no frame could be read.
435         * If preserveCurrent is true, then the frame is read into the nextFrame
436         * member rather than the currentMBFImage member and the nextFrame is
437         * returned (while currentMBFImage will still contain the previous frame).
438         * Note that if preserveCurrent is true, it will invoke a copy between
439         * images. If preserveCurrent is false and nextFrame is set, this method may
440         * have unexpected results as it does not swap current and next back. See
441         * {@link #getNextFrame()} which swaps back when a frame has been pre-read
442         * from the stream.
443         * 
444         * @param preserveCurrent
445         *            Whether to preserve the current frame
446         * @return The frame that was read, or NULL if no frame could be read.
447         */
448        synchronized private MBFImage readFrame(final boolean preserveCurrent)
449        {
450                // System.out.println( "readFrame( "+preserveCurrent+" )");
451
452                if (this.reader == null)
453                        return null;
454
455                // If we need to preserve the current frame, we need to copy the frame
456                // because the readPacket() will cause the frame to be overwritten
457                final long currentTimestamp = this.timestamp;
458                final boolean currentKeyFrameFlag = this.currentFrameIsKeyFrame;
459                if (preserveCurrent && this.nextFrame == null)
460                {
461                        // We make a copy of the current image and set the current image
462                        // to point to that (thereby preserving it). We then set the next
463                        // frame image to point to the buffer that the readPacket() will
464                        // fill.
465                        if (this.currentMBFImage != null)
466                        {
467                                final MBFImage tmp = this.currentMBFImage.clone();
468                                this.nextFrame = this.currentMBFImage;
469                                this.currentMBFImage = tmp;
470                        }
471                }
472                // If nextFrame wasn't null, we can just write into it as must be
473                // pointing to the current frame buffer
474
475                IError e = null;
476                boolean tryAgain = false;
477                do
478                {
479                        tryAgain = false;
480
481                        // Read packets until we have a new frame.
482                        while ((e = this.reader.readPacket()) == null && !this.currentFrameUpdated)
483                                ;
484
485                        if (e != null && e.getType() == IError.Type.ERROR_EOF && this.loop)
486                        {
487                                // We're looping, so we update the timestamp offset.
488                                this.timestampOffset += (this.timestamp - this.timestampOffset);
489                                tryAgain = true;
490                                this.seekToBeginning();
491                        }
492                } while (tryAgain);
493
494                // Check if we're at the end of the file
495                if (!this.currentFrameUpdated || e != null)
496                {
497                        // Logger.error( "Got video demux error: "+e.getType() );
498                        return null;
499                }
500
501                // We've read a frame so we're done looping
502                this.currentFrameUpdated = false;
503
504                if (preserveCurrent)
505                {
506                        // Swap the current values into the next-frame values
507                        this.nextFrameIsKeyFrame = this.currentFrameIsKeyFrame;
508                        this.currentFrameIsKeyFrame = currentKeyFrameFlag;
509                        this.nextFrameTimestamp = this.timestamp;
510                        this.timestamp = currentTimestamp;
511
512                        // Return the next frame
513                        if (this.nextFrame != null)
514                                return this.nextFrame;
515                        return this.currentMBFImage;
516                }
517                // Not preserving anything, so just return the frame
518                else
519                        return this.currentMBFImage;
520        }
521
522        /**
523         * Returns a video timecode for the current frame.
524         * 
525         * @return A video timecode for the current frame.
526         */
527        public VideoTimecode getCurrentTimecode()
528        {
529                return new HrsMinSecFrameTimecode((long) (this.timestamp / 1000d * this.fps), this.fps);
530        }
531
532        /**
533         * {@inheritDoc}
534         * 
535         * @see org.openimaj.video.Video#getCurrentFrame()
536         */
537        @Override
538        public MBFImage getCurrentFrame()
539        {
540                if (this.currentMBFImage == null)
541                        this.currentMBFImage = this.getNextFrame();
542                return this.currentMBFImage;
543        }
544
545        /**
546         * {@inheritDoc}
547         * 
548         * @see org.openimaj.video.Video#getWidth()
549         */
550        @Override
551        public int getWidth()
552        {
553                return this.width;
554        }
555
556        /**
557         * {@inheritDoc}
558         * 
559         * @see org.openimaj.video.Video#getHeight()
560         */
561        @Override
562        public int getHeight()
563        {
564                return this.height;
565        }
566
567        /**
568         * {@inheritDoc}
569         * 
570         * @see org.openimaj.video.Video#hasNextFrame()
571         */
572        @Override
573        public boolean hasNextFrame()
574        {
575                if (this.nextFrame == null)
576                {
577                        this.nextFrame = this.readFrame(true);
578                        return this.nextFrame != null;
579                }
580                else
581                        return true;
582        }
583
584        /**
585         * {@inheritDoc}
586         * <p>
587         * Note: if you created the video from a {@link DataInput} or
588         * {@link InputStream}, there is no way that it can be reset.
589         * 
590         * @see org.openimaj.video.Video#reset()
591         */
592        @Override
593        synchronized public void reset()
594        {
595                if (this.reader == null) {
596                        if (this.url == null)
597                                return;
598
599                        this.create(url);
600                } else {
601                        this.seekToBeginning();
602                }
603        }
604
605        /**
606         * This is a convenience method that will seek the stream to be the
607         * beginning. As the seek method seems a bit flakey in some codec containers
608         * in Xuggle, we'll try and use a few different methods to get us back to
609         * the beginning. That means that this method may be slower than seek(0) if
610         * it needs to try multiple methods.
611         * <p>
612         * Note: if you created the video from a {@link DataInput} or
613         * {@link InputStream}, there is no way that it can be reset.
614         */
615        synchronized public void seekToBeginning()
616        {
617                // if the video came from a stream, there is no chance of returning!
618                if (this.url == null)
619                        return;
620
621                // Try to seek to byte 0. That's the start of the file.
622                this.reader.getContainer().seekKeyFrame(this.streamIndex,
623                                0, 0, 0, IContainer.SEEK_FLAG_BYTE);
624
625                // Got to the beginning? We're done.
626                if (this.timestamp == 0)
627                        return;
628
629                // Try to seek to key frame at timestamp 0.
630                this.reader.getContainer().seekKeyFrame(this.streamIndex,
631                                0, 0, 0, IContainer.SEEK_FLAG_FRAME);
632
633                // Got to the beginning? We're done.
634                if (this.timestamp == 0)
635                        return;
636
637                // Try to seek backwards to timestamp 0.
638                this.reader.getContainer().seekKeyFrame(this.streamIndex,
639                                0, 0, 0, IContainer.SEEK_FLAG_BACKWARDS);
640
641                // Got to the beginning? We're done.
642                if (this.timestamp == 0)
643                        return;
644
645                // Try to seek to timestamp 0 any way possible.
646                this.reader.getContainer().seekKeyFrame(this.streamIndex,
647                                0, 0, 0, IContainer.SEEK_FLAG_ANY);
648
649                // Got to the beginning? We're done.
650                if (this.timestamp == 0)
651                        return;
652
653                // We're really struggling to get this container back to the start.
654                // So, try recreating the whole reader again.
655                this.reader.close();
656                this.reader = null;
657                this.create(url);
658
659                this.getNextFrame();
660
661                // We tried everything. It's either worked or it hasn't.
662                return;
663        }
664
665        /**
666         * Create the necessary reader
667         */
668        synchronized private void create(String urlstring)
669        {
670                setupReader();
671
672                // Check whether the string we have is a valid URI
673                IContainer container = null;
674                int openResult = 0;
675                try
676                {
677                        // If it's a valid URI, we'll try to open the container using the
678                        // URI string.
679                        container = IContainer.make();
680                        openResult = container.open(urlstring, IContainer.Type.READ, null, true, true);
681
682                        // If there was an error trying to open the container in this way,
683                        // it may be that we have a resource URL (which ffmpeg doesn't
684                        // understand), so we'll try opening an InputStream to the resource.
685                        if (openResult < 0)
686                        {
687                                logger.info("URL " + urlstring + " could not be opened by ffmpeg. " +
688                                                "Trying to open a stream to the URL instead.");
689                                final InputStream is = new DataInputStream(new URL(urlstring).openStream());
690                                openResult = container.open(is, null, true, true);
691
692                                if (openResult < 0)
693                                {
694                                        logger.error("Error opening container. Error " + openResult +
695                                                        " (" + IError.errorNumberToType(openResult).toString() + ")");
696                                        return;
697                                }
698                        }
699                } catch (final MalformedURLException e) {
700                        e.printStackTrace();
701                        return;
702                } catch (final IOException e) {
703                        e.printStackTrace();
704                        return;
705                }
706
707                setupReader(container);
708        }
709
710        /**
711         * Create the necessary reader
712         */
713        synchronized private void create(InputStream stream)
714        {
715                setupReader();
716
717                // Check whether the string we have is a valid URI
718                final IContainer container = IContainer.make();
719                final int openResult = container.open(stream, null, true, true);
720
721                if (openResult < 0)
722                {
723                        logger.error("Error opening container. Error " + openResult +
724                                        " (" + IError.errorNumberToType(openResult).toString() + ")");
725                        return;
726                }
727
728                setupReader(container);
729        }
730
731        /**
732         * Create the necessary reader
733         */
734        synchronized private void create(DataInput input)
735        {
736                setupReader();
737
738                // Check whether the string we have is a valid URI
739                final IContainer container = IContainer.make();
740                final int openResult = container.open(input, null, true, true);
741
742                if (openResult < 0)
743                {
744                        logger.error("Error opening container. Error " + openResult +
745                                        " (" + IError.errorNumberToType(openResult).toString() + ")");
746                        return;
747                }
748
749                setupReader(container);
750        }
751
752        private void setupReader() {
753                // Assume we'll start at the beginning again
754                this.currentFrame = 0;
755
756                // If the reader is already open, we'll close it first and
757                // reinstantiate it.
758                if (this.reader != null && this.reader.isOpen())
759                {
760                        this.reader.close();
761                        this.reader = null;
762                }
763        }
764
765        private void setupReader(IContainer container) {
766                // Set up a new reader using the container that reads the images.
767                this.reader = ToolFactory.makeReader(container);
768                this.reader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);
769                this.reader.addListener(new FrameGetter());
770
771                // Find the video stream.
772                IStream s = null;
773                int i = 0;
774                while (i < container.getNumStreams())
775                {
776                        s = container.getStream(i);
777                        if (s != null && s.getStreamCoder().getCodecType() ==
778                                        ICodec.Type.CODEC_TYPE_VIDEO)
779                        {
780                                // Save the stream index so that we only get frames from
781                                // this stream in the FrameGetter
782                                this.streamIndex = i;
783                                break;
784                        }
785                        i++;
786                }
787
788                if (container.getDuration() == Global.NO_PTS)
789                        this.totalFrames = -1;
790                else
791                        this.totalFrames = (long) (s.getDuration() *
792                                        s.getTimeBase().getDouble() * s.getFrameRate().getDouble());
793
794                // If we found the video stream, set the FPS
795                if (s != null)
796                        this.fps = s.getFrameRate().getDouble();
797
798                // If we found a video stream, setup the MBFImage buffer.
799                if (s != null)
800                {
801                        final int w = s.getStreamCoder().getWidth();
802                        final int h = s.getStreamCoder().getHeight();
803                        this.width = w;
804                        this.height = h;
805                }
806        }
807
808        /**
809         * {@inheritDoc}
810         * 
811         * @see org.openimaj.video.Video#getTimeStamp()
812         */
813        @Override
814        public long getTimeStamp()
815        {
816                return this.timestamp;
817        }
818
819        /**
820         * {@inheritDoc}
821         * 
822         * @see org.openimaj.video.Video#getFPS()
823         */
824        @Override
825        public double getFPS()
826        {
827                return this.fps;
828        }
829
830        /**
831         * {@inheritDoc}
832         * 
833         * @see org.openimaj.video.Video#getCurrentFrameIndex()
834         */
835        @Override
836        public synchronized int getCurrentFrameIndex()
837        {
838                return (int) (this.timestamp / 1000d * this.fps);
839        }
840
841        /**
842         * {@inheritDoc}
843         * 
844         * @see org.openimaj.video.Video#setCurrentFrameIndex(long)
845         */
846        @Override
847        public void setCurrentFrameIndex(final long newFrame)
848        {
849                this.seekPrecise(newFrame / this.fps);
850        }
851
852        /**
853         * Implements a precise seeking mechanism based on the Xuggle seek method
854         * and the naive seek method which simply reads frames.
855         * <p>
856         * Note: if you created the video from a {@link DataInput} or
857         * {@link InputStream}, you can only seek forwards.
858         * 
859         * @param timestamp
860         *            The timestamp to get, in seconds.
861         */
862        public void seekPrecise(double timestamp)
863        {
864                // Use the Xuggle seek method first to get near the frame
865                this.seek(timestamp);
866
867                // The timestamp field is in milliseconds, so we need to * 1000 to
868                // compare
869                timestamp *= 1000;
870
871                // Work out the number of milliseconds per frame
872                final double timePerFrame = 1000d / this.fps;
873
874                // If we're not in the right place, keep reading until we are.
875                // Note the right place is the frame before the timestamp we're given:
876                // |---frame 1---|---frame2---|---frame3---|
877                // ^- given timestamp
878                // ... so we should show frame2 not frame3.
879                while (this.timestamp <= timestamp - timePerFrame && this.getNextFrame() != null)
880                        ;
881        }
882
883        /**
884         * {@inheritDoc}
885         * <p>
886         * Note: if you created the video from a {@link DataInput} or
887         * {@link InputStream}, you can only seek forwards.
888         * 
889         * @see org.openimaj.video.Video#seek(double)
890         */
891        @Override
892        synchronized public void seek(final double timestamp)
893        {
894                // Based on the code of this class:
895                // http://www.google.com/codesearch#DzBPmFOZfmA/trunk/0.5/unstable/videoplayer/src/classes/org/jdesktop/wonderland/modules/videoplayer/client/VideoPlayerImpl.java&q=seekKeyFrame%20position&type=cs
896                // using the timebase, calculate the time in timebase units requested
897                // Check we've actually got a container
898                if (this.reader == null) {
899                        if (this.url == null)
900                                return;
901
902                        this.create(url);
903                }
904
905                // Convert between milliseconds and stream timestamps
906                final double timebase = this.reader.getContainer().getStream(
907                                this.streamIndex).getTimeBase().getDouble();
908                final long position = (long) (timestamp / timebase);
909
910                final long min = Math.max(0, position - 100);
911                final long max = position;
912
913                final int ret = this.reader.getContainer().seekKeyFrame(this.streamIndex,
914                                min, position, max, IContainer.SEEK_FLAG_ANY);
915
916                if (ret >= 0)
917                        this.getNextFrame();
918                else
919                        logger.error("Seek returned an error value: " + ret + ": "
920                                        + IError.errorNumberToType(ret));
921        }
922
923        /**
924         * Returns the duration of the video in seconds.
925         * 
926         * @return The duraction of the video in seconds.
927         */
928        public synchronized long getDuration()
929        {
930                final long duration = (this.reader.getContainer().
931                                getStream(this.streamIndex).getDuration());
932                final double timebase = this.reader.getContainer().
933                                getStream(this.streamIndex).getTimeBase().getDouble();
934
935                return (long) (duration * timebase);
936        }
937
938        /**
939         * {@inheritDoc}
940         * 
941         * @see org.openimaj.video.Video#close()
942         */
943        @Override
944        public synchronized void close()
945        {
946                if (this.reader != null)
947                {
948                        synchronized (this.reader)
949                        {
950                                if (this.reader.isOpen())
951                                {
952                                        this.reader.close();
953                                        this.reader = null;
954                                }
955                        }
956                }
957        }
958}