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}