move php files to a more sane directory

This commit is contained in:
czaks 2014-04-06 21:21:17 +02:00
parent 0ac459d076
commit 781fde7789
5 changed files with 0 additions and 0 deletions

51
inc/lib/webm/README.md Normal file
View file

@ -0,0 +1,51 @@
This project is an effort to enable imageboards to host small video clips. With modern video compression, it's possible to share much higher-quality videos in a few megabytes than what you can do with animated GIF files.
The software here extends [Tinyboard](http://tinyboard.org/) to display metadata and create pseudo-thumbnails for WebM video files. It is intended to work on very basic web hosting services, including any hosting service that can run Tinyboard. In particular, it does not depend on any external video conversion software such as FFmpeg or Libav. Rather, it parses the video container to extract a single frame from the video to use in place of a thumbnail. If you can run FFmpeg or Libav on your server, it's a good idea to modify this code to use those tools to create true thumbnails; in the future, an option will be added to enable this.
A board using this code can be found at:
http://containerchan.org/tb/demo/
Be aware that this is beta software. Please report any bugs you find.
Much of the code is not specific to Tinyboard, and you are welcome to use it in your own projects. See the [core](https://github.com/ccd0/containerchan/tree/core) branch for a version without material from Tinyboard.
Installation
------------
Create a directory named cc at the root of your Tinyboard installation. Upload these files into that directory.
Replace the files templates/post_thread.html and templates/post_reply.html with the files given here.
Move video.png to the static directory.
Add these lines to inc/instance-config.php:
$config['allowed_ext_files'][] = 'webm';
$config['file_icons']['webm'] = 'video.png';
$config['additional_javascript'][] = 'cc/settings.js';
$config['additional_javascript'][] = 'cc/expandvideo.js';
require_once 'cc/posthandler.php';
event_handler('post', 'postHandler');
And add this to stylesheets/style.css:
video.post-image {
display: block;
float: left;
margin: 10px 20px;
border: none;
}
div.post video.post-image {
padding: 0px;
margin: 10px 25px 5px 5px;
}
span.settings {
position: fixed;
top: 1em;
right: 1em;
}
License
-------
See [LICENSE.md](https://github.com/ccd0/containerchan/blob/master/LICENSE.md).

524
inc/lib/webm/matroska.php Normal file
View file

@ -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;
}

25
inc/lib/webm/player.php Normal file
View file

@ -0,0 +1,25 @@
<?php
/* This file is dedicated to the public domain; you may do as you wish with it. */
$params = '?v=' . urlencode($_GET['v']) . '&amp;t=' . urlencode($_GET['t']);
$loop = ($_GET['loop'] != "0");
?><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php echo htmlspecialchars($_GET['t']); ?></title>
<link rel="stylesheet" href="playerstyle.css">
<script src="settings.js"></script>
<script src="playersettings.js"></script>
</head>
<body>
<div id="playerheader">
<a id="loop0" href="<?php echo $params; ?>&amp;loop=0"<?php if (!$loop) echo ' style="font-weight: bold"'; ?>>[play once]</a>
<a id="loop1" href="<?php echo $params; ?>&amp;loop=1"<?php if ($loop) echo ' style="font-weight: bold"'; ?>>[loop]</a>
</div>
<div id="playercontent">
<video controls<?php if ($loop) echo ' loop'; ?> src="<?php echo htmlspecialchars($_GET['v']); ?>">
Your browser does not support HTML5 video. <a href="<?php echo htmlspecialchars($_GET['v']); ?>">[Download]</a>
</video>
</div>
</body>
</html>

View file

@ -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'];
}
}
}
}
}

173
inc/lib/webm/videodata.php Normal file
View file

@ -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;
}