1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
51
52 private static Logger logger = Logger.getLogger(VideoMetaInfo.class);
53
54 private MetaInfoFile file;
55
56
57
58 public VideoMetaInfo(MetaInfoFile file)
59 {
60 this.file = file;
61 }
62
63
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
213 setLength((int) (totalFrames * (microSecPerFrame / 1000000L)));
214 in.skipBytes(12);
215
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;
582
583 long create_time;
584
585 long pkts_count;
586
587 public long play_time;
588
589 long send_time;
590
591 int preroll;
592
593 int ignore;
594 int flags;
595
596
597 int min_pktsize;
598
599 int max_pktsize;
600
601 int max_bitrate;
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;
627 public GUID error_guid;
628 public long time_offset;
629 public int stream_size;
630 public int error_size;
631 public short stream;
632 public int reserved;
633
634
635
636
637
638
639 public int width;
640 public int height;
641 public byte flags;
642 public short data_size;
643
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 }