diff options
Diffstat (limited to 'inc/lib/webm')
| -rw-r--r-- | inc/lib/webm/README.md | 26 | ||||
| -rw-r--r-- | inc/lib/webm/matroska-elements.txt | 224 | ||||
| -rw-r--r-- | inc/lib/webm/matroska.php | 524 | ||||
| -rw-r--r-- | inc/lib/webm/posthandler.php | 49 | ||||
| -rw-r--r-- | inc/lib/webm/videodata.php | 173 |
5 files changed, 996 insertions, 0 deletions
diff --git a/inc/lib/webm/README.md b/inc/lib/webm/README.md new file mode 100644 index 00000000..baa7a641 --- /dev/null +++ b/inc/lib/webm/README.md @@ -0,0 +1,26 @@ +This directory contains files being the integration of containerchan with vichan-devel imageboards. + +Containerchan allows posting of webm files, like they were the regular images. + +An original board using this code can be found at: +http://containerchan.org/tb/demo/ + +The original repo containing the containerchan (possibly with no Tinyboard integration) can be found here: +https://github.com/ccd0/containerchan + + +Be aware that this is beta software. Please report any bugs you find. + +Installation +------------ + +Add these lines to inc/instance-config.php: + + $config['allowed_ext_files'][] = 'webm'; + $config['additional_javascript'][] = 'js/webm-settings.js'; + $config['additional_javascript'][] = 'js/expand-video.js'; + +License +------- + +See [LICENSE.md](https://github.com/ccd0/containerchan/blob/master/LICENSE.md). diff --git a/inc/lib/webm/matroska-elements.txt b/inc/lib/webm/matroska-elements.txt new file mode 100644 index 00000000..07f8eecc --- /dev/null +++ b/inc/lib/webm/matroska-elements.txt @@ -0,0 +1,224 @@ +a45dfa3 container EBML root +286 uint EBMLVersion a45dfa3 +2f7 uint EBMLReadVersion a45dfa3 +2f2 uint EBMLMaxIDLength a45dfa3 +2f3 uint EBMLMaxSizeLength a45dfa3 +282 string DocType a45dfa3 +287 uint DocTypeVersion a45dfa3 +285 uint DocTypeReadVersion a45dfa3 +6c binary Void * +3f binary CRC-32 * +b538667 container SignatureSlot * +3e8a uint SignatureAlgo b538667 +3e9a uint SignatureHash b538667 +3ea5 binary SignaturePublicKey b538667 +3eb5 binary Signature b538667 +3e5b container SignatureElements b538667 +3e7b container SignatureElementList 3e5b +2532 binary SignedElement 3e7b +8538067 container Segment root +14d9b74 container SeekHead 8538067 +dbb container Seek 14d9b74 +13ab binary SeekID dbb +13ac uint SeekPosition dbb +549a966 container Info 8538067 +33a4 binary SegmentUID 549a966 +3384 string SegmentFilename 549a966 +1cb923 binary PrevUID 549a966 +1c83ab string PrevFilename 549a966 +1eb923 binary NextUID 549a966 +1e83bb string NextFilename 549a966 +444 binary SegmentFamily 549a966 +2924 container ChapterTranslate 549a966 +29fc uint ChapterTranslateEditionUID 2924 +29bf uint ChapterTranslateCodec 2924 +29a5 binary ChapterTranslateID 2924 +ad7b1 uint TimecodeScale 549a966 +489 float Duration 549a966 +461 date DateUTC 549a966 +3ba9 string Title 549a966 +d80 string MuxingApp 549a966 +1741 string WritingApp 549a966 +f43b675 container Cluster 8538067 +67 uint Timecode f43b675 +1854 container SilentTracks f43b675 +18d7 uint SilentTrackNumber 1854 +27 uint Position f43b675 +2b uint PrevSize f43b675 +23 binary SimpleBlock f43b675 +20 container BlockGroup f43b675 +21 binary Block 20 +22 binary BlockVirtual 20 +35a1 container BlockAdditions 20 +26 container BlockMore 35a1 +6e uint BlockAddID 26 +25 binary BlockAdditional 26 +1b uint BlockDuration 20 +7a uint ReferencePriority 20 +7b int ReferenceBlock 20 +7d int ReferenceVirtual 20 +24 binary CodecState 20 +35a2 int DiscardPadding 20 +e container Slices 20 +68 container TimeSlice e +4c uint LaceNumber 68 +4d uint FrameNumber 68 +4b uint BlockAdditionID 68 +4e uint Delay 68 +4f uint SliceDuration 68 +48 container ReferenceFrame 20 +49 uint ReferenceOffset 48 +4a uint ReferenceTimeCode 48 +2f binary EncryptedBlock f43b675 +654ae6b container Tracks 8538067 +2e container TrackEntry 654ae6b +57 uint TrackNumber 2e +33c5 uint TrackUID 2e +3 uint TrackType 2e +39 uint FlagEnabled 2e +8 uint FlagDefault 2e +15aa uint FlagForced 2e +1c uint FlagLacing 2e +2de7 uint MinCache 2e +2df8 uint MaxCache 2e +3e383 uint DefaultDuration 2e +34e7a uint DefaultDecodedFieldDuration 2e +3314f float TrackTimecodeScale 2e +137f int TrackOffset 2e +15ee uint MaxBlockAdditionID 2e +136e string Name 2e +2b59c string Language 2e +6 string CodecID 2e +23a2 binary CodecPrivate 2e +58688 string CodecName 2e +3446 uint AttachmentLink 2e +1a9697 string CodecSettings 2e +1b4040 string CodecInfoURL 2e +6b240 string CodecDownloadURL 2e +2a uint CodecDecodeAll 2e +2fab uint TrackOverlay 2e +16aa uint CodecDelay 2e +16bb uint SeekPreRoll 2e +2624 container TrackTranslate 2e +26fc uint TrackTranslateEditionUID 2624 +26bf uint TrackTranslateCodec 2624 +26a5 binary TrackTranslateTrackID 2624 +60 container Video 2e +1a uint FlagInterlaced 60 +13b8 uint StereoMode 60 +13c0 uint AlphaMode 60 +13b9 uint OldStereoMode 60 +30 uint PixelWidth 60 +3a uint PixelHeight 60 +14aa uint PixelCropBottom 60 +14bb uint PixelCropTop 60 +14cc uint PixelCropLeft 60 +14dd uint PixelCropRight 60 +14b0 uint DisplayWidth 60 +14ba uint DisplayHeight 60 +14b2 uint DisplayUnit 60 +14b3 uint AspectRatioType 60 +eb524 binary ColourSpace 60 +fb523 float GammaValue 60 +383e3 float FrameRate 60 +61 container Audio 2e +35 float SamplingFrequency 61 +38b5 float OutputSamplingFrequency 61 +1f uint Channels 61 +3d7b binary ChannelPositions 61 +2264 uint BitDepth 61 +62 container TrackOperation 2e +63 container TrackCombinePlanes 62 +64 container TrackPlane 63 +65 uint TrackPlaneUID 64 +66 uint TrackPlaneType 64 +69 container TrackJoinBlocks 62 +6d uint TrackJoinUID 69 +40 uint TrickTrackUID 2e +41 binary TrickTrackSegmentUID 2e +46 uint TrickTrackFlag 2e +47 uint TrickMasterTrackUID 2e +44 binary TrickMasterTrackSegmentUID 2e +2d80 container ContentEncodings 2e +2240 container ContentEncoding 2d80 +1031 uint ContentEncodingOrder 2240 +1032 uint ContentEncodingScope 2240 +1033 uint ContentEncodingType 2240 +1034 container ContentCompression 2240 +254 uint ContentCompAlgo 1034 +255 binary ContentCompSettings 1034 +1035 container ContentEncryption 2240 +7e1 uint ContentEncAlgo 1035 +7e2 binary ContentEncKeyID 1035 +7e3 binary ContentSignature 1035 +7e4 binary ContentSigKeyID 1035 +7e5 uint ContentSigAlgo 1035 +7e6 uint ContentSigHashAlgo 1035 +c53bb6b container Cues 8538067 +3b container CuePoint c53bb6b +33 uint CueTime 3b +37 container CueTrackPositions 3b +77 uint CueTrack 37 +71 uint CueClusterPosition 37 +70 uint CueRelativePosition 37 +32 uint CueDuration 37 +1378 uint CueBlockNumber 37 +6a uint CueCodecState 37 +5b container CueReference 37 +16 uint CueRefTime 5b +17 uint CueRefCluster 5b +135f uint CueRefNumber 5b +6b uint CueRefCodecState 5b +941a469 container Attachments 8538067 +21a7 container AttachedFile 941a469 +67e string FileDescription 21a7 +66e string FileName 21a7 +660 string FileMimeType 21a7 +65c binary FileData 21a7 +6ae uint FileUID 21a7 +675 binary FileReferral 21a7 +661 uint FileUsedStartTime 21a7 +662 uint FileUsedEndTime 21a7 +43a770 container Chapters 8538067 +5b9 container EditionEntry 43a770 +5bc uint EditionUID 5b9 +5bd uint EditionFlagHidden 5b9 +5db uint EditionFlagDefault 5b9 +5dd uint EditionFlagOrdered 5b9 +36 container ChapterAtom 5b9 36 +33c4 uint ChapterUID 36 +1654 string ChapterStringUID 36 +11 uint ChapterTimeStart 36 +12 uint ChapterTimeEnd 36 +18 uint ChapterFlagHidden 36 +598 uint ChapterFlagEnabled 36 +2e67 binary ChapterSegmentUID 36 +2ebc uint ChapterSegmentEditionUID 36 +23c3 uint ChapterPhysicalEquiv 36 +f container ChapterTrack 36 +9 uint ChapterTrackNumber f +0 container ChapterDisplay 36 +5 string ChapString 0 +37c string ChapLanguage 0 +37e string ChapCountry 0 +2944 container ChapProcess 36 +2955 uint ChapProcessCodecID 2944 +50d binary ChapProcessPrivate 2944 +2911 container ChapProcessCommand 2944 +2922 uint ChapProcessTime 2911 +2933 binary ChapProcessData 2911 +254c367 container Tags 8538067 +3373 container Tag 254c367 +23c0 container Targets 3373 +28ca uint TargetTypeValue 23c0 +23ca string TargetType 23c0 +23c5 uint TagTrackUID 23c0 +23c9 uint TagEditionUID 23c0 +23c4 uint TagChapterUID 23c0 +23c6 uint TagAttachmentUID 23c0 +27c8 container SimpleTag 3373 27c8 +5a3 string TagName 27c8 +47a string TagLanguage 27c8 +484 uint TagDefault 27c8 +487 string TagString 27c8 +485 binary TagBinary 27c8 diff --git a/inc/lib/webm/matroska.php b/inc/lib/webm/matroska.php new file mode 100644 index 00000000..5f1c38ee --- /dev/null +++ b/inc/lib/webm/matroska.php @@ -0,0 +1,524 @@ +<?php +/* This file is dedicated to the public domain; you may do as you wish with it. */ + +// Information needed to parse an element type +class EBMLElementType { + public $name; + public $datatype; + public $validParents; +} + +// Information needed to parse all possible element types in a document +class EBMLElementTypeList { + private $_els; + private $_ids; + + public function __construct($filename) { + $lines = file($filename); + foreach($lines as $line) { + $fields = explode(' ', trim($line)); + $t = new EBMLElementType; + $id = hexdec($fields[0]); + $t->datatype = $fields[1]; + $t->name = $fields[2]; + $t->validParents = array(); + for ($i = 0; $i + 3 < count($fields); $i++) { + if ($fields[$i+3] == '*' || $fields[$i+3] == 'root') { + $t->validParents[$i] = $fields[$i+3]; + } else { + $t->validParents[$i] = hexdec($fields[$i+3]); + } + } + $this->_els[$id] = $t; + $this->_ids[strtoupper($t->name)] = $id; + } + } + + public function exists($id) { + return isset($this->_els[$id]); + } + + public function name($id) { + if (!isset($this->_els[$id])) return NULL; + return $this->_els[$id]->name; + } + + public function id($name) { + $name = strtoupper($name); + if (!isset($this->_ids[$name])) return NULL; + return $this->_ids[$name]; + } + + public function datatype($id) { + if ($id == 'root') return 'container'; + if (!isset($this->_els[$id])) return 'binary'; + return $this->_els[$id]->datatype; + } + + public function validChild($id1, $id2) { + if (!isset($this->_els[$id2])) return TRUE; + $parents = $this->_els[$id2]->validParents; + return in_array('*', $parents) || in_array($id1, $parents); + } +} + +// Matroska element types +global $EBML_ELEMENTS; +$EBML_ELEMENTS = new EBMLElementTypeList(dirname(__FILE__) . '/matroska-elements.txt'); + +// Decode big-endian integer +function ebmlDecodeInt($data, $signed=FALSE, $carryIn=0) { + $n = $carryIn; + if (strlen($data) > 8) throw new Exception('not supported: integer too long'); + for ($i = 0; $i < strlen($data); $i++) { + if ($n > (PHP_INT_MAX >> 8) || $n < ((-PHP_INT_MAX-1) >> 8)) { + $n = floatval($n); + } + $n = $n * 0x100 + ord($data[$i]); + if ($i == 0 && $signed && ($n & 0x80) != 0) { + $n -= 0x100; + } + } + return $n; +} + +// Decode big-endian IEEE float +function ebmlDecodeFloat($data) { + switch (strlen($data)) { + case 0: + return 0; + case 4: + switch(pack('f', 1e9)) { + case '(knN': + $arr = unpack('f', strrev($data)); + return $arr[1]; + case 'Nnk(': + $arr = unpack('f', $data); + return $arr[1]; + default: + error_log('cannot decode floats'); + return NULL; + } + case 8: + switch(pack('d', 1e9)) { + case "\x00\x00\x00\x00\x65\xcd\xcd\x41": + $arr = unpack('d', strrev($data)); + return $arr[1]; + case "\x41\xcd\xcd\x65\x00\x00\x00\x00": + $arr = unpack('d', $data); + return $arr[1]; + default: + error_log('cannot decode floats'); + return NULL; + } + default: + error_log('unsupported float length'); + return NULL; + } +} + +// Decode big-endian signed offset from Jan 01, 2000 in nanoseconds +// Convert to offset from Jan 01, 1970 in seconds +function ebmlDecodeDate($data) { + return ebmlDecodeInt($data, TRUE) * 1e-9 + 946684800; +} + +// Decode data of specified datatype +function ebmlDecode($data, $datatype) { + switch ($datatype) { + case 'int': return ebmlDecodeInt($data, TRUE); + case 'uint': return ebmlDecodeInt($data, FALSE); + case 'float': return ebmlDecodeFloat($data); + case 'string': return chop($data, "\0"); + case 'date': return ebmlDecodeDate($data); + case 'binary': return $data; + default: throw new Exception('unknown datatype'); + } +} + +// Methods for reading data from section of EBML file +class EBMLReader { + private $_fileHandle; + private $_offset; + private $_size; + private $_position; + + public function __construct($fileHandle, $offset=0, $size=NULL) { + $this->_fileHandle = $fileHandle; + $this->_offset = $offset; + $this->_size = $size; + $this->_position = 0; + } + + // Tell position within data section + public function position() { + return $this->_position; + } + + // Set position within data section + public function setPosition($position) { + $this->_position = $position; + } + + // Total size of data section (NULL if unknown) + public function size() { + return $this->_size; + } + + // Set end of data section + public function setSize($size) { + if ($this->_size === NULL) { + $this->_size = $size; + } else { + throw new Exception('size already set'); + } + } + + // Determine whether we are at end of data + public function endOfData() { + if ($this->_size === NULL) { + fseek($this->_fileHandle, $this->_offset + $this->_position); + fread($this->_fileHandle, 1); + if (feof($this->_fileHandle)) { + $this->_size = $this->_position; + return TRUE; + } else { + return FALSE; + } + } else { + return $this->_position >= $this->_size; + } + } + + // Create EBMLReader containing $size bytes and advance + public function nextSlice($size) { + $slice = new EBMLReader($this->_fileHandle, $this->_offset + $this->_position, $size); + if ($size !== NULL) { + $this->_position += $size; + if ($this->_size !== NULL && $this->_position > $this->_size) { + throw new Exception('unexpected end of data'); + } + } + return $slice; + } + + // Read entire region + public function readAll() { + if ($this->_size == 0) return ''; + if ($this->_size === NULL) throw new Exception('unknown length'); + fseek($this->_fileHandle, $this->_offset); + $data = fread($this->_fileHandle, $this->_size); + if ($data === FALSE || strlen($data) != $this->_size) { + throw new Exception('error reading from file'); + } + return $data; + } + + // Read $size bytes + public function read($size) { + return $this->nextSlice($size)->readAll(); + } + + // Read variable-length integer + public function readVarInt($signed=FALSE) { + // Read size and remove flag + $n = ord($this->read(1)); + $size = 0; + if ($n == 0) { + throw new Exception('not supported: variable-length integer too long'); + } + $flag = 0x80; + while (($n & $flag) == 0) { + $flag = $flag >> 1; + $size++; + } + $n -= $flag; + + // Read remaining data + $rawInt = $this->read($size); + + // Check for all ones + if ($n == $flag - 1 && $rawInt == str_repeat("\xFF", $size)) { + return NULL; + } + + // Range shift for signed integers + if ($signed) { + if ($flag == 0x01) { + $n = ord($rawInt[0]) - 0x80; + $rawInt = $rawInt.substr(1); + } else { + $n -= ($flag >> 1); + } + } + + // Convert to integer + $n = ebmlDecodeInt($rawInt, FALSE, $n); + + // Range shift for signed integers + if ($signed) { + if ($n == PHP_INT_MAX) { + $n = floatval($n); + } + $n++; + } + + return $n; + } +} + +// EBML element +class EBMLElement { + private $_id; + private $_name; + private $_datatype; + private $_content; + private $_headSize; + + public function __construct($id, $content, $headSize) { + global $EBML_ELEMENTS; + $this->_id = $id; + $this->_name = $EBML_ELEMENTS->name($this->_id); + $this->_datatype = $EBML_ELEMENTS->datatype($this->_id); + $this->_content = $content; + $this->_headSize = $headSize; + } + + public function id() {return $this->_id;} + public function name() {return $this->_name;} + public function datatype() {return $this->_datatype;} + public function content() {return $this->_content;} + public function headSize() {return $this->_headSize;} + + // Total size of element (including ID and datasize) + public function size() { + return $this->_headSize + $this->_content->size(); + } + + // Read and interpret content + public function value() { + if ($this->_datatype == 'binary') { + return $this->_content; + } else { + return ebmlDecode($this->_content->readAll(), $this->_datatype); + } + } +} + +// Iterate over EBML elements in data +class EBMLElementList extends EBMLElement implements Iterator { + private $_cache; + private $_position; + private static $MAX_ELEMENTS = 10000; + + public function __construct($id, $content, $headSize) { + parent::__construct($id, $content, $headSize); + $this->_cache = array(); + $this->_position = 0; + } + + public function rewind() { + $this->_position = 0; + } + + public function current() { + if ($this->valid()) { + return $this->_cache[$this->_position]; + } else { + return NULL; + } + } + + public function key() { + return $this->_position; + } + + public function next() { + $this->_position += $this->current()->size(); + if ($this->content()->size() !== NULL && $this->_position > $this->content()->size()) { + throw new Exception('unexpected end of data'); + } + } + + public function valid() { + global $EBML_ELEMENTS; + if (isset($this->_cache[$this->_position])) return TRUE; + $this->content()->setPosition($this->_position); + if ($this->content()->endOfData()) return FALSE; + $id = $this->content()->readVarInt(); + if ($id === NULL) throw new Exception('invalid ID'); + if ($this->content()->size() === NULL && !$EBML_ELEMENTS->validChild($this->id(), $id)) { + $this->content()->setSize($this->_position); + return FALSE; + } + $size = $this->content()->readVarInt(); + $headSize = $this->content()->position() - $this->_position; + $content = $this->content()->nextSlice($size); + if ($EBML_ELEMENTS->datatype($id) == 'container') { + $element = new EBMLElementList($id, $content, $headSize); + } else { + if ($size === NULL) { + throw new Exception('non-container element of unknown size'); + } + $element = new EBMLElement($id, $content, $headSize); + } + $this->_cache[$this->_position] = $element; + return TRUE; + } + + // Total size of element (including ID and size) + public function size() { + if ($this->content()->size() === NULL) { + $iElement = 0; + foreach ($this as $element) { // iterate over elements to find end + $iElement++; + if ($iElement > self::$MAX_ELEMENTS) throw new Exception('not supported: too many elements'); + } + } + return $this->headSize() + $this->content()->size(); + } + + // Read and interpret content + public function value() { + return $this; + } + + // Get element value by name + public function get($name, $defaultValue=NULL) { + $iElement = 0; + foreach ($this as $element) { + $iElement++; + if ($iElement > self::$MAX_ELEMENTS) throw new Exception('not supported: too many elements'); + if (strtoupper($element->name()) == strtoupper($name)) { + return $element->value(); + } + } + return $defaultValue; + } +} + +// Parse block +class MatroskaBlock { + const LACING_NONE = 0; + const LACING_XIPH = 1; + const LACING_EBML = 3; + const LACING_FIXED = 2; + public $trackNumber; + public $timecode; + public $keyframe; + public $invisible; + public $lacing; + public $discardable; + public $frames; + + public function __construct($reader) { + # Header + $this->trackNumber = $reader->readVarInt(); + $this->timecode = ebmlDecodeInt($reader->read(2), TRUE); + $flags = ord($reader->read(1)); + if (($flags & 0x70) != 0) { + throw new Exception('reserved flags set'); + } + $this->keyframe = (($flags & 0x80) != 0); + $this->invisible = (($flags & 0x08) != 0); + $this->lacing = ($flags >> 1) & 0x03; + $this->discardable = (($flags & 0x01) != 0); + + # Lacing sizes + if ($this->lacing == self::LACING_NONE) { + $nsizes = 0; + } else { + $nsizes = ord($reader->read(1)); + } + $sizes = array(); + switch ($this->lacing) { + case self::LACING_XIPH: + for ($i = 0; $i < $nsizes; $i++) { + $size = 0; + $x = 255; + while ($x == 255) { + $x = ord($reader->read(1)); + $size += $x; + if ($size > 65536) throw new Exception('not supported: laced frame too long'); + } + $sizes[$i] = $size; + } + break; + case self::LACING_EBML: + $size = 0; + for ($i = 0; $i < $nsizes; $i++) { + $dsize = $reader->readVarInt($i != 0); + if ($dsize === NULL || $size + $dsize < 0) { + throw new Exception('invalid frame size'); + } + $size += $dsize; + $sizes[$i] = $size; + } + break; + case self::LACING_FIXED: + $lenRemaining = $reader->size() - $reader->position(); + if ($lenRemaining % ($nsizes + 1) != 0) { + throw new Exception('data size not divisible by frame count'); + } + $size = (int) ($lenRemaining / ($nsizes + 1)); + for ($i = 0; $i < $nsizes; $i++) { + $sizes[$i] = $size; + } + break; + } + + # Frames + $this->frames = array(); + for ($i = 0; $i < $nsizes; $i++) { + $this->frames[$i] = $reader->nextSlice($sizes[$i]); + } + $this->frames[$nsizes] = $reader->nextSlice($reader->size() - $reader->position()); + } +} + +// Create element list from $fileHandle +function readMatroska($fileHandle) { + $reader = new EBMLReader($fileHandle); + if ($reader->read(4) != "\x1a\x45\xdf\xa3") { + throw new Exception('not an EBML file'); + } + $root = new EBMLElementList('root', $reader, 0); + $header = $root->get('EBML'); + $ebmlVersion = $header->get('EBMLReadVersion', 1); + $docType = $header->get('DocType'); + $docTypeVersion = $header->get('DocTypeReadVersion', 1); + if ($ebmlVersion != 1) { + throw new Exception('unsupported EBML version'); + } + if ($docType != 'matroska' && $docType != 'webm') { + throw new Exception ('unsupported document type'); + } + if ($docTypeVersion < 1 || $docTypeVersion > 4) { + throw new Exception ('unsupported document type version'); + } + return $root; +} + +function ebmlEncodeVarInt($n) { + $data = ''; + $flag = 0x80; + while ($n >= $flag) { + if ($flag == 0) { + throw new Exception('not supported: number too large'); + } + $data = chr($n & 0xFF) . $data; + $n = $n >> 8; + $flag = $flag >> 1; + } + $data = chr($n | $flag) . $data; + return $data; +} + +function ebmlEncodeElementName($name) { + global $EBML_ELEMENTS; + return ebmlEncodeVarInt($EBML_ELEMENTS->id($name)); +} + +function ebmlEncodeElement($name, $content) { + return ebmlEncodeElementName($name) . ebmlEncodeVarInt(strlen($content)) . $content; +} diff --git a/inc/lib/webm/posthandler.php b/inc/lib/webm/posthandler.php new file mode 100644 index 00000000..50adbe80 --- /dev/null +++ b/inc/lib/webm/posthandler.php @@ -0,0 +1,49 @@ +<?php +// Glue code for handling a Tinyboard post. +// Portions of this file are derived from Tinyboard code. + +function postHandler($post) { + global $board, $config; + + if ($post->has_file && $post->extension == 'webm') { + require_once dirname(__FILE__) . '/videodata.php'; + $videoDetails = videoData($post->file_path); + if (!isset($videoDetails['container']) || $videoDetails['container'] != 'webm') return "not a WebM file"; + + // Set thumbnail + $thumbName = $board['dir'] . $config['dir']['thumb'] . $post->file_id . '.webm'; + if ($config['spoiler_images'] && isset($_POST['spoiler'])) { + // Use spoiler thumbnail + $post->thumb = 'spoiler'; + $size = @getimagesize($config['spoiler_image']); + $post->thumbwidth = $size[0]; + $post->thumbheight = $size[1]; + } elseif (isset($videoDetails['frame']) && $thumbFile = fopen($thumbName, 'wb')) { + // Use single frame from video as pseudo-thumbnail + fwrite($thumbFile, $videoDetails['frame']); + fclose($thumbFile); + $post->thumb = $post->file_id . '.webm'; + } else { + // Fall back to file thumbnail + $post->thumb = 'file'; + } + unset($videoDetails['frame']); + + // Set width and height + if (isset($videoDetails['width']) && isset($videoDetails['height'])) { + $post->width = $videoDetails['width']; + $post->height = $videoDetails['height']; + if ($post->thumb != 'file' && $post->thumb != 'spoiler') { + $thumbMaxWidth = $post->op ? $config['thumb_op_width'] : $config['thumb_width']; + $thumbMaxHeight = $post->op ? $config['thumb_op_height'] : $config['thumb_height']; + if ($videoDetails['width'] > $thumbMaxWidth || $videoDetails['height'] > $thumbMaxHeight) { + $post->thumbwidth = min($thumbMaxWidth, intval(round($videoDetails['width'] * $thumbMaxHeight / $videoDetails['height']))); + $post->thumbheight = min($thumbMaxHeight, intval(round($videoDetails['height'] * $thumbMaxWidth / $videoDetails['width']))); + } else { + $post->thumbwidth = $videoDetails['width']; + $post->thumbheight = $videoDetails['height']; + } + } + } + } +} diff --git a/inc/lib/webm/videodata.php b/inc/lib/webm/videodata.php new file mode 100644 index 00000000..d54d484b --- /dev/null +++ b/inc/lib/webm/videodata.php @@ -0,0 +1,173 @@ +<?php +/* This file is dedicated to the public domain; you may do as you wish with it. */ +require dirname(__FILE__) . '/matroska.php'; + +function matroskaSeekElement($name, $pos) { + return ebmlEncodeElement('Seek', + ebmlEncodeElement('SeekID', ebmlEncodeElementName($name)) + . ebmlEncodeElement('SeekPosition', pack('N', $pos)) + ); +} + +// Make video from single WebM keyframe +function muxWebMFrame($videoTrack, $frame) { + $lenSeekHead = 73; + $lenCues = 24; + + // Determine version for EBML header + $version = 2; + $videoAttr = $videoTrack->get('Video'); + if (isset($videoAttr)) { + if ($videoAttr->get('StereoMode') !== NULL) $version = 3; + if ($videoAttr->get('AlphaMode') !== NULL) $version = 3; + } + if ($videoTrack->get('CodecDelay') !== NULL) $version = 4; + if ($videoTrack->get('SeekPreRoll') !== NULL) $version = 4; + if ($frame->name() == 'BlockGroup' && $frame->get('DiscardPadding') !== NULL) $version = 4; + + // EBML header + $ebml = ebmlEncodeElement('EBML', + ebmlEncodeElement('DocType', "webm") + . ebmlEncodeElement('DocTypeVersion', chr($version)) + . ebmlEncodeElement('DocTypeReadVersion', "\x02") + ); + + // Segment + $info = ebmlEncodeElement('Info', + ebmlEncodeElement('Duration', "\x41\x20\x00\x00") + . ebmlEncodeElement('MuxingApp', 'ccframe') + . ebmlEncodeElement('WritingApp', 'ccframe') + ); + $tracks = ebmlEncodeElement('Tracks', + ebmlEncodeElement('TrackEntry', $videoTrack->content()->readAll()) + ); + $cues = ebmlEncodeElement('Cues', + ebmlEncodeElement('CuePoint', + ebmlEncodeElement('CueTime', "\x00") + . ebmlEncodeElement('CueTrackPositions', + ebmlEncodeElement('CueTrack', pack('N', $videoTrack->get('TrackNumber'))) + . ebmlEncodeElement('CueClusterPosition', pack('N', $lenSeekHead + strlen($info) + strlen($tracks) + $lenCues)) + ) + ) + ); + if (strlen($cues) != $lenCues) throw new Exception('length of Cues element wrong'); + $cluster = ebmlEncodeElement('Cluster', + ebmlEncodeElement('Timecode', "\x00") + . ebmlEncodeElement($frame->name(), $frame->content()->readAll()) + . ebmlEncodeElement('Void', '') + ); + $seekHead = ebmlEncodeElement('SeekHead', + matroskaSeekElement('Info', $lenSeekHead) + . matroskaSeekElement('Tracks', $lenSeekHead + strlen($info)) + . matroskaSeekElement('Cues', $lenSeekHead + strlen($info) + strlen($tracks)) + . matroskaSeekElement('Cluster', $lenSeekHead + strlen($info) + strlen($tracks) + $lenCues) + ); + if (strlen($seekHead) != $lenSeekHead) throw new Exception('length of SeekHead element wrong'); + $segment = ebmlEncodeElement('Segment', $seekHead . $info . $tracks . $cues . $cluster); + + return $ebml . $segment; +} + +// Locate first WebM keyframe of track $trackNumber after timecode $skip +function firstWebMFrame($segment, $trackNumber, $skip=0) { + foreach($segment as $x1) { + if ($x1->name() == 'Cluster') { + $cluserTimecode = $x1->Get('Timecode'); + foreach($x1 as $blockGroup) { + $blockRaw = NULL; + if ($blockGroup->name() == 'SimpleBlock') { + $blockRaw = $blockGroup->value(); + } elseif ($blockGroup->name() == 'BlockGroup') { + $blockRaw = $blockGroup->get('Block'); + } + if (isset($blockRaw)) { + $block = new MatroskaBlock($blockRaw); + if ($block->trackNumber == $trackNumber && $block->keyframe) { + if (!isset($cluserTimecode) || $cluserTimecode + $block->timecode >= $skip) { + return $blockGroup; + } elseif (!isset($frame1)) { + $frame1 = $blockGroup; + } + } + } + } + } + } + return isset($frame1) ? $frame1 : NULL; +} + +function videoData($filename) { + $data = array(); + + // Open file + $fileHandle = fopen($filename, 'rb'); + if (!$fileHandle) { + error_log('could not open file'); + return $data; + } + + try { + $root = readMatroska($fileHandle); + $data['container'] = $root->get('EBML')->get('DocType'); + + // Locate segment information and tracks + $segment = $root->get('Segment'); + if (!isset($segment)) throw new Exception('missing Segment element'); + + // Get segment information + $info = $segment->get('Info'); + if (isset($info)) { + $timecodeScale = $info->get('TimecodeScale'); + $duration = $info->get('Duration'); + if (isset($timecodeScale) && isset($duration)) { + $data['duration'] = 1e-9 * $timecodeScale * $duration; + } + } + + // Locate video track + $tracks = $segment->get('Tracks'); + if (!isset($tracks)) throw new Exception('missing Tracks element'); + foreach($tracks as $trackEntry) { + if ($trackEntry->name() == 'TrackEntry' && $trackEntry->get('TrackType') == 1) { + $videoTrack = $trackEntry; + break; + } + } + if (!isset($videoTrack)) throw new Exception('no video track'); + + // Get track information + $videoAttr = $videoTrack->get('Video'); + if (!isset($videoAttr)) throw new Exception('missing video parameters'); + $pixelWidth = $videoAttr->get('PixelWidth'); + $pixelHeight = $videoAttr->get('PixelHeight'); + if (!isset($pixelWidth) || !isset($pixelHeight)) throw new Exception('no width or height'); + if ($pixelWidth == 0 || $pixelHeight == 0) throw new Exception('bad PixelWidth/PixelHeight'); + $displayWidth = $videoAttr->get('DisplayWidth', $pixelWidth); + $displayHeight = $videoAttr->get('DisplayHeight', $pixelHeight); + if ($displayWidth == 0 || $displayHeight == 0) throw new Exception('bad DisplayWidth/DisplayHeight'); + $data['width'] = $displayWidth; + $data['height'] = $displayHeight; + + // Extract frame to use as thumbnail + if ($videoAttr->get('AlphaMode') != NULL) { + if (!($pixelWidth % 2 == 0 && $pixelHeight % 2 == 0 && $displayWidth % 2 == 0 && $displayHeight % 2 == 0)) { + throw new Exception('preview frame blocked due to Chromium bug'); + } + } + $trackNumber = $videoTrack->get('TrackNumber'); + if (!isset($trackNumber)) throw new Exception('missing track number'); + if (isset($data['duration']) && $data['duration'] >= 5) { + $skip = 1e9 / $timecodeScale; + } else { + $skip = 0; + } + $frame = firstWebMFrame($segment, $trackNumber, $skip); + if (!isset($frame)) throw new Exception('no keyframes'); + $data['frame'] = muxWebMFrame($videoTrack, $frame); + } catch (Exception $e) { + error_log($e->getMessage()); + } + + fclose($fileHandle); + return $data; +} |
