1
0
mirror of https://github.com/vichan-devel/vichan.git synced 2024-11-27 17:00:52 +01:00

add files

This commit is contained in:
ccd0 2013-11-09 01:11:22 -08:00
parent 705af14064
commit 674b2e8f1a
13 changed files with 1421 additions and 2 deletions

22
LICENSE.md Normal file
View File

@ -0,0 +1,22 @@
# License
Copyright (c) 2010-2013 Tinyboard Development Group (tinyboard.org)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
All copyright notices and permission notices (including this file) shall be
included and remain unedited in all copies or substantial portions of the
Software. This explicitly includes but is not limited to the Tinyboard copyright
notices found in the footers of some template files.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,2 +1,27 @@
containerchan
=============
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 the 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 video conversion software such as FFmpeg. For this reason, it cannot create true thumbnails, but uses pseudo-thumbnails consisting of a single frame extracted from the video.
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.
The modified Tinyboard templates (post_reply.html and post_thread.html) are subject to the Tinyboard licence (see LICENSE.md). The portions of this software not derived from Tinyboard are released into the public domain.
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.
Add these lines to inc/instance-config.php:
$config['allowed_ext_files'][] = 'webm';
$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;}

BIN
collapse.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 B

118
expandvideo.js Normal file
View File

@ -0,0 +1,118 @@
function setupVideo(thumb, url) {
var video = null;
var videoContainer, videoHide;
var expanded = false;
var hovering = false;
function unexpand() {
if (expanded) {
expanded = false;
if (video.pause) video.pause();
videoContainer.style.display = "none";
thumb.style.display = "inline";
}
}
function unhover() {
if (hovering) {
hovering = false;
if (video.pause) video.pause();
video.style.display = "none";
}
}
function getVideo() {
if (video == null) {
video = document.createElement("video");
video.src = url;
video.loop = true;
video.innerText = "Your browser does not support HTML5 video.";
video.onclick = function(e) {
if (e.shiftKey) {
unexpand();
e.preventDefault();
}
};
videoHide = document.createElement("img");
videoHide.src = configRoot + "cc/collapse.gif";
videoHide.alt = "[ - ]";
videoHide.title = "Collapse to thumbnail";
videoHide.style.verticalAlign = "top";
videoHide.style.marginRight = "2px";
videoHide.onclick = unexpand;
videoContainer = document.createElement("div");
videoContainer.style.whiteSpace = "nowrap";
videoContainer.appendChild(videoHide);
videoContainer.appendChild(video);
thumb.parentNode.insertBefore(videoContainer, thumb.nextSibling);
}
}
thumb.onclick = function(e) {
if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
getVideo();
expanded = true;
hovering = false;
video.style.position = "static";
video.style.maxWidth = "";
video.style.maxHeight = "";
video.style.display = "inline";
videoHide.style.display = "inline";
videoContainer.style.display = "block";
thumb.style.display = "none";
video.muted = setting("videomuted");
video.controls = true;
video.play();
return false;
}
};
thumb.onmouseover = function(e) {
if (setting("videohover")) {
getVideo();
expanded = false;
hovering = true;
video.style.position = "fixed";
video.style.right = "0px";
video.style.top = "0px";
video.style.maxWidth = (document.body.parentNode.getBoundingClientRect().right - thumb.getBoundingClientRect().right) + "px";
video.style.maxHeight = "100%";
video.style.display = "inline";
videoHide.style.display = "none";
videoContainer.style.display = "inline";
video.muted = setting("videomuted");
video.controls = false;
video.play();
}
};
thumb.onmouseout = unhover;
}
window.onload = function() {
settingsPanel.style.position = "absolute";
settingsPanel.style.top = "1em";
settingsPanel.style.right = "1em";
document.body.insertBefore(settingsPanel, document.body.firstChild);
var thumbs = document.querySelectorAll("a.file");
for (var i = 0; i < thumbs.length; i++) {
if (/\.webm$/.test(thumbs[i].pathname)) {
setupVideo(thumbs[i], thumbs[i].href);
} else {
var m = thumbs[i].search.match(/\bv=([^&]*)/);
if (m != null) {
var url = decodeURIComponent(m[1]);
if (/\.webm$/.test(url)) setupVideo(thumbs[i], url);
}
}
}
};

