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}