View Javadoc

1   /*
2    *  XNap - A P2P framework and client.
3    *
4    *  See the file AUTHORS for copyright information.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation.
9    *
10   *  This program is distributed in the hope that it will be useful,
11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   *  GNU General Public License for more details.
14   *
15   *  You should have received a copy of the GNU General Public License
16   *  along with this program; if not, write to the Free Software
17   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18   */
19  package org.xnap.io;
20  
21  import java.io.IOException;
22  import java.io.RandomAccessFile;
23  
24  import org.apache.log4j.Logger;
25  import org.xnap.util.Formatter;
26  
27  /***
28   * Class to extract a video's play length and resolution.
29   *
30   * The video parsing code for avi and mpg files is taken from the ziga
31   * project. See README for more information.
32   * The video parsing code for asf files is taken from the avifile project
33   * (http://avifile.sourceforge.net/).
34   */
35  public class VideoMetaInfo
36  {
37  
38      // --- Constant(s) ---
39  
40      protected static final int PACK_START_CODE = 0x000001BA;
41      protected static final int SEQ_START_CODE = 0x000001B3;
42      protected static final int GROUP_START_CODE = 0x000001B8;
43      protected static final int MAX_FORWARD_READ_LENGTH = 50000;
44      protected static final int MAX_BACKWARD_READ_LENGTH = 3000000;
45  
46      protected static final int ASF_FILE_PROPERTIES_SIZE = 80;
47      protected static final int ASF_STREAM_PROPERTIES_SIZE = 1024;
48      protected static final int GUID_SIZE = 16;
49  
50      // --- Data Field(s) ---
51  
52  	private static Logger logger = Logger.getLogger(VideoMetaInfo.class);
53  
54  	private MetaInfoFile file;
55  	
56      // --- Constructor(s) ---
57  
58      public VideoMetaInfo(MetaInfoFile file)
59      {
60  		this.file = file;
61      }
62  
63      // --- Method(s) ---
64  
65  	public static boolean handle(MetaInfoFile file)
66      {
67  		try {
68  			new Parser(file);
69  		}
70  		catch (IOException e) {
71  			return false;
72  		}
73  
74  		file.put("provider:video", "true");
75  		return true;
76      }
77  
78      public static boolean hasInfo(MetaInfoFile file)
79      {
80      	return "true".equals(file.get("provider:video"));
81      }
82  
83      public int getHeight()
84      {
85  		return ((Integer)file.get(MetaInfoFile.HEIGHT)).intValue();
86      }
87  
88      public String getInfo()
89      {
90  		return Formatter.formatLength(getLength());
91      }
92  
93      public int getLength()
94      {
95  		return ((Integer)file.get(MetaInfoFile.LENGTH)).intValue();
96      }
97  
98      public int getWidth()
99      {
100 		return ((Integer)file.get(MetaInfoFile.WIDTH)).intValue();
101     }
102 
103     public static class Provider implements MetaInfoProvider {
104 
105 		/***
106 		 *  @see org.xnap.io.MetaInfoProvider#handle(org.xnap.io.MetaInfoFile)
107 		 */
108 		public boolean handle(MetaInfoFile file)
109 		{
110 			return VideoMetaInfo.handle(file);
111 		} 
112     
113     }
114 
115 	private static class Parser {
116 
117 		protected final transient GUID ASF_HEADER_GUID =
118 			new GUID(0x75b22630, 0x668e, 0x11cf,
119 					 new byte[] { (byte)0xa6, (byte)0xd9, (byte)0x00, (byte)0xaa,
120 								  (byte)0x00, (byte)0x62, (byte)0xce,
121 								  (byte)0x6c });
122 	
123 	    protected final transient GUID ASF_FILE_PROPERTIES_GUID =
124 			new GUID(0x8cabdca1, 0xa947, 0x11cf,
125 					 new byte[] { (byte)0x8e, (byte)0xe4, (byte)0x00, (byte)0xc0,
126 								  (byte)0x0c, (byte)0x20, (byte)0x53,
127 								  (byte)0x65 });
128 	
129 	    protected final transient GUID ASF_STREAM_PROPERTIES_GUID =
130 			new GUID(0xb7dc0791, 0xa9b7, 0x11cf,
131 					 new byte[] { (byte)0x8e, (byte)0xe6, (byte)0x00, (byte)0xc0,
132 								  (byte)0x0c, (byte)0x20, (byte)0x53,
133 								  (byte)0x65 });
134 	
135 	    protected final transient GUID ASF_AUDIO_MEDIA_GUID =
136 			new GUID(0xf8699e40, 0x5b4d, 0x11cf,
137 					 new byte[] { (byte)0xa8, (byte)0xfd, (byte)0x00, (byte)0x80,
138 								  (byte)0x5f, (byte)0x5c, (byte)0x44,
139 								  (byte)0x2b });
140 	
141 	    protected final transient GUID ASF_CODEC_LIST_GUID =
142 			new GUID(0x86d15240, 0x311d, 0x11d0,
143 					 new byte[] { (byte)0xa3, (byte)0xa4, (byte)0x00, (byte)0xa0,
144 								  (byte)0xc9, (byte)0x03, (byte)0x48,
145 								  (byte)0xf6 });
146 	
147 			
148 		private MetaInfoFile file;
149 		private RandomAccessFile in;
150 		
151 		public Parser(MetaInfoFile file) throws IOException
152 		{
153 			this.file = file;
154 
155 			in = new RandomAccessFile(file, "r");		
156 			try {
157 				if (!lookForAVI()) {
158 					if (!lookForMPEG()) {
159 						if (!lookForASF()) {
160 							throw new IOException("Invalid file");
161 						}
162 					}
163 				}
164 			}
165 			finally {
166 				try {
167 					in.close();
168 				} catch (IOException e) {}
169 			}
170 		}
171 
172 		private void setHeight(int height)
173 		{
174 			file.put(MetaInfoFile.HEIGHT, new Integer(height));
175 		}
176 	
177 		private void setLength(int length)
178 		{
179 			file.put(MetaInfoFile.LENGTH, new Integer(length));
180 		}
181 	
182 		private void setWidth(int width)
183 		{
184 			file.put(MetaInfoFile.WIDTH, new Integer(width));
185 		}
186 	
187 	    protected boolean lookForAVI() throws IOException
188 	    {
189 			boolean avihFound = false;
190 			in.seek(0);
191 	
192 			String s = readChunk();
193 	
194 			if (s.equals("RIFF")) {
195 				in.skipBytes(4);
196 				s = readChunk();
197 	
198 				if (s.equals("AVI ")) {
199 					while (true) {
200 						s = readChunk();
201 						in.skipBytes(4);
202 	
203 						if (s.equals("LIST")) {
204 							s = readChunk();
205 							if (s.equals("hdrl")) {
206 								s = readChunk();
207 								int avihLength = readBigEndianInt();
208 								if (s.equals("avih") && avihLength >= 56) {
209 									double microSecPerFrame = readBigEndianInt();
210 									in.skipBytes(12);
211 									double totalFrames = readBigEndianInt();
212 									// set member variable length
213 									setLength((int) (totalFrames * (microSecPerFrame / 1000000L)));
214 									in.skipBytes(12);
215 									// set members
216 									setWidth(readBigEndianInt());
217 									setHeight(readBigEndianInt());
218 									in.skipBytes(avihLength - 40);
219 									avihFound = true;
220 								}
221 							}
222 						}
223 						else {
224 							break;
225 						}
226 					}
227 				}
228 			}
229 	
230 			return avihFound;
231 	    }
232 	
233 	    protected boolean lookForASF() throws IOException
234 	    {
235 			boolean haveMainHeader = false;
236 			boolean haveStreamHeader = false;
237 			boolean haveCodecListHeader = false;
238 			ASFMainHeader mainHeader = null;
239 			ASFStreamHeader streamHeader = null;
240 		    ASFCodecList codecList = null;
241 	
242 			in.seek(0);
243 	
244 			logger.debug("looking for asf");
245 	
246 			while (!haveMainHeader || !haveStreamHeader || !haveCodecListHeader) {
247 				GUID guid;
248 				long size;
249 	
250 				guid = new GUID();
251 	
252 				size = readBigEndianLong() - 24;
253 	
254 				logger.debug("GUID " + guid);
255 				logger.debug("size " + size);
256 	
257 				if (size < 0)
258 					return false;
259 	
260 				if (guid.equals(ASF_HEADER_GUID)) {
261 					logger.debug("Found header, skipping 6 bytes");
262 					in.skipBytes(6);
263 				}
264 				else if (guid.equals(ASF_FILE_PROPERTIES_GUID)) {
265 					logger.debug("Found asf main header");
266 					if (size < ASF_FILE_PROPERTIES_SIZE) {
267 						logger.debug("main header too small");
268 						return false;
269 					}
270 	
271 					mainHeader = new ASFMainHeader();
272 					in.skipBytes((int) (size - ASF_FILE_PROPERTIES_SIZE));
273 					haveMainHeader = true;
274 				}
275 				else if (guid.equals(ASF_STREAM_PROPERTIES_GUID)) {
276 					logger.debug("Found stream props");
277 					streamHeader = new ASFStreamHeader(size);
278 					if (size > streamHeader.count) {
279 						in.skipBytes((int) (size - streamHeader.count));
280 					}
281 					haveStreamHeader = true;
282 				}
283 				else if (guid.equals(ASF_CODEC_LIST_GUID)) {
284 					logger.debug("Found codec list header");
285 					codecList = new ASFCodecList(size);
286 					haveCodecListHeader = true;
287 				}
288 				else {
289 					logger.debug("Unknown packet, skipping " + size);
290 					in.skipBytes((int)size);
291 				}
292 			}
293 	
294 			if (haveMainHeader && haveStreamHeader && haveCodecListHeader) {
295 	
296 				setLength((int)(mainHeader.play_time / 10000000L));
297 	
298 				if (streamHeader.videoHeader) {
299 					setHeight(streamHeader.height);
300 					setWidth(streamHeader.width);
301 				}
302 	
303 				return true;
304 			}
305 	
306 			return false;
307 	    }
308 	
309 	    protected boolean lookForMPEG() throws IOException
310 	    {
311 			in.seek(0);
312 	
313 			if (nextStartCode()) {
314 				if (in.readInt() == PACK_START_CODE) {
315 					byte[] b = new byte[6];
316 					Long initialSCR = null;
317 	
318 					in.readFully(b);
319 	
320 					if ((b[0] & 0xF0) == 0x20) {
321 						initialSCR = getMPEGSCR(b);
322 					}
323 					else if ((b[0] & 0xC0) == 0x40) {
324 						initialSCR = getMPEG2SCR(b);
325 					}
326 	
327 					boolean seqStartCodeFound = false;
328 					while (true) {
329 						if (nextStartCode()) {
330 							if(in.readInt() == SEQ_START_CODE) {
331 								seqStartCodeFound = true;
332 								break;
333 							}
334 						}
335 						else {
336 							break;
337 						}
338 					}
339 					if (seqStartCodeFound) {
340 						b = new byte[3];
341 						in.readFully(b);
342 	
343 						setWidth((((b[0] & 0xff) << 4) | (b[1] & 0xf0)));
344 	
345 	
346 						setHeight((((b[1] & 0x0f) << 8) | (b[2] & 0xff)));
347 	
348 						boolean groupStartCodeFound = false;
349 						in.seek(in.length());
350 						while (true) {
351 							if (previousStartCode()) {
352 								if(in.readInt() == PACK_START_CODE) {
353 									groupStartCodeFound = true;
354 									break;
355 								}
356 							}
357 							else {
358 								break;
359 							}
360 							in.seek(in.getFilePointer() - 4);
361 						}
362 	
363 						if (groupStartCodeFound) {
364 							b = new byte[6];
365 							Long lastSCR = null;
366 	
367 							in.readFully(b);
368 	
369 							if ((b[0] & 0xF0) == 0x20) {
370 								lastSCR = getMPEGSCR(b);
371 							}
372 							else if ((b[0] & 0xC0) == 0x40) {
373 								lastSCR = getMPEG2SCR(b);
374 							}
375 	
376 							if (initialSCR != null && lastSCR != null) {
377 								setLength((int) (lastSCR.longValue()
378 												- initialSCR.longValue()));
379 							}
380 						}
381 	
382 						return true;
383 					}
384 				}
385 			}
386 			return false;
387 	    }
388 	
389 	    protected final String readChunk() throws IOException
390 	    {
391 			byte[] b = new byte[4];
392 			in.readFully(b);
393 			return new String(b);
394 	    }
395 	
396 	    protected final short readBigEndianShort() throws IOException
397 	    {
398 			byte[] b = new byte[2];
399 			in.readFully(b);
400 			return (short) ((b[0] & 0xff) | ((b[1] & 0xff) << 8));
401 	    }
402 	
403 	    protected final int readBigEndianInt() throws IOException
404 	    {
405 			byte[] b = new byte[4];
406 			in.readFully(b);
407 			return (b[0] & 0xff) | ((b[1] & 0xff) << 8) | ((b[2] & 0xff) << 16)
408 				| ((b[3] & 0xff) << 24);
409 	    }
410 	
411 	    protected final long readBigEndianLong() throws IOException
412 	    {
413 			return (readBigEndianInt() | (readBigEndianInt() << 32));
414 	    }
415 	
416 	
417 	    protected Long getMPEGSCR(byte[] b)
418 	    {
419 			long scr;
420 	
421 			int highbit = (b[0] >> 3) & 0x01;
422 			long low4Bytes = (((b[0] & 0xff) >> 1) & 0x03) << 30
423 				|(b[1] & 0xff) << 22 | ((b[2] & 0xff) >> 1) << 15 |
424 				(b[3] & 0xff) << 7 | (b[4] & 0xff) >> 1;
425 	
426 			scr = low4Bytes;
427 			scr /= 90000;
428 	
429 			return new Long(scr);
430 	    }
431 	
432 	    protected Long getMPEG2SCR(byte[] b)
433 	    {
434 			long scr;
435 	
436 			int highbit = (b[0] & 0x20) >> 5;
437 	
438 			long low4Bytes = ((b[0] & 0x18)	>> 3) << 30 | (b[0] & 0x03) << 28
439 				| (b[1] & 0xff) << 20 | ((b[2] & 0xF8) >> 1)  << 15 |
440 				(b[2] & 0x03) << 13 | (b[3] & 0xff) << 5 | (b[4] & 0xff) >> 3;
441 	
442 			int sys_clock_extension=(b[4] & 0x3) << 7 | ((b[5] & 0xff) >> 1);
443 	
444 			scr = low4Bytes;
445 			if (sys_clock_extension == 0) {
446 				scr /= 90000;
447 				return new Long(scr);
448 			}
449 			else {
450 				//???
451 				return null;
452 			}
453 	    }
454 	
455 	    protected boolean nextStartCode()
456 	    {
457 			byte[] b = new byte[1024];
458 			int available;
459 			boolean startCodeFound = false;
460 	
461 			try {
462 				for (int i = 0; i < MAX_FORWARD_READ_LENGTH && !startCodeFound;
463 					 i += available) {
464 	
465 					available = in.read(b);
466 					if (available > 0) {
467 						i += available;
468 						for (int offset = 0; offset < available-2; offset++) {
469 							if (b[offset] == 0 && b[offset+1] == 0 && b[offset+2] == 1) {
470 								in.seek(in.getFilePointer() - (available-offset));
471 								startCodeFound = true;
472 								break;
473 							}
474 						}
475 					}
476 					else {
477 						break;
478 					}
479 				}
480 			}
481 			catch(IOException e) {
482 			}
483 			return startCodeFound;
484 	    }
485 	
486 	    protected boolean previousStartCode()
487 	    {
488 			byte[] b = new byte[8024];
489 			boolean startCodeFound = false;
490 	
491 			try {
492 				for (int i = 0; i < MAX_BACKWARD_READ_LENGTH && !startCodeFound;
493 					 i += b.length) {
494 					long fp = in.getFilePointer() -  b.length;
495 					if (fp < 0) {
496 						if (fp <= b.length) {
497 							break;
498 						}
499 						fp = 0;
500 					}
501 					in.seek(fp);
502 					in.readFully(b);
503 					for (int offset = b.length-1; offset > 1; offset--) {
504 						if (b[offset-2] == 0 && b[offset-1] == 0
505 							&& b[offset] == 1) {
506 							in.seek(in.getFilePointer() - (b.length-offset) - 2);
507 							startCodeFound = true;
508 							break;
509 						}
510 					}
511 					if (!startCodeFound) {
512 						in.seek(in.getFilePointer() -  b.length);
513 					}
514 				}
515 			} catch(IOException e) {
516 			}
517 			return startCodeFound;
518 	    }
519 	
520 	    protected class GUID
521 	    {
522 			public int f1;
523 			public short f2;
524 			public short f3;
525 			public byte[] f4;
526 	
527 			public GUID() throws IOException
528 			{
529 				f4 = new byte[8];
530 	
531 				f1 = readBigEndianInt();
532 				f2 = readBigEndianShort();
533 				f3 = readBigEndianShort();
534 	
535 				for (int i = 0; i < f4.length; i++) {
536 					f4[i] = in.readByte();
537 				}
538 	
539 			}
540 	
541 			public GUID(int f1, int f2, int f3, byte[] f4)
542 			{
543 				this.f1 = f1;
544 				this.f2 = (short) f2;
545 				this.f3 = (short) f3;
546 				this.f4 = f4;
547 			}
548 	
549 	
550 			public boolean equals(Object o)
551 			{
552 				if (! (o instanceof GUID)) {
553 					return false;
554 				}
555 	
556 				GUID g = (GUID) o;
557 	
558 				if (f1 != g.f1 || f2 != g.f2 || f3 != g.f3) {
559 					return false;
560 				}
561 	
562 				for (int i = 0; i < f4.length; i++) {
563 					if (f4[i] != g.f4[i])
564 						return false;
565 				}
566 	
567 				return true;
568 			}
569 	
570 			public String toString()
571 			{
572 				return "f1 " + f1 + "\n"
573 					+ "f2 " + f2 + "\n"
574 					+ "f3 " + f3 + "\n";
575 			}
576 	    }
577 	
578 	    protected class ASFMainHeader
579 	    {
580 			public GUID guid;
581 			long file_size;		// in bytes
582 	                                // invalid if broadcasting
583 			long create_time;	// time of creation, in 100-nanosecond units since 1.1.1601
584 	                                // invalid if broadcasting
585 			long pkts_count;	// how many packets are there in the file
586 	                                // invalid if broadcasting
587 			public long play_time;		// play time, in 100-nanosecond units
588 	                                // invalid if broadcasting
589 			long send_time;		// time to send file, in 100-nanosecond units
590 	                                // invalid if broadcasting (could be ignored)
591 			int preroll;		// timestamp of the first packet, in milliseconds
592 			// if nonzero - substract from time
593 			int ignore;            // preroll is 64bit - but let's just ignore it
594 			int flags;		// 0x01 - broadcast
595 			// 0x02 - seekable
596 	                                // rest is reserved should be 0
597 			int min_pktsize;	// size of a data packet
598 	                                // invalid if broadcasting
599 			int max_pktsize;	// shall be the same as for min_pktsize
600 	                                // invalid if broadcasting
601 			int max_bitrate;	// bandwith of stream in bps
602 	
603 	
604 			public ASFMainHeader() throws IOException
605 			{
606 				guid = new GUID();
607 	
608 				file_size = readBigEndianLong();
609 				create_time = readBigEndianLong();
610 				pkts_count = readBigEndianLong();
611 				play_time = readBigEndianLong();
612 				send_time = readBigEndianLong();
613 	
614 				preroll = readBigEndianInt();
615 				ignore = readBigEndianInt();
616 				flags = readBigEndianInt();
617 				min_pktsize = readBigEndianInt();
618 				max_pktsize = readBigEndianInt();
619 				max_bitrate = readBigEndianInt();
620 			}
621 	
622 	    }
623 	
624 	    protected class ASFStreamHeader
625 	    {
626 			public GUID stream_guid;	// type of media stream
627 			public GUID error_guid;	// data error correction used
628 			public long time_offset;	// presentation time (in 100-nanosecond unit)
629 			public int stream_size;	// size of type-specific data
630 			public int error_size;	        // size of error correct data
631 			public short stream;	        // number ( 1, 2 ... )
632 			public int reserved;	        // usually the same in both streams:
633 			// Eyes on me.asf: 0x62dffd4
634 			// Alsou - Solo.asf: 0x10
635 			// Baby one more time.asf: 0x10
636 			// Cure_LastDayOfSummer.wma: 0x818f900c
637 			// Windows Movie Maker Sample File.wmv: 0x3f
638 	
639 			public int width;	// witdth of encoded image
640 			public int height;	// height of encoded image
641 			public byte flags;	// shall be set to 2
642 			public short data_size;
643 			//  BITMAPINFOHEADER bh;
644 	
645 			public int count = 0;
646 			public boolean videoHeader = false;
647 	
648 			public ASFStreamHeader(long size) throws IOException
649 			{
650 				if (count < size - GUID_SIZE) {
651 					stream_guid = new GUID();
652 					count += GUID_SIZE;
653 				}
654 				if (count < size - GUID_SIZE) {
655 					error_guid = new GUID();
656 					count += GUID_SIZE;
657 				}
658 	
659 				if (count < size - 8) {
660 					time_offset = readBigEndianLong();
661 					count += 8;
662 				}
663 				if (count < size - 4) {
664 					stream_size = readBigEndianInt();
665 					count += 4;
666 				}
667 				if (count < size - 4) {
668 					error_size = readBigEndianInt();
669 					count += 4;
670 				}
671 				if (count < size - 2) {
672 					stream = readBigEndianShort();
673 					count += 2;
674 				}
675 				if (count < size - 4) {
676 					reserved = readBigEndianInt();
677 					count += 4;
678 				}
679 	
680 				if (!stream_guid.equals(ASF_AUDIO_MEDIA_GUID)) {
681 					videoHeader = true;
682 					if (count < size - 4) {
683 						setWidth(readBigEndianInt());
684 						count += 4;
685 					}
686 					if (count < size - 4) {
687 						setHeight(readBigEndianInt());
688 						count += 4;
689 					}
690 					if (count < size - 1) {
691 						flags = in.readByte();
692 						count += 1;
693 					}
694 					if (count < size - 2) {
695 						data_size = readBigEndianShort();
696 						count += 2;
697 					}
698 				}
699 			}
700 	    }
701 	
702 	    protected class ASFCodecList
703 	    {
704 			public final String[] names = { "Codec Name", "Codec Description",
705 											"Information" };
706 	
707 			public ASFCodecList(long size) throws IOException
708 			{
709 				byte[] buffer = new byte[(int)size];
710 	
711 				in.readFully(buffer);
712 	
713 				StringBuffer sb = new StringBuffer(buffer.length);
714 				StringBuffer sbcount = new StringBuffer(buffer.length);
715 				for (int i = 0; i < buffer.length; i++) {
716 					sb.append((char) buffer[i]);
717 					sbcount.append(i % 10);
718 				}
719 	
720 				logger.debug(sb);
721 				logger.debug(sbcount);
722 	
723 				int count = (buffer[16] & 0xFF) | ((buffer[17] & 0xFF) << 8)
724 					| ((buffer[18] & 0xFF) << 16) | ((buffer[19] & 0xFF) << 32);
725 	
726 				logger.debug("count " + count);
727 	
728 				int index = 20;
729 	
730 				for (int i = 0; i < count; i++) {
731 					short ctype = getShort(buffer, index);
732 					index += 2;
733 					logger.debug("ASF reader Codec Type: " + ctype);
734 	
735 					for (int j = 0; j < 3; j++) {
736 						short csize = getShort(buffer, index);
737 						index += 2;
738 						if (j < 2) {
739 							csize *= 2;
740 							String s = getString(buffer, index, csize);
741 							logger.debug(names[j] + " " + s);
742 						}
743 						index += csize;
744 					}
745 				}
746 			}
747 	
748 			public short getShort(byte[] buffer, int offset)
749 			{
750 				return (short) ((buffer[offset] & 0xFF)
751 								| ((buffer[offset + 1] & 0xFF) << 8));
752 			}
753 	
754 			public String getString(byte[] buffer, int offset, int length)
755 			{
756 				StringBuffer sb = new StringBuffer(length);
757 	
758 				for (int i = 0; i < length && buffer[i] != '\0'; i += 2) {
759 					sb.append((char) buffer[i]);
760 				}
761 	
762 				return sb.toString();
763 			}
764 	
765 	    }
766 
767 	}
768 }