1: <?php
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55: define('GETID3_FLV_TAG_AUDIO', 8);
56: define('GETID3_FLV_TAG_VIDEO', 9);
57: define('GETID3_FLV_TAG_META', 18);
58:
59: define('GETID3_FLV_VIDEO_H263', 2);
60: define('GETID3_FLV_VIDEO_SCREEN', 3);
61: define('GETID3_FLV_VIDEO_VP6FLV', 4);
62: define('GETID3_FLV_VIDEO_VP6FLV_ALPHA', 5);
63: define('GETID3_FLV_VIDEO_SCREENV2', 6);
64: define('GETID3_FLV_VIDEO_H264', 7);
65:
66: define('H264_AVC_SEQUENCE_HEADER', 0);
67: define('H264_PROFILE_BASELINE', 66);
68: define('H264_PROFILE_MAIN', 77);
69: define('H264_PROFILE_EXTENDED', 88);
70: define('H264_PROFILE_HIGH', 100);
71: define('H264_PROFILE_HIGH10', 110);
72: define('H264_PROFILE_HIGH422', 122);
73: define('H264_PROFILE_HIGH444', 144);
74: define('H264_PROFILE_HIGH444_PREDICTIVE', 244);
75:
76: class getid3_flv extends getid3_handler {
77:
78: const magic = 'FLV';
79:
80: public $max_frames = 100000;
81:
82: public function Analyze() {
83: $info = &$this->getid3->info;
84:
85: $this->fseek($info['avdataoffset']);
86:
87: $FLVdataLength = $info['avdataend'] - $info['avdataoffset'];
88: $FLVheader = $this->fread(5);
89:
90: $info['fileformat'] = 'flv';
91: $info['flv']['header']['signature'] = substr($FLVheader, 0, 3);
92: $info['flv']['header']['version'] = getid3_lib::BigEndian2Int(substr($FLVheader, 3, 1));
93: $TypeFlags = getid3_lib::BigEndian2Int(substr($FLVheader, 4, 1));
94:
95: if ($info['flv']['header']['signature'] != self::magic) {
96: $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes(self::magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['flv']['header']['signature']).'"';
97: unset($info['flv'], $info['fileformat']);
98: return false;
99: }
100:
101: $info['flv']['header']['hasAudio'] = (bool) ($TypeFlags & 0x04);
102: $info['flv']['header']['hasVideo'] = (bool) ($TypeFlags & 0x01);
103:
104: $FrameSizeDataLength = getid3_lib::BigEndian2Int($this->fread(4));
105: $FLVheaderFrameLength = 9;
106: if ($FrameSizeDataLength > $FLVheaderFrameLength) {
107: $this->fseek($FrameSizeDataLength - $FLVheaderFrameLength, SEEK_CUR);
108: }
109: $Duration = 0;
110: $found_video = false;
111: $found_audio = false;
112: $found_meta = false;
113: $found_valid_meta_playtime = false;
114: $tagParseCount = 0;
115: $info['flv']['framecount'] = array('total'=>0, 'audio'=>0, 'video'=>0);
116: $flv_framecount = &$info['flv']['framecount'];
117: while ((($this->ftell() + 16) < $info['avdataend']) && (($tagParseCount++ <= $this->max_frames) || !$found_valid_meta_playtime)) {
118: $ThisTagHeader = $this->fread(16);
119:
120: $PreviousTagLength = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 0, 4));
121: $TagType = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 4, 1));
122: $DataLength = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 5, 3));
123: $Timestamp = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 8, 3));
124: $LastHeaderByte = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 15, 1));
125: $NextOffset = $this->ftell() - 1 + $DataLength;
126: if ($Timestamp > $Duration) {
127: $Duration = $Timestamp;
128: }
129:
130: $flv_framecount['total']++;
131: switch ($TagType) {
132: case GETID3_FLV_TAG_AUDIO:
133: $flv_framecount['audio']++;
134: if (!$found_audio) {
135: $found_audio = true;
136: $info['flv']['audio']['audioFormat'] = ($LastHeaderByte >> 4) & 0x0F;
137: $info['flv']['audio']['audioRate'] = ($LastHeaderByte >> 2) & 0x03;
138: $info['flv']['audio']['audioSampleSize'] = ($LastHeaderByte >> 1) & 0x01;
139: $info['flv']['audio']['audioType'] = $LastHeaderByte & 0x01;
140: }
141: break;
142:
143: case GETID3_FLV_TAG_VIDEO:
144: $flv_framecount['video']++;
145: if (!$found_video) {
146: $found_video = true;
147: $info['flv']['video']['videoCodec'] = $LastHeaderByte & 0x07;
148:
149: $FLVvideoHeader = $this->fread(11);
150:
151: if ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H264) {
152:
153:
154: $AVCPacketType = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 0, 1));
155: if ($AVCPacketType == H264_AVC_SEQUENCE_HEADER) {
156:
157: $configurationVersion = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 1));
158: $AVCProfileIndication = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 1));
159: $profile_compatibility = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 1));
160: $lengthSizeMinusOne = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 7, 1));
161: $numOfSequenceParameterSets = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 8, 1));
162:
163: if (($numOfSequenceParameterSets & 0x1F) != 0) {
164:
165:
166:
167: $spsSize = getid3_lib::LittleEndian2Int(substr($FLVvideoHeader, 9, 2));
168:
169: $sps = $this->fread($spsSize);
170: if (strlen($sps) == $spsSize) {
171: $spsReader = new AVCSequenceParameterSetReader($sps);
172: $spsReader->readData();
173: $info['video']['resolution_x'] = $spsReader->getWidth();
174: $info['video']['resolution_y'] = $spsReader->getHeight();
175: }
176: }
177: }
178:
179:
180: } elseif ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H263) {
181:
182: $PictureSizeType = (getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 3, 2))) >> 7;
183: $PictureSizeType = $PictureSizeType & 0x0007;
184: $info['flv']['header']['videoSizeType'] = $PictureSizeType;
185: switch ($PictureSizeType) {
186: case 0:
187:
188:
189:
190:
191:
192:
193:
194: $PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 2)) >> 7;
195: $PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 2)) >> 7;
196: $info['video']['resolution_x'] = $PictureSizeEnc['x'] & 0xFF;
197: $info['video']['resolution_y'] = $PictureSizeEnc['y'] & 0xFF;
198: break;
199:
200: case 1:
201: $PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 3)) >> 7;
202: $PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 3)) >> 7;
203: $info['video']['resolution_x'] = $PictureSizeEnc['x'] & 0xFFFF;
204: $info['video']['resolution_y'] = $PictureSizeEnc['y'] & 0xFFFF;
205: break;
206:
207: case 2:
208: $info['video']['resolution_x'] = 352;
209: $info['video']['resolution_y'] = 288;
210: break;
211:
212: case 3:
213: $info['video']['resolution_x'] = 176;
214: $info['video']['resolution_y'] = 144;
215: break;
216:
217: case 4:
218: $info['video']['resolution_x'] = 128;
219: $info['video']['resolution_y'] = 96;
220: break;
221:
222: case 5:
223: $info['video']['resolution_x'] = 320;
224: $info['video']['resolution_y'] = 240;
225: break;
226:
227: case 6:
228: $info['video']['resolution_x'] = 160;
229: $info['video']['resolution_y'] = 120;
230: break;
231:
232: default:
233: $info['video']['resolution_x'] = 0;
234: $info['video']['resolution_y'] = 0;
235: break;
236:
237: }
238:
239: } elseif ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_VP6FLV_ALPHA) {
240:
241:
242: if (!isset($info['video']['resolution_x'])) {
243: $PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 2));
244: $PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 7, 2));
245: $info['video']['resolution_x'] = ($PictureSizeEnc['x'] & 0xFF) << 3;
246: $info['video']['resolution_y'] = ($PictureSizeEnc['y'] & 0xFF) << 3;
247: }
248:
249:
250: }
251: if (!empty($info['video']['resolution_x']) && !empty($info['video']['resolution_y'])) {
252: $info['video']['pixel_aspect_ratio'] = $info['video']['resolution_x'] / $info['video']['resolution_y'];
253: }
254: }
255: break;
256:
257:
258: case GETID3_FLV_TAG_META:
259: if (!$found_meta) {
260: $found_meta = true;
261: $this->fseek(-1, SEEK_CUR);
262: $datachunk = $this->fread($DataLength);
263: $AMFstream = new AMFStream($datachunk);
264: $reader = new AMFReader($AMFstream);
265: $eventName = $reader->readData();
266: $info['flv']['meta'][$eventName] = $reader->readData();
267: unset($reader);
268:
269: $copykeys = array('framerate'=>'frame_rate', 'width'=>'resolution_x', 'height'=>'resolution_y', 'audiodatarate'=>'bitrate', 'videodatarate'=>'bitrate');
270: foreach ($copykeys as $sourcekey => $destkey) {
271: if (isset($info['flv']['meta']['onMetaData'][$sourcekey])) {
272: switch ($sourcekey) {
273: case 'width':
274: case 'height':
275: $info['video'][$destkey] = intval(round($info['flv']['meta']['onMetaData'][$sourcekey]));
276: break;
277: case 'audiodatarate':
278: $info['audio'][$destkey] = getid3_lib::CastAsInt(round($info['flv']['meta']['onMetaData'][$sourcekey] * 1000));
279: break;
280: case 'videodatarate':
281: case 'frame_rate':
282: default:
283: $info['video'][$destkey] = $info['flv']['meta']['onMetaData'][$sourcekey];
284: break;
285: }
286: }
287: }
288: if (!empty($info['flv']['meta']['onMetaData']['duration'])) {
289: $found_valid_meta_playtime = true;
290: }
291: }
292: break;
293:
294: default:
295:
296: break;
297: }
298: $this->fseek($NextOffset);
299: }
300:
301: $info['playtime_seconds'] = $Duration / 1000;
302: if ($info['playtime_seconds'] > 0) {
303: $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
304: }
305:
306: if ($info['flv']['header']['hasAudio']) {
307: $info['audio']['codec'] = self::audioFormatLookup($info['flv']['audio']['audioFormat']);
308: $info['audio']['sample_rate'] = self::audioRateLookup($info['flv']['audio']['audioRate']);
309: $info['audio']['bits_per_sample'] = self::audioBitDepthLookup($info['flv']['audio']['audioSampleSize']);
310:
311: $info['audio']['channels'] = $info['flv']['audio']['audioType'] + 1;
312: $info['audio']['lossless'] = ($info['flv']['audio']['audioFormat'] ? false : true);
313: $info['audio']['dataformat'] = 'flv';
314: }
315: if (!empty($info['flv']['header']['hasVideo'])) {
316: $info['video']['codec'] = self::videoCodecLookup($info['flv']['video']['videoCodec']);
317: $info['video']['dataformat'] = 'flv';
318: $info['video']['lossless'] = false;
319: }
320:
321:
322: if (!empty($info['flv']['meta']['onMetaData']['duration'])) {
323: $info['playtime_seconds'] = $info['flv']['meta']['onMetaData']['duration'];
324: $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
325: }
326: if (isset($info['flv']['meta']['onMetaData']['audiocodecid'])) {
327: $info['audio']['codec'] = self::audioFormatLookup($info['flv']['meta']['onMetaData']['audiocodecid']);
328: }
329: if (isset($info['flv']['meta']['onMetaData']['videocodecid'])) {
330: $info['video']['codec'] = self::videoCodecLookup($info['flv']['meta']['onMetaData']['videocodecid']);
331: }
332: return true;
333: }
334:
335:
336: public static function audioFormatLookup($id) {
337: static $lookup = array(
338: 0 => 'Linear PCM, platform endian',
339: 1 => 'ADPCM',
340: 2 => 'mp3',
341: 3 => 'Linear PCM, little endian',
342: 4 => 'Nellymoser 16kHz mono',
343: 5 => 'Nellymoser 8kHz mono',
344: 6 => 'Nellymoser',
345: 7 => 'G.711A-law logarithmic PCM',
346: 8 => 'G.711 mu-law logarithmic PCM',
347: 9 => 'reserved',
348: 10 => 'AAC',
349: 11 => 'Speex',
350: 12 => false,
351: 13 => false,
352: 14 => 'mp3 8kHz',
353: 15 => 'Device-specific sound',
354: );
355: return (isset($lookup[$id]) ? $lookup[$id] : false);
356: }
357:
358: public static function audioRateLookup($id) {
359: static $lookup = array(
360: 0 => 5500,
361: 1 => 11025,
362: 2 => 22050,
363: 3 => 44100,
364: );
365: return (isset($lookup[$id]) ? $lookup[$id] : false);
366: }
367:
368: public static function audioBitDepthLookup($id) {
369: static $lookup = array(
370: 0 => 8,
371: 1 => 16,
372: );
373: return (isset($lookup[$id]) ? $lookup[$id] : false);
374: }
375:
376: public static function videoCodecLookup($id) {
377: static $lookup = array(
378: GETID3_FLV_VIDEO_H263 => 'Sorenson H.263',
379: GETID3_FLV_VIDEO_SCREEN => 'Screen video',
380: GETID3_FLV_VIDEO_VP6FLV => 'On2 VP6',
381: GETID3_FLV_VIDEO_VP6FLV_ALPHA => 'On2 VP6 with alpha channel',
382: GETID3_FLV_VIDEO_SCREENV2 => 'Screen video v2',
383: GETID3_FLV_VIDEO_H264 => 'Sorenson H.264',
384: );
385: return (isset($lookup[$id]) ? $lookup[$id] : false);
386: }
387: }
388:
389: class AMFStream {
390: public $bytes;
391: public $pos;
392:
393: public function __construct(&$bytes) {
394: $this->bytes =& $bytes;
395: $this->pos = 0;
396: }
397:
398: public function readByte() {
399: return getid3_lib::BigEndian2Int(substr($this->bytes, $this->pos++, 1));
400: }
401:
402: public function readInt() {
403: return ($this->readByte() << 8) + $this->readByte();
404: }
405:
406: public function readLong() {
407: return ($this->readByte() << 24) + ($this->readByte() << 16) + ($this->readByte() << 8) + $this->readByte();
408: }
409:
410: public function readDouble() {
411: return getid3_lib::BigEndian2Float($this->read(8));
412: }
413:
414: public function readUTF() {
415: $length = $this->readInt();
416: return $this->read($length);
417: }
418:
419: public function readLongUTF() {
420: $length = $this->readLong();
421: return $this->read($length);
422: }
423:
424: public function read($length) {
425: $val = substr($this->bytes, $this->pos, $length);
426: $this->pos += $length;
427: return $val;
428: }
429:
430: public function peekByte() {
431: $pos = $this->pos;
432: $val = $this->readByte();
433: $this->pos = $pos;
434: return $val;
435: }
436:
437: public function peekInt() {
438: $pos = $this->pos;
439: $val = $this->readInt();
440: $this->pos = $pos;
441: return $val;
442: }
443:
444: public function peekLong() {
445: $pos = $this->pos;
446: $val = $this->readLong();
447: $this->pos = $pos;
448: return $val;
449: }
450:
451: public function peekDouble() {
452: $pos = $this->pos;
453: $val = $this->readDouble();
454: $this->pos = $pos;
455: return $val;
456: }
457:
458: public function peekUTF() {
459: $pos = $this->pos;
460: $val = $this->readUTF();
461: $this->pos = $pos;
462: return $val;
463: }
464:
465: public function peekLongUTF() {
466: $pos = $this->pos;
467: $val = $this->readLongUTF();
468: $this->pos = $pos;
469: return $val;
470: }
471: }
472:
473: class AMFReader {
474: public $stream;
475:
476: public function __construct(&$stream) {
477: $this->stream =& $stream;
478: }
479:
480: public function readData() {
481: $value = null;
482:
483: $type = $this->stream->readByte();
484: switch ($type) {
485:
486:
487: case 0:
488: $value = $this->readDouble();
489: break;
490:
491:
492: case 1:
493: $value = $this->readBoolean();
494: break;
495:
496:
497: case 2:
498: $value = $this->readString();
499: break;
500:
501:
502: case 3:
503: $value = $this->readObject();
504: break;
505:
506:
507: case 6:
508: return null;
509: break;
510:
511:
512: case 8:
513: $value = $this->readMixedArray();
514: break;
515:
516:
517: case 10:
518: $value = $this->readArray();
519: break;
520:
521:
522: case 11:
523: $value = $this->readDate();
524: break;
525:
526:
527: case 13:
528: $value = $this->readLongString();
529: break;
530:
531:
532: case 15:
533: $value = $this->readXML();
534: break;
535:
536:
537: case 16:
538: $value = $this->readTypedObject();
539: break;
540:
541:
542: default:
543: $value = '(unknown or unsupported data type)';
544: break;
545: }
546:
547: return $value;
548: }
549:
550: public function readDouble() {
551: return $this->stream->readDouble();
552: }
553:
554: public function readBoolean() {
555: return $this->stream->readByte() == 1;
556: }
557:
558: public function readString() {
559: return $this->stream->readUTF();
560: }
561:
562: public function readObject() {
563:
564:
565:
566: $data = array();
567:
568: while ($key = $this->stream->readUTF()) {
569: $data[$key] = $this->readData();
570: }
571:
572: if (($key == '') && ($this->stream->peekByte() == 0x09)) {
573:
574: $this->stream->readByte();
575: }
576: return $data;
577: }
578:
579: public function readMixedArray() {
580:
581: $highestIndex = $this->stream->readLong();
582:
583: $data = array();
584:
585: while ($key = $this->stream->readUTF()) {
586: if (is_numeric($key)) {
587: $key = (float) $key;
588: }
589: $data[$key] = $this->readData();
590: }
591:
592: if (($key == '') && ($this->stream->peekByte() == 0x09)) {
593:
594: $this->stream->readByte();
595: }
596:
597: return $data;
598: }
599:
600: public function readArray() {
601: $length = $this->stream->readLong();
602: $data = array();
603:
604: for ($i = 0; $i < $length; $i++) {
605: $data[] = $this->readData();
606: }
607: return $data;
608: }
609:
610: public function readDate() {
611: $timestamp = $this->stream->readDouble();
612: $timezone = $this->stream->readInt();
613: return $timestamp;
614: }
615:
616: public function readLongString() {
617: return $this->stream->readLongUTF();
618: }
619:
620: public function readXML() {
621: return $this->stream->readLongUTF();
622: }
623:
624: public function readTypedObject() {
625: $className = $this->stream->readUTF();
626: return $this->readObject();
627: }
628: }
629:
630: class AVCSequenceParameterSetReader {
631: public $sps;
632: public $start = 0;
633: public $currentBytes = 0;
634: public $currentBits = 0;
635: public $width;
636: public $height;
637:
638: public function __construct($sps) {
639: $this->sps = $sps;
640: }
641:
642: public function readData() {
643: $this->skipBits(8);
644: $this->skipBits(8);
645: $profile = $this->getBits(8);
646: if ($profile > 0) {
647: $this->skipBits(8);
648: $level_idc = $this->getBits(8);
649: $this->expGolombUe();
650: $this->expGolombUe();
651: $picOrderType = $this->expGolombUe();
652: if ($picOrderType == 0) {
653: $this->expGolombUe();
654: } elseif ($picOrderType == 1) {
655: $this->skipBits(1);
656: $this->expGolombSe();
657: $this->expGolombSe();
658: $num_ref_frames_in_pic_order_cnt_cycle = $this->expGolombUe();
659: for ($i = 0; $i < $num_ref_frames_in_pic_order_cnt_cycle; $i++) {
660: $this->expGolombSe();
661: }
662: }
663: $this->expGolombUe();
664: $this->skipBits(1);
665: $pic_width_in_mbs_minus1 = $this->expGolombUe();
666: $pic_height_in_map_units_minus1 = $this->expGolombUe();
667:
668: $frame_mbs_only_flag = $this->getBits(1);
669: if ($frame_mbs_only_flag == 0) {
670: $this->skipBits(1);
671: }
672: $this->skipBits(1);
673: $frame_cropping_flag = $this->getBits(1);
674:
675: $frame_crop_left_offset = 0;
676: $frame_crop_right_offset = 0;
677: $frame_crop_top_offset = 0;
678: $frame_crop_bottom_offset = 0;
679:
680: if ($frame_cropping_flag) {
681: $frame_crop_left_offset = $this->expGolombUe();
682: $frame_crop_right_offset = $this->expGolombUe();
683: $frame_crop_top_offset = $this->expGolombUe();
684: $frame_crop_bottom_offset = $this->expGolombUe();
685: }
686: $this->skipBits(1);
687:
688:
689: $this->width = (($pic_width_in_mbs_minus1 + 1) * 16) - ($frame_crop_left_offset * 2) - ($frame_crop_right_offset * 2);
690: $this->height = ((2 - $frame_mbs_only_flag) * ($pic_height_in_map_units_minus1 + 1) * 16) - ($frame_crop_top_offset * 2) - ($frame_crop_bottom_offset * 2);
691: }
692: }
693:
694: public function skipBits($bits) {
695: $newBits = $this->currentBits + $bits;
696: $this->currentBytes += (int)floor($newBits / 8);
697: $this->currentBits = $newBits % 8;
698: }
699:
700: public function getBit() {
701: $result = (getid3_lib::BigEndian2Int(substr($this->sps, $this->currentBytes, 1)) >> (7 - $this->currentBits)) & 0x01;
702: $this->skipBits(1);
703: return $result;
704: }
705:
706: public function getBits($bits) {
707: $result = 0;
708: for ($i = 0; $i < $bits; $i++) {
709: $result = ($result << 1) + $this->getBit();
710: }
711: return $result;
712: }
713:
714: public function expGolombUe() {
715: $significantBits = 0;
716: $bit = $this->getBit();
717: while ($bit == 0) {
718: $significantBits++;
719: $bit = $this->getBit();
720:
721: if ($significantBits > 31) {
722:
723: return 0;
724: }
725: }
726: return (1 << $significantBits) + $this->getBits($significantBits) - 1;
727: }
728:
729: public function expGolombSe() {
730: $result = $this->expGolombUe();
731: if (($result & 0x01) == 0) {
732: return -($result >> 1);
733: } else {
734: return ($result + 1) >> 1;
735: }
736: }
737:
738: public function getWidth() {
739: return $this->width;
740: }
741:
742: public function getHeight() {
743: return $this->height;
744: }
745: }
746: