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        private final static Logger logger = Logger.getLogger(XuggleVideo.class);
102
103        static {
104                // This allows us to read videos from jar: urls
105                URLProtocolManager.getManager().registerFactory("jar", new JarURLProtocolHandlerFactory());
106
107                // This converter converts the frames into MBFImages for us
108                ConverterFactory.registerConverter(new ConverterFactory.Type(
109                                ConverterFactory.XUGGLER_BGR_24, MBFImageConverter.class,
110                                IPixelFormat.Type.BGR24, BufferedImage.TYPE_3BYTE_BGR));
111        }
112
113        /** The reader used to read the video */
114        private IMediaReader reader = null;
115
116        /** Used to tell, when reading packets, if we got enough for a new frame */
117        private boolean currentFrameUpdated = false;
118
119        /** The current frame - only ever one object that's reused */
120        private MBFImage currentMBFImage;
121
122        /** Whether the current frame is a key frame or not */
123        private boolean currentFrameIsKeyFrame = false;
124
125        /** The stream index that we'll be reading from */
126        private int streamIndex = -1;
127
128        /** Width of the video frame */
129        private int width = -1;
130
131        /** Height of the video frame */
132        private int height = -1;
133
134        /** A cache of the calculation of he total number of frames in the video */
135        private long totalFrames = -1;
136
137        /** A cache of the url of the video */
138        private final String url;
139
140        /** A cache of whether the video should be looped or not */
141        private final boolean loop;
142
143        /** The timestamp of the frame currently being decoded */
144        private long timestamp;
145
146        /** The offset to add to all timestamps (used for looping) */
147        private long timestampOffset = 0;
148
149        /** The number of frames per second */
150        private double fps;
151
152        /** The next frame in the stream */
153        private MBFImage nextFrame = null;
154
155        /** The timestamp of the next frame */
156        public long nextFrameTimestamp = 0;
157
158        /** Whether the next frame is a key frame or not */
159        public boolean nextFrameIsKeyFrame = false;
160
161        /**
162         * This implements the Xuggle MediaTool listener that will be called every
163         * time a video picture has been decoded from the stream. This class creates
164         * a BufferedImage for each video frame and updates the currentFrameUpdated
165         * boolean when one arrives.
166         *
167         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
168         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
169         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
170         *
171         * @created 1 Jun 2011
172         */
173        protected class FrameGetter extends MediaListenerAdapter {
174                /**
175                 * {@inheritDoc}
176                 *
177                 * @see com.xuggle.mediatool.MediaToolAdapter#onVideoPicture(com.xuggle.mediatool.event.IVideoPictureEvent)
178                 */
179                @Override
180                public void onVideoPicture(final IVideoPictureEvent event) {
181                        // event.getPicture().getTimeStamp();
182                        if (event.getStreamIndex() == XuggleVideo.this.streamIndex) {
183                                XuggleVideo.this.currentMBFImage = ((MBFImageWrapper) event.getImage()).img;
184                                XuggleVideo.this.currentFrameIsKeyFrame = event.getMediaData().isKeyFrame();
185                                XuggleVideo.this.timestamp = (long) ((event.getPicture().getTimeStamp()
186                                                * event.getPicture().getTimeBase().getDouble()) * 1000)
187                                                + XuggleVideo.this.timestampOffset;
188                                XuggleVideo.this.currentFrameUpdated = true;
189                        }
190                }
191        }
192
193        /**
194         * Wrapper that created an MBFImage from a BufferedImage.
195         *
196         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
197         *
198         * @created 1 Nov 2011
199         */
200        protected static final class MBFImageWrapper extends BufferedImage {
201                MBFImage img;
202
203                public MBFImageWrapper(final MBFImage img) {
204                        super(1, 1, BufferedImage.TYPE_INT_RGB);
205                        this.img = img;
206                }
207        }
208
209        /**
210         * Converter for converting IVideoPictures directly to MBFImages.
211         *
212         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
213         *
214         * @created 1 Nov 2011
215         */
216        protected static final class MBFImageConverter extends BgrConverter {
217                private final MBFImageWrapper bimg = new MBFImageWrapper(null);
218                private final byte[] buffer;
219
220                public MBFImageConverter(
221                                final IPixelFormat.Type pictureType, final int pictureWidth,
222                                final int pictureHeight, final int imageWidth, final int imageHeight)
223                {
224                        super(pictureType, pictureWidth, pictureHeight, imageWidth, imageHeight);
225
226                        this.bimg.img = new MBFImage(imageWidth, imageHeight, ColourSpace.RGB);
227                        this.buffer = new byte[imageWidth * imageHeight * 3];
228                }
229
230                @Override
231                public BufferedImage toImage(IVideoPicture picture) {
232                        // test that the picture is valid
233                        this.validatePicture(picture);
234
235                        // resample as needed
236                        IVideoPicture resamplePicture = null;
237                        final AtomicReference<JNIReference> ref = new AtomicReference<JNIReference>(null);
238                        try {
239                                if (this.willResample()) {
240                                        resamplePicture = AConverter.resample(picture, this.mToImageResampler);
241                                        picture = resamplePicture;
242                                }
243
244                                // get picture parameters
245                                final int w = picture.getWidth();
246                                final int h = picture.getHeight();
247
248                                final float[][] r = this.bimg.img.bands.get(0).pixels;
249                                final float[][] g = this.bimg.img.bands.get(1).pixels;
250                                final float[][] b = this.bimg.img.bands.get(2).pixels;
251
252                                picture.getDataCached().get(0, this.buffer, 0, this.buffer.length);
253                                for (int y = 0, i = 0; y < h; y++) {
254                                        for (int x = 0; x < w; x++, i += 3) {
255                                                b[y][x] = ImageUtilities.BYTE_TO_FLOAT_LUT[(this.buffer[i] & 0xFF)];
256                                                g[y][x] = ImageUtilities.BYTE_TO_FLOAT_LUT[(this.buffer[i + 1] & 0xFF)];
257                                                r[y][x] = ImageUtilities.BYTE_TO_FLOAT_LUT[(this.buffer[i + 2] & 0xFF)];
258                                        }
259                                }
260
261                                return this.bimg;
262                        } finally {
263                                if (resamplePicture != null)
264                                        resamplePicture.delete();
265                                if (ref.get() != null)
266                                        ref.get().delete();
267                        }
268                }
269        }
270
271        /**
272         * Default constructor that takes the video file to read.
273         *
274         * @param videoFile
275         *            The video file to read.
276         */
277        public XuggleVideo(final File videoFile) {
278                this(videoFile.toURI().toString());
279        }
280
281        /**
282         * Default constructor that takes the video file to read.
283         *
284         * @param videoFile
285         *            The video file to read.
286         * @param loop
287         *            should the video loop
288         */
289        public XuggleVideo(final File videoFile, final boolean loop) {
290                this(videoFile.toURI().toString(), loop);
291        }
292
293        /**
294         * Default constructor that takes the location of a video file to read. This
295         * can either be a filename or a URL.
296         *
297         * @param url
298         *            The URL of the file to read
299         */
300        public XuggleVideo(final String url) {
301                this(url, false);
302        }
303
304        /**
305         * Default constructor that takes the URL of a video file to read.
306         *
307         * @param url
308         *            The URL of the file to read
309         */
310        public XuggleVideo(final URL url) {
311                this(url.toString(), false);
312        }
313
314        /**
315         * Default constructor that takes the location of a video file to read. This
316         * can either be a filename or a URL. The second parameter determines
317         * whether the video will loop indefinitely. If so, {@link #getNextFrame()}
318         * will never return null; otherwise this method will return null at the end
319         * of the video.
320         *
321         * @param url
322         *            The URL of the file to read
323         * @param loop
324         *            Whether to loop the video indefinitely
325         */
326        public XuggleVideo(final URL url, final boolean loop) {
327                this(url.toString(), loop);
328        }
329
330        /**
331         * Default constructor that takes the location of a video file to read. This
332         * can either be a filename or a URL. The second parameter determines
333         * whether the video will loop indefinitely. If so, {@link #getNextFrame()}
334         * will never return null; otherwise this method will return null at the end
335         * of the video.
336         *
337         * @param url
338         *            The URL of the file to read
339         * @param loop
340         *            Whether to loop the video indefinitely
341         */
342        public XuggleVideo(final String url, final boolean loop) {
343                this.url = url;
344                this.loop = loop;
345                this.create(url);
346        }
347
348        /**
349         * Default constructor that takes an input stream. Note that only
350         * "streamable" video codecs can be used in this way.
351         *
352         * @param stream
353         *            The video data stream
354         */
355        public XuggleVideo(final InputStream stream) {
356                this.url = null;
357                this.loop = false;
358                this.create(stream);
359        }
360
361        /**
362         * Default constructor that takes a data input. Note that only "streamable"
363         * video codecs can be used in this way.
364         *
365         * @param input
366         *            The video data
367         */
368        public XuggleVideo(final DataInput input) {
369                this.url = null;
370                this.loop = false;
371                this.create(input);
372        }
373
374        /**
375         * {@inheritDoc}
376         *
377         * @see org.openimaj.video.Video#countFrames()
378         */
379        @Override
380        public long countFrames() {
381                return this.totalFrames;
382        }
383
384        /**
385         * {@inheritDoc}
386         *
387         * @see org.openimaj.video.Video#getNextFrame()
388         */
389        @Override
390        public MBFImage getNextFrame() {
391                if (this.nextFrame != null) {
392                        // We've already read the next frame, so we simply move on.
393                        this.currentMBFImage = this.nextFrame;
394                        this.timestamp = this.nextFrameTimestamp;
395                        this.currentFrameIsKeyFrame = this.nextFrameIsKeyFrame;
396                        this.nextFrame = null;
397                } else {
398                        // Read a frame from the stream.
399                        this.currentMBFImage = this.readFrame(false);
400                }
401
402                if (this.currentMBFImage != null) {
403                        // Increment frame counter
404                        this.currentFrame++;
405                }
406
407                return this.currentMBFImage;
408        }
409
410        /**
411         * Reads a frame from the stream, or returns null if no frame could be read.
412         * If preserveCurrent is true, then the frame is read into the nextFrame
413         * member rather than the currentMBFImage member and the nextFrame is
414         * returned (while currentMBFImage will still contain the previous frame).
415         * Note that if preserveCurrent is true, it will invoke a copy between
416         * images. If preserveCurrent is false and nextFrame is set, this method may
417         * have unexpected results as it does not swap current and next back. See
418         * {@link #getNextFrame()} which swaps back when a frame has been pre-read
419         * from the stream.
420         *
421         * @param preserveCurrent
422         *            Whether to preserve the current frame
423         * @return The frame that was read, or NULL if no frame could be read.
424         */
425        synchronized private MBFImage readFrame(final boolean preserveCurrent) {
426                // System.out.println( "readFrame( "+preserveCurrent+" )");
427
428                if (this.reader == null)
429                        return null;
430
431                // If we need to preserve the current frame, we need to copy the frame
432                // because the readPacket() will cause the frame to be overwritten
433                final long currentTimestamp = this.timestamp;
434                final boolean currentKeyFrameFlag = this.currentFrameIsKeyFrame;
435                if (preserveCurrent && this.nextFrame == null) {
436                        // We make a copy of the current image and set the current image
437                        // to point to that (thereby preserving it). We then set the next
438                        // frame image to point to the buffer that the readPacket() will
439                        // fill.
440                        if (this.currentMBFImage != null) {
441                                final MBFImage tmp = this.currentMBFImage.clone();
442                                this.nextFrame = this.currentMBFImage;
443                                this.currentMBFImage = tmp;
444                        }
445                }
446                // If nextFrame wasn't null, we can just write into it as must be
447                // pointing to the current frame buffer
448
449                IError e = null;
450                boolean tryAgain = false;
451                do {
452                        tryAgain = false;
453
454                        // Read packets until we have a new frame.
455                        while ((e = this.reader.readPacket()) == null && !this.currentFrameUpdated)
456                                ;
457
458                        if (e != null && e.getType() == IError.Type.ERROR_EOF && this.loop) {
459                                // We're looping, so we update the timestamp offset.
460                                this.timestampOffset += (this.timestamp - this.timestampOffset);
461                                tryAgain = true;
462                                this.seekToBeginning();
463                        }
464                } while (tryAgain);
465
466                // Check if we're at the end of the file
467                if (!this.currentFrameUpdated || e != null) {
468                        // Logger.error( "Got video demux error: "+e.getType() );
469                        return null;
470                }
471
472                // We've read a frame so we're done looping
473                this.currentFrameUpdated = false;
474
475                if (preserveCurrent) {
476                        // Swap the current values into the next-frame values
477                        this.nextFrameIsKeyFrame = this.currentFrameIsKeyFrame;
478                        this.currentFrameIsKeyFrame = currentKeyFrameFlag;
479                        this.nextFrameTimestamp = this.timestamp;
480                        this.timestamp = currentTimestamp;
481
482                        // Return the next frame
483                        if (this.nextFrame != null)
484                                return this.nextFrame;
485                        return this.currentMBFImage;
486                }
487                // Not preserving anything, so just return the frame
488                else
489                        return this.currentMBFImage;
490        }
491
492        /**
493         * Returns a video timecode for the current frame.
494         *
495         * @return A video timecode for the current frame.
496         */
497        public VideoTimecode getCurrentTimecode() {
498                return new HrsMinSecFrameTimecode((long) (this.timestamp / 1000d * this.fps), this.fps);
499        }
500
501        /**
502         * {@inheritDoc}
503         *
504         * @see org.openimaj.video.Video#getCurrentFrame()
505         */
506        @Override
507        public MBFImage getCurrentFrame() {
508                if (this.currentMBFImage == null)
509                        this.currentMBFImage = this.getNextFrame();
510                return this.currentMBFImage;
511        }
512
513        /**
514         * {@inheritDoc}
515         *
516         * @see org.openimaj.video.Video#getWidth()
517         */
518        @Override
519        public int getWidth() {
520                return this.width;
521        }
522
523        /**
524         * {@inheritDoc}
525         *
526         * @see org.openimaj.video.Video#getHeight()
527         */
528        @Override
529        public int getHeight() {
530                return this.height;
531        }
532
533        /**
534         * {@inheritDoc}
535         *
536         * @see org.openimaj.video.Video#hasNextFrame()
537         */
538        @Override
539        public boolean hasNextFrame() {
540                if (this.nextFrame == null) {
541                        this.nextFrame = this.readFrame(true);
542                        return this.nextFrame != null;
543                } else
544                        return true;
545        }
546
547        /**
548         * {@inheritDoc}
549         * <p>
550         * Note: if you created the video from a {@link DataInput} or
551         * {@link InputStream}, there is no way that it can be reset.
552         *
553         * @see org.openimaj.video.Video#reset()
554         */
555        @Override
556        synchronized public void reset() {
557                if (this.reader == null) {
558                        if (this.url == null)
559                                return;
560
561                        this.create(url);
562                } else {
563                        this.seekToBeginning();
564                }
565        }
566
567        /**
568         * This is a convenience method that will seek the stream to be the
569         * beginning. As the seek method seems a bit flakey in some codec containers
570         * in Xuggle, we'll try and use a few different methods to get us back to
571         * the beginning. That means that this method may be slower than seek(0) if
572         * it needs to try multiple methods.
573         * <p>
574         * Note: if you created the video from a {@link DataInput} or
575         * {@link InputStream}, there is no way that it can be reset.
576         */
577        synchronized public void seekToBeginning() {
578                // if the video came from a stream, there is no chance of returning!
579                if (this.url == null)
580                        return;
581
582                // Try to seek to byte 0. That's the start of the file.
583                this.reader.getContainer().seekKeyFrame(this.streamIndex,
584                                0, 0, 0, IContainer.SEEK_FLAG_BYTE);
585
586                // Got to the beginning? We're done.
587                if (this.timestamp == 0)
588                        return;
589
590                // Try to seek to key frame at timestamp 0.
591                this.reader.getContainer().seekKeyFrame(this.streamIndex,
592                                0, 0, 0, IContainer.SEEK_FLAG_FRAME);
593
594                // Got to the beginning? We're done.
595                if (this.timestamp == 0)
596                        return;
597
598                // Try to seek backwards to timestamp 0.
599                this.reader.getContainer().seekKeyFrame(this.streamIndex,
600                                0, 0, 0, IContainer.SEEK_FLAG_BACKWARDS);
601
602                // Got to the beginning? We're done.
603                if (this.timestamp == 0)
604                        return;
605
606                // Try to seek to timestamp 0 any way possible.
607                this.reader.getContainer().seekKeyFrame(this.streamIndex,
608                                0, 0, 0, IContainer.SEEK_FLAG_ANY);
609
610                // Got to the beginning? We're done.
611                if (this.timestamp == 0)
612                        return;
613
614                // We're really struggling to get this container back to the start.
615                // So, try recreating the whole reader again.
616                this.reader.close();
617                this.reader = null;
618                this.create(url);
619
620                this.getNextFrame();
621
622                // We tried everything. It's either worked or it hasn't.
623                return;
624        }
625
626        /**
627         * Create the necessary reader
628         */
629        synchronized private void create(String urlstring) {
630                setupReader();
631
632                // Check whether the string we have is a valid URI
633                IContainer container = null;
634                int openResult = 0;
635                try {
636                        // If it's a valid URI, we'll try to open the container using the
637                        // URI string.
638                        container = IContainer.make();
639                        openResult = container.open(urlstring, IContainer.Type.READ, null, true, true);
640
641                        // If there was an error trying to open the container in this way,
642                        // it may be that we have a resource URL (which ffmpeg doesn't
643                        // understand), so we'll try opening an InputStream to the resource.
644                        if (openResult < 0) {
645                                logger.trace("URL " + urlstring + " could not be opened by ffmpeg. " +
646                                                "Trying to open a stream to the URL instead.");
647                                final InputStream is = new DataInputStream(new URL(urlstring).openStream());
648                                openResult = container.open(is, null, true, true);
649
650                                if (openResult < 0) {
651                                        logger.error("Error opening container. Error " + openResult +
652                                                        " (" + IError.errorNumberToType(openResult).toString() + ")");
653                                        return;
654                                }
655                        }
656                } catch (final MalformedURLException e) {
657                        e.printStackTrace();
658                        return;
659                } catch (final IOException e) {
660                        e.printStackTrace();
661                        return;
662                }
663
664                setupReader(container);
665        }
666
667        /**
668         * Create the necessary reader
669         */
670        synchronized private void create(InputStream stream) {
671                setupReader();
672
673                // Check whether the string we have is a valid URI
674                final IContainer container = IContainer.make();
675                final int openResult = container.open(stream, null, true, true);
676
677                if (openResult < 0) {
678                        logger.error("Error opening container. Error " + openResult +
679                                        " (" + IError.errorNumberToType(openResult).toString() + ")");
680                        return;
681                }
682
683                setupReader(container);
684        }
685
686        /**
687         * Create the necessary reader
688         */
689        synchronized private void create(DataInput input) {
690                setupReader();
691
692                // Check whether the string we have is a valid URI
693                final IContainer container = IContainer.make();
694                final int openResult = container.open(input, null, true, true);
695
696                if (openResult < 0) {
697                        logger.error("Error opening container. Error " + openResult +
698                                        " (" + IError.errorNumberToType(openResult).toString() + ")");
699                        return;
700                }
701
702                setupReader(container);
703        }
704
705        private void setupReader() {
706                // Assume we'll start at the beginning again
707                this.currentFrame = 0;
708
709                // If the reader is already open, we'll close it first and
710                // reinstantiate it.
711                if (this.reader != null && this.reader.isOpen()) {
712                        this.reader.close();
713                        this.reader = null;
714                }
715        }
716
717        private void setupReader(IContainer container) {
718                // Set up a new reader using the container that reads the images.
719                this.reader = ToolFactory.makeReader(container);
720                this.reader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);
721                this.reader.addListener(new FrameGetter());
722
723                // Find the video stream.
724                IStream s = null;
725                int i = 0;
726                while (i < container.getNumStreams()) {
727                        s = container.getStream(i);
728                        if (s != null && s.getStreamCoder().getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {
729                                // Save the stream index so that we only get frames from
730                                // this stream in the FrameGetter
731                                this.streamIndex = i;
732                                break;
733                        }
734                        i++;
735                }
736
737                if (container.getDuration() == Global.NO_PTS)
738                        this.totalFrames = -1;
739                else
740                        this.totalFrames = (long) (s.getDuration() *
741                                        s.getTimeBase().getDouble() * s.getFrameRate().getDouble());
742
743                // If we found the video stream, set the FPS
744                if (s != null)
745                        this.fps = s.getFrameRate().getDouble();
746
747                // If we found a video stream, setup the MBFImage buffer.
748                if (s != null) {
749                        final int w = s.getStreamCoder().getWidth();
750                        final int h = s.getStreamCoder().getHeight();
751                        this.width = w;
752                        this.height = h;
753                }
754        }
755
756        /**
757         * {@inheritDoc}
758         *
759         * @see org.openimaj.video.Video#getTimeStamp()
760         */
761        @Override
762        public long getTimeStamp() {
763                return this.timestamp;
764        }
765
766        /**
767         * {@inheritDoc}
768         *
769         * @see org.openimaj.video.Video#getFPS()
770         */
771        @Override
772        public double getFPS() {
773                return this.fps;
774        }
775
776        /**
777         * {@inheritDoc}
778         *
779         * @see org.openimaj.video.Video#getCurrentFrameIndex()
780         */
781        @Override
782        public synchronized int getCurrentFrameIndex() {
783                return (int) (this.timestamp / 1000d * this.fps);
784        }
785
786        /**
787         * {@inheritDoc}
788         *
789         * @see org.openimaj.video.Video#setCurrentFrameIndex(long)
790         */
791        @Override
792        public void setCurrentFrameIndex(final long newFrame) {
793                this.seekPrecise(newFrame / this.fps);
794        }
795
796        /**
797         * Implements a precise seeking mechanism based on the Xuggle seek method
798         * and the naive seek method which simply reads frames.
799         * <p>
800         * Note: if you created the video from a {@link DataInput} or
801         * {@link InputStream}, you can only seek forwards.
802         *
803         * @param timestamp
804         *            The timestamp to get, in seconds.
805         */
806        public void seekPrecise(double timestamp) {
807                // Use the Xuggle seek method first to get near the frame
808                this.seek(timestamp);
809
810                // The timestamp field is in milliseconds, so we need to * 1000 to
811                // compare
812                timestamp *= 1000;
813
814                // Work out the number of milliseconds per frame
815                final double timePerFrame = 1000d / this.fps;
816
817                // If we're not in the right place, keep reading until we are.
818                // Note the right place is the frame before the timestamp we're given:
819                // |---frame 1---|---frame2---|---frame3---|
820                // ^- given timestamp
821                // ... so we should show frame2 not frame3.
822                while (this.timestamp <= timestamp - timePerFrame && this.getNextFrame() != null)
823                        ;
824        }
825
826        /**
827         * {@inheritDoc}
828         * <p>
829         * Note: if you created the video from a {@link DataInput} or
830         * {@link InputStream}, you can only seek forwards.
831         *
832         * @see org.openimaj.video.Video#seek(double)
833         */
834        @Override
835        synchronized public void seek(final double timestamp) {
836                // Based on the code of this class:
837                // 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
838                // using the timebase, calculate the time in timebase units requested
839                // Check we've actually got a container
840                if (this.reader == null) {
841                        if (this.url == null)
842                                return;
843
844                        this.create(url);
845                }
846
847                // Convert between milliseconds and stream timestamps
848                final double timebase = this.reader.getContainer().getStream(
849                                this.streamIndex).getTimeBase().getDouble();
850                final long position = (long) (timestamp / timebase);
851
852                final long min = Math.max(0, position - 100);
853                final long max = position;
854
855                final int ret = this.reader.getContainer().seekKeyFrame(this.streamIndex,
856                                min, position, max, IContainer.SEEK_FLAG_ANY);
857
858                if (ret >= 0)
859                        this.getNextFrame();
860                else
861                        logger.error("Seek returned an error value: " + ret + ": "
862                                        + IError.errorNumberToType(ret));
863        }
864
865        /**
866         * Returns the duration of the video in seconds.
867         *
868         * @return The duration of the video in seconds.
869         */
870        public synchronized long getDuration() {
871                final long duration = (this.reader.getContainer().getStream(this.streamIndex).getDuration());
872                final double timebase = this.reader.getContainer().getStream(this.streamIndex).getTimeBase().getDouble();
873
874                return Math.round(duration * timebase);
875        }
876
877        /**
878         * {@inheritDoc}
879         *
880         * @see org.openimaj.video.Video#close()
881         */
882        @Override
883        public synchronized void close() {
884                if (this.reader != null) {
885                        synchronized (this.reader) {
886                                if (this.reader.isOpen()) {
887                                        this.reader.close();
888                                        this.reader = null;
889                                }
890                        }
891                }
892        }
893}