224
matroska-elements.txt Normal file
View File

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

487
matroska.php Normal file
View File

@ -0,0 +1,487 @@
<?php
// 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;
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;
}
}
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 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);
}
}
// 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;
protected $_elementTypeList;
public function __construct($id, $content, $headSize, $elementTypeList) {
$this->_id = $id;
$this->_name = $elementTypeList->name($this->_id);
$this->_datatype = $elementTypeList->datatype($this->_id);
$this->_content = $content;
$this->_headSize = $headSize;
$this->_elementTypeList = $elementTypeList;
}
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, $elementTypeList) {
parent::__construct($id, $content, $headSize, $elementTypeList);
$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() {
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 && !$this->_elementTypeList->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 ($this->_elementTypeList->datatype($id) == 'container') {
$element = new EBMLElementList($id, $content, $headSize, $this->_elementTypeList);
} else {
if ($size === NULL) {
throw new Exception('non-container element of unknown size');
}
$element = new EBMLElement($id, $content, $headSize, $this->_elementTypeList);
}
$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 ($element->name() == $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');
}
$matroskaElementTypeList = new EBMLElementTypeList(dirname(__FILE__) . '/matroska-elements.txt');
$root = new EBMLElementList('root', $reader, 0, $matroskaElementTypeList);
$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;
}

14
player.php Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php echo htmlspecialchars($_GET["t"]) ?></title>
<script src="settings.js"></script>
<script src="playersettings.js"></script>
</head>
<body>
<video controls loop src="<?php echo htmlspecialchars($_GET["v"]) ?>">
Your browser does not support HTML5 video.
</video>
</body>
</html>

7
playersettings.js Normal file
View File

@ -0,0 +1,7 @@
window.onload = function() {
settingsPanel.style.cssFloat = "right";
document.body.insertBefore(settingsPanel, document.body.firstChild);
var video = document.getElementsByTagName("video")[0];
video.muted = setting("videomuted");
video.play();
};

125
post_reply.html Normal file
View File

@ -0,0 +1,125 @@
{% filter remove_whitespace %}
{# tabs and new lines will be ignored #}
<div class="post reply" id="reply_{{ post.id }}">
<p class="intro"{% if not index %} id="{{ post.id }}"{% endif %}>
<input type="checkbox" class="delete" name="delete_{{ post.id }}" id="delete_{{ post.id }}" />
<label for="delete_{{ post.id }}">
{% if post.subject|length > 0 %}
{# show subject #}
<span class="subject">{{ post.subject|bidi_cleanup }}</span>
{% endif %}
{% if post.email|length > 0 %}
{# start email #}
<a class="email" href="mailto:{{ post.email }}">
{% endif %}
{% set capcode = post.capcode|capcode %}
<span {% if capcode.name %}style="{{ capcode.name }}" {% endif %}class="name">{{ post.name|bidi_cleanup }}</span>
{% if post.trip|length > 0 %}
<span {% if capcode.trip %}style="{{ capcode.trip }}" {% endif %}class="trip">{{ post.trip }}</span>
{% endif %}
{% if post.email|length > 0 %}
{# end email #}
</a>
{% endif %}
{% if capcode %}
{{ capcode.cap }}
{% endif %}
{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %}
[<a class="ip-link" style="margin:0;" href="?/IP/{{ post.ip }}">{{ post.ip }}</a>]
{% endif %}
{% if config.display_flags and post.modifiers.flag %}
<img class="flag" src="{{ config.uri_flags|sprintf(post.modifiers.flag) }}"
style="{% if post.modifiers['flag style'] %}{{ post.modifiers['flag style'] }}{% else %}{{ config.flag_style }}{% endif %}"
{% if post.modifiers['flag alt'] %}alt="{{ post.modifiers['flag alt'] | e('html_attr') }}" title="{{ post.modifiers['flag alt'] | e('html_attr') }}"{% endif %}>
{% endif %}
<time datetime="{{ post.time|date('%Y-%m-%dT%H:%M:%S') }}{{ timezone() }}">{{ post.time|date(config.post_date) }}</time>
</label>
{% if config.poster_ids %}
ID: {{ post.ip|poster_id(post.thread) }}
{% endif %}
<a class="post_no" {% if not index %}onclick="highlightReply({{ post.id }})" {% endif %}href="{{ post.link }}">No.</a>
<a class="post_no"
{% if not index %}
onclick="citeReply({{ post.id }});"
{% endif %}
href="{% if index %}
{{ post.link('q') }}
{% else %}
javascript:void(0);
{% endif %}">
{{ post.id }}
</a>
</p>
{% if post.embed %}
{{ post.embed }}
{% elseif post.file == 'deleted' %}
<img class="post-image deleted" src="{{ config.image_deleted }}" alt="" />
{% elseif post.file and post.file %}
<p class="fileinfo">File: <a href="{{ config.uri_img }}{{ post.file }}">{{ post.file }}</a> <span class="unimportant">
(
{% if post.thumb == 'spoiler' %}
Spoiler Image,
{% endif %}
{{ post.filesize|filesize }}
{% if post.filewidth and post.fileheight %}
, {{ post.filewidth}}x{{ post.fileheight }}
{% if config.show_ratio %}
, {{ post.ratio }}
{% endif %}
{% endif %}
{% if config.show_filename and post.filename %}
,
{% if post.filename|length > config.max_filename_display %}
<span class="postfilename" title="{{ post.filename|e }}">{{ post.filename|truncate(config.max_filename_display)|bidi_cleanup }}</span>
{% else %}
<span class="postfilename">{{ post.filename|e|bidi_cleanup }}</span>
{% endif %}
{% endif %}
{% if post.thumb != 'file' and config.image_identification %}
,
<span class='image_id'>
<a href="http://imgops.com/{{ config.domain }}{{ config.uri_img }}{{ post.file }}">io</a>
{% if post.file|extension == 'jpg' %}
<a href="http://regex.info/exif.cgi?url={{ config.domain }}{{ config.uri_img }}{{ post.file }}">e</a>
{% endif %}
<a href="http://www.google.com/searchbyimage?image_url={{ config.domain }}{{ config.uri_img }}{{ post.file }}">g</a>
<a href="http://www.tineye.com/search?url={{ config.domain }}{{ config.uri_img }}{{ post.file }}">t</a>
</span>
{% endif %}
)
</span>
</p>
<a href="
{% if post.file|extension == 'webm' %}
{{ config.root }}cc/player.php?v={{ config.uri_img }}{{ post.file }}&amp;t={{ post.filename|e('url') }}
{% else %}
{{ config.uri_img }}{{ post.file }}
{% endif %}" target="_blank"{% if post.thumb == 'file' or post.modifiers['is_file'] == '1' or post.filename|extension == 'webm' %} class="file"{% endif %}>
<{% if post.thumb|extension == 'webm' %}video preload{% else %}img{% endif %} class="post-image" src="
{% if post.thumb == 'file' %}
{{ config.root }}
{% if config.file_icons[post.filename|extension] %}
{{ config.file_thumb|sprintf(config.file_icons[post.filename|extension]) }}
{% else %}
{{ config.file_thumb|sprintf(config.file_icons.default) }}
{% endif %}
{% elseif post.thumb == 'spoiler' %}
{{ config.root }}{{ config.spoiler_image }}
{% else %}
{{ config.uri_thumb }}{{ post.thumb }}
{% endif %}" style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px"
{% if post.thumb|extension == 'webm' %}></video>{% else %}alt="" />{% endif %}
</a>
{% endif %}
{{ post.postControls }}
<div class="body">
{% endfilter %}{% if index %}{{ post.body|truncate_body(post.link) }}{% else %}{{ post.body }}{% endif %}{% filter remove_whitespace %}
{% if post.modifiers['ban message'] %}
{{ config.mod.ban_message|sprintf(post.modifiers['ban message']) }}
{% endif %}
</div>
</div>
<br/>
{% endfilter %}

174
post_thread.html Normal file
View File

@ -0,0 +1,174 @@
{% filter remove_whitespace %}
{# tabs and new lines will be ignored #}
<div id="thread_{{ post.id }}">
{% if post.embed %}
{{ post.embed }}
{% elseif post.file == 'deleted' %}
<img class="post-image deleted" src="{{ config.image_deleted }}" alt="" />
{% elseif post.file and post.file %}
<p class="fileinfo">{% trans %}File:{% endtrans %} <a href="{{ config.uri_img }}{{ post.file }}">{{ post.file }}</a> <span class="unimportant">
(
{% if post.thumb == 'spoiler' %}
{% trans %}Spoiler Image{% endtrans %},
{% endif %}
{{ post.filesize|filesize }}
{% if post.filewidth and post.fileheight %}
, {{ post.filewidth}}x{{ post.fileheight }}
{% if config.show_ratio %}
, {{ post.ratio }}
{% endif %}
{% endif %}
{% if config.show_filename and post.filename %}
,
{% if post.filename|length > config.max_filename_display %}
<span class="postfilename" title="{{ post.filename|e }}">{{ post.filename|truncate(config.max_filename_display)|bidi_cleanup }}</span>
{% else %}
<span class="postfilename">{{ post.filename|e|bidi_cleanup }}</span>
{% endif %}
{% endif %}
{% if post.thumb != 'file' and config.image_identification %}
,
<span class='image_id'>
<a href="http://imgops.com/{{ config.domain }}{{ config.uri_img }}{{ post.file }}">io</a>
{% if post.file|extension == 'jpg' %}
<a href="http://regex.info/exif.cgi?url={{ config.domain }}{{ config.uri_img }}{{ post.file }}">e</a>
{% endif %}
<a href="http://www.google.com/searchbyimage?image_url={{ config.domain }}{{ config.uri_img }}{{ post.file }}">g</a>
<a href="http://www.tineye.com/search?url={{ config.domain }}{{ config.uri_img }}{{ post.file }}">t</a>
</span>
{% endif %}
)
</span></p>
<a href="
{% if post.file|extension == 'webm' %}
{{ config.root }}cc/player.php?v={{ config.uri_img }}{{ post.file }}&amp;t={{ post.filename|e('url') }}
{% else %}
{{ config.uri_img }}{{ post.file }}
{% endif %}" target="_blank"{% if post.thumb == 'file' or post.modifiers['is_file'] == '1' or post.filename|extension == 'webm' %} class="file"{% endif %}>
<{% if post.thumb|extension == 'webm' %}video preload{% else %}img{% endif %} class="post-image" src="
{% if post.thumb == 'file' %}
{{ config.root }}
{% if config.file_icons[post.filename|extension] %}
{{ config.file_thumb|sprintf(config.file_icons[post.filename|extension]) }}
{% else %}
{{ config.file_thumb|sprintf(config.file_icons.default) }}
{% endif %}
{% elseif post.thumb == 'spoiler' %}
{{ config.root }}{{ config.spoiler_image }}
{% else %}
{{ config.uri_thumb }}{{ post.thumb }}
{% endif %}" style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px"
{% if post.thumb|extension == 'webm' %}></video>{% else %}alt="" />{% endif %}</a>
{% endif %}
<div class="post op"><p class="intro"{% if not index %} id="{{ post.id }}"{% endif %}>
<input type="checkbox" class="delete" name="delete_{{ post.id }}" id="delete_{{ post.id }}" />
<label for="delete_{{ post.id }}">
{% if post.subject|length > 0 %}
{# show subject #}
<span class="subject">{{ post.subject|bidi_cleanup }}</span>
{% endif %}
{% if post.email|length > 0 %}
{# start email #}
<a class="email" href="mailto:{{ post.email }}">
{% endif %}
{% set capcode = post.capcode|capcode %}
<span {% if capcode.name %}style="{{ capcode.name }}" {% endif %}class="name">{{ post.name|bidi_cleanup }}</span>
{% if post.trip|length > 0 %}
<span {% if capcode.trip %}style="{{ capcode.trip }}" {% endif %}class="trip">{{ post.trip }}</span>
{% endif %}
{% if post.email|length > 0 %}
{# end email #}
</a>
{% endif %}
{% if capcode %}
{{ capcode.cap }}
{% endif %}
{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %}
[<a class="ip-link" style="margin:0;" href="?/IP/{{ post.ip }}">{{ post.ip }}</a>]
{% endif %}
{% if config.display_flags and post.modifiers.flag %}
<img class="flag" src="{{ config.uri_flags|sprintf(post.modifiers.flag) }}"
style="{% if post.modifiers['flag style'] %}{{ post.modifiers['flag style'] }}{% else %}{{ config.flag_style }}{% endif %}"
{% if post.modifiers['flag alt'] %}alt="{{ post.modifiers['flag alt'] | e('html_attr') }}" title="{{ post.modifiers['flag alt'] | e('html_attr') }}"{% endif %}>
{% endif %}
<time datetime="{{ post.time|date('%Y-%m-%dT%H:%M:%S') }}{{ timezone() }}">{{ post.time|date(config.post_date) }}</time>
</label>
{% if config.poster_ids %}
ID: {{ post.ip|poster_id(post.id) }}
{% endif %}
<a class="post_no" href="{{ post.link }}">No.</a>
<a class="post_no"
{% if not index %}
onclick="citeReply({{ post.id }});"
{% endif %}
href="{% if index %}
{{ post.link('q') }}
{% else %}
javascript:void(0);
{% endif %}">
{{ post.id }}
</a>
{% if post.sticky %}
{% if config.font_awesome %}
<i class="icon-pushpin icon"></i>
{% else %}
<img class="icon" title="Sticky" src="{{ config.image_sticky }}" alt="Sticky" />
{% endif %}
{% endif %}
{% if post.locked %}
{% if config.font_awesome %}
<i class="icon-lock icon"></i>
{% else %}
<img class="icon" title="Locked" src="{{ config.image_locked }}" alt="Locked" />
{% endif %}
{% endif %}
{% if post.bumplocked and (config.mod.view_bumplock < 0 or (post.mod and post.mod|hasPermission(config.mod.view_bumplock, board.uri))) %}
{% if config.font_awesome %}
<i class="icon-anchor icon"></i>
{% else %}
<img class="icon" title="Bumplocked" src="{{ config.image_bumplocked }}" alt="Bumplocked" />
{% endif %}
{% endif %}
{% if index %}
<a href="{{ post.root }}{{ board.dir }}{{ config.dir.res }}{{ config.file_page|sprintf(post.id) }}">[{% trans %}Reply{% endtrans %}]</a>
{% endif %}
{{ post.postControls }}
</p>
<div class="body">
{% endfilter %}{% if index %}{{ post.body|truncate_body(post.link) }}{% else %}{{ post.body }}{% endif %}{% filter remove_whitespace %}
{% if post.modifiers['ban message'] %}
{{ config.mod.ban_message|sprintf(post.modifiers['ban message']) }}
{% endif %}
</div>
{% if post.omitted or post.omitted_images %}
<span class="omitted">
{% if post.omitted %}
{% trans %}
1 post
{% plural post.omitted %}
{{ count }} posts
{% endtrans %}
{% if post.omitted_images %}
{% trans %}and{% endtrans %}
{% endif %}
{% endif %}
{% if post.omitted_images %}
{% trans %}
1 image reply
{% plural post.omitted_images %}
{{ count }} image replies
{% endtrans %}
{% endif %} {% trans %}omitted. Click reply to view.{% endtrans %}
</span>
{% endif %}
{% if not index %}
{% endif %}
</div>{% endfilter %}
{% set hr = post.hr %}
{% for post in post.posts %}
{% include 'post_reply.html' %}
{% endfor %}
<br class="clear"/>{% if hr %}<hr/>{% endif %}
</div>

46
posthandler.php Normal file
View File

@ -0,0 +1,46 @@
<?php
function postHandler($post) {
global $board, $config;
if ($post->has_file && $post->extension == 'webm') {
require_once dirname(__FILE__) . '/videodata.php';
$videoDetails = videoData($post->file_path);
// 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'];
}
}
}
}
}

46
settings.js Normal file
View File

@ -0,0 +1,46 @@
var settingsPanel = document.createElement("div");
settingsPanel.innerHTML = '<div style="text-align: right;">Settings</div><div style="display: none;">'
+ '<label><input type="checkbox" name="videoexpand" checked>Expand videos inline</label><br>'
+ '<label><input type="checkbox" name="videohover" checked>Play videos on hover</label><br>'
+ '<label><input type="checkbox" name="videomuted">Start videos muted</label><br>'
+ '</div>';
function refreshSettings() {
var settingsItems = settingsPanel.getElementsByTagName("input");
for (var i = 0; i < settingsItems.length; i++) {
var box = settingsItems[i];
if (box.name in localStorage) {
box.checked = JSON.parse(localStorage[box.name]);
} else {
localStorage[box.name] = JSON.stringify(box.checked);
}
}
}
function setupCheckbox(box) {
box.onchange = function(e) {
localStorage[box.name] = JSON.stringify(box.checked);
};
}
refreshSettings();
var settingsItems = settingsPanel.getElementsByTagName("input");
for (var i = 0; i < settingsItems.length; i++) {
setupCheckbox(settingsItems[i]);
}
settingsPanel.onmouseover = function(e) {
refreshSettings();
var settingsSections = settingsPanel.getElementsByTagName("div");
settingsSections[0].style.fontWeight = "bold";
settingsSections[1].style.display = "block";
};
settingsPanel.onmouseout = function(e) {
var settingsSections = settingsPanel.getElementsByTagName("div");
settingsSections[0].style.fontWeight = "normal";
settingsSections[1].style.display = "none";
};
function setting(name) {
return JSON.parse(localStorage[name]);
}

131
videodata.php Normal file
View File

@ -0,0 +1,131 @@
<?php
require dirname(__FILE__) . '/matroska.php';
// Header for single VPx keyframe
function vpxFrameHeader($size, $width, $height, $codecID) {
return "\x1A\x45\xDF\xA3\x9F\x42\x86\x81\x01\x42\xF7\x81\x01\x42\xF2\x81"
. "\x04\x42\xF3\x81\x08\x42\x82\x84\x77\x65\x62\x6D\x42\x87\x81\x02"
. "\x42\x85\x81\x02\x18\x53\x80\x67\x08" . pack('N', $size + 173) . "\x11\x4D\x9B"
. "\x74\xB8\x4D\xBB\x8B\x53\xAB\x84\x15\x49\xA9\x66\x53\xAC\x81\x3D"
. "\x4D\xBB\x8B\x53\xAB\x84\x16\x54\xAE\x6B\x53\xAC\x81\x58\x4D\xBB"
. "\x8B\x53\xAB\x84\x1C\x53\xBB\x6B\x53\xAC\x81\x85\x4D\xBB\x8B\x53"
. "\xAB\x84\x1F\x43\xB6\x75\x53\xAC\x81\x97\x15\x49\xA9\x66\x96\x2A"
. "\xD7\xB1\x83\x0F\x42\x40\x44\x89\x84\x41\x20\x00\x00\x4D\x80\x81"
. "\x66\x57\x41\x81\x66\x16\x54\xAE\x6B\xA8\xAE\xA6\xD7\x81\x01\x73"
. "\xC5\x81\x01\x83\x81\x01\x23\xE3\x83\x83\x98\x96\x80\x86\x85" . $codecID
. "\xE0\x8C\xB0\x84" . pack('N', $width) . "\xBA\x84" . pack('N', $height)
. "\x1C\x53\xBB\x6B\x8D\xBB\x8B\xB3\x81\x00\xB7\x86\xF7\x81"
. "\x01\xF1\x81\x97\x1F\x43\xB6\x75\x08" . pack('N', $size + 13) . "\xE7\x81\x00"
. "\xA3\x08" . pack('N', $size + 4) . "\x81\x00\x00\x80";
}
// Locate first VPx keyframe of track $trackNumber after timecode $skip
function firstVPxFrame($segment, $trackNumber, $skip=0) {
foreach($segment as $x1) {
if ($x1->name() == 'Cluster') {
$cluserTimecode = $x1->Get('Timecode');
foreach($x1 as $x2) {
$blockRaw = NULL;
if ($x2->name() == 'SimpleBlock') {
$blockRaw = $x2->value();
} elseif ($x2->name() == 'BlockGroup') {
$blockRaw = $x2->get('Block');
}
if (isset($blockRaw)) {
$block = new MatroskaBlock($blockRaw);
if ($block->trackNumber == $trackNumber) {
$frame = $block->frames[0];
if ($block->keyframe) {
if (!isset($cluserTimecode) || $cluserTimecode + $block->timecode >= $skip) {
return $frame;
} elseif (!isset($frame1)) {
$frame1 = $frame;
}
}
}
}
}
}
}
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');
if (isset($videoAttr)) {
$pixelWidth = $videoAttr->get('PixelWidth');
$pixelHeight = $videoAttr->get('PixelHeight');
if ($pixelWidth == 0 || $pixelHeight == 0) {
error_log('bad PixelWidth/PixelHeight');
$pixelWidth = NULL;
$pixelHeight = NULL;
}
$data['width'] = $videoAttr->get('DisplayWidth', $pixelWidth);
$data['height'] = $videoAttr->get('DisplayHeight', $pixelHeight);
if ($data['width'] == 0 || $data['height'] == 0) {
error_log('bad DisplayWidth/DisplayHeight');
$data['width'] = $pixelWidth;
$data['height'] = $pixelHeight;
}
}
// Extract frame to use as thumbnail
$trackNumber = $videoTrack->get('TrackNumber');
if (!isset($trackNumber)) throw new Exception('missing track number');
$codecID = $videoTrack->get('CodecID');
if ($codecID != 'V_VP8' && $codecID != 'V_VP9') throw new Exception('codec is not VP8 or VP9');
if (!isset($pixelWidth) || !isset($pixelHeight)) throw new Exception('no width or height');
if (isset($data['duration']) && $data['duration'] >= 5) {
$skip = 1e9 / $timecodeScale;
} else {
$skip = 0;
}
$frame = firstVPxFrame($segment, $trackNumber, $skip);
if (!isset($frame)) throw new Exception('no keyframes');
$data['frame'] = vpxFrameHeader($frame->size(), $pixelWidth, $pixelHeight, $codecID) . $frame->readAll();
} catch (Exception $e) {
error_log($e->getMessage());
}
fclose($fileHandle);
return $data;
}