summaryrefslogtreecommitdiff
path: root/containerchan/videodata.php
diff options
context:
space:
mode:
Diffstat (limited to 'containerchan/videodata.php')
-rw-r--r--containerchan/videodata.php173
1 files changed, 173 insertions, 0 deletions
diff --git a/containerchan/videodata.php b/containerchan/videodata.php
new file mode 100644
index 00000000..d54d484b
--- /dev/null
+++ b/containerchan/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;
+}