2013-11-09 04:11:22 -05:00
|
|
|
<?php
|
|
|
|
require dirname(__FILE__) . '/matroska.php';
|
|
|
|
|
2013-11-30 15:18:35 -05:00
|
|
|
function matroskaSeekElement($name, $pos) {
|
|
|
|
return ebmlEncodeElement('Seek',
|
|
|
|
ebmlEncodeElement('SeekID', ebmlEncodeElementName($name))
|
|
|
|
. ebmlEncodeElement('SeekPosition', pack('N', $pos))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2013-11-30 19:15:52 -05:00
|
|
|
// Make video from single WebM keyframe
|
|
|
|
function muxWebMFrame($videoTrack, $frame) {
|
2013-11-30 15:18:35 -05:00
|
|
|
$lenSeekHead = 73;
|
|
|
|
$lenCues = 24;
|
2013-11-30 19:15:52 -05:00
|
|
|
|
|
|
|
// 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
|
2013-11-30 15:18:35 -05:00
|
|
|
$ebml = ebmlEncodeElement('EBML',
|
|
|
|
ebmlEncodeElement('DocType', "webm")
|
2013-11-30 19:15:52 -05:00
|
|
|
. ebmlEncodeElement('DocTypeVersion', chr($version))
|
2013-11-30 15:18:35 -05:00
|
|
|
. ebmlEncodeElement('DocTypeReadVersion', "\x02")
|
2013-11-30 05:07:05 -05:00
|
|
|
);
|
2013-11-30 19:15:52 -05:00
|
|
|
|
|
|
|
// Segment
|
2013-11-30 15:18:35 -05:00
|
|
|
$info = ebmlEncodeElement('Info',
|
|
|
|
ebmlEncodeElement('Duration', "\x41\x20\x00\x00")
|
|
|
|
. ebmlEncodeElement('MuxingApp', 'ccframe')
|
|
|
|
. ebmlEncodeElement('WritingApp', 'ccframe')
|
2013-11-30 05:07:05 -05:00
|
|
|
);
|
2013-11-30 15:18:35 -05:00
|
|
|
$tracks = ebmlEncodeElement('Tracks',
|
|
|
|
ebmlEncodeElement('TrackEntry', $videoTrack->content()->readAll())
|
2013-11-30 05:07:05 -05:00
|
|
|
);
|
2013-11-30 15:18:35 -05:00
|
|
|
$cues = ebmlEncodeElement('Cues',
|
|
|
|
ebmlEncodeElement('CuePoint',
|
|
|
|
ebmlEncodeElement('CueTime', "\x00")
|
|
|
|
. ebmlEncodeElement('CueTrackPositions',
|
2013-11-30 19:15:52 -05:00
|
|
|
ebmlEncodeElement('CueTrack', pack('N', $videoTrack->get('TrackNumber')))
|
2013-11-30 15:18:35 -05:00
|
|
|
. ebmlEncodeElement('CueClusterPosition', pack('N', $lenSeekHead + strlen($info) + strlen($tracks) + $lenCues))
|
2013-11-30 05:07:05 -05:00
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
2013-11-30 15:18:35 -05:00
|
|
|
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', '')
|
2013-11-30 05:07:05 -05:00
|
|
|
);
|
2013-11-30 15:18:35 -05:00
|
|
|
$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)
|
2013-11-30 05:07:05 -05:00
|
|
|
);
|
2013-11-30 15:18:35 -05:00
|
|
|
if (strlen($seekHead) != $lenSeekHead) throw new Exception('length of SeekHead element wrong');
|
|
|
|
$segment = ebmlEncodeElement('Segment', $seekHead . $info . $tracks . $cues . $cluster);
|
2013-11-30 19:15:52 -05:00
|
|
|
|
2013-11-30 05:07:05 -05:00
|
|
|
return $ebml . $segment;
|
2013-11-09 04:11:22 -05:00
|
|
|
}
|
|
|
|
|
2013-11-30 19:15:52 -05:00
|
|
|
// Locate first WebM keyframe of track $trackNumber after timecode $skip
|
|
|
|
function firstWebMFrame($segment, $trackNumber, $skip=0) {
|
2013-11-09 04:11:22 -05:00
|
|
|
foreach($segment as $x1) {
|
|
|
|
if ($x1->name() == 'Cluster') {
|
|
|
|
$cluserTimecode = $x1->Get('Timecode');
|
2013-11-30 15:18:35 -05:00
|
|
|
foreach($x1 as $blockGroup) {
|
2013-11-09 04:11:22 -05:00
|
|
|
$blockRaw = NULL;
|
2013-11-30 15:18:35 -05:00
|
|
|
if ($blockGroup->name() == 'SimpleBlock') {
|
|
|
|
$blockRaw = $blockGroup->value();
|
|
|
|
} elseif ($blockGroup->name() == 'BlockGroup') {
|
|
|
|
$blockRaw = $blockGroup->get('Block');
|
2013-11-09 04:11:22 -05:00
|
|
|
}
|
|
|
|
if (isset($blockRaw)) {
|
|
|
|
$block = new MatroskaBlock($blockRaw);
|
2013-11-30 15:18:35 -05:00
|
|
|
if ($block->trackNumber == $trackNumber && $block->keyframe) {
|
|
|
|
if (!isset($cluserTimecode) || $cluserTimecode + $block->timecode >= $skip) {
|
|
|
|
return $blockGroup;
|
|
|
|
} elseif (!isset($frame1)) {
|
|
|
|
$frame1 = $blockGroup;
|
2013-11-09 04:11:22 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
|
|
// 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');
|
2013-12-01 02:50:06 -05:00
|
|
|
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;
|
2013-11-09 04:11:22 -05:00
|
|
|
|
|
|
|
// Extract frame to use as thumbnail
|
2013-12-01 02:50:06 -05:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
}
|
2013-11-09 04:11:22 -05:00
|
|
|
$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;
|
|
|
|
}
|
2013-11-30 19:15:52 -05:00
|
|
|
$frame = firstWebMFrame($segment, $trackNumber, $skip);
|
2013-11-09 04:11:22 -05:00
|
|
|
if (!isset($frame)) throw new Exception('no keyframes');
|
2013-11-30 19:15:52 -05:00
|
|
|
$data['frame'] = muxWebMFrame($videoTrack, $frame);
|
2013-11-09 04:11:22 -05:00
|
|
|
} catch (Exception $e) {
|
|
|
|
error_log($e->getMessage());
|
|
|
|
}
|
|
|
|
|
|
|
|
fclose($fileHandle);
|
|
|
|
return $data;
|
|
|
|
}
|