From 705af14064f71c4709a54276e3f34d56cfe4b32c Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 01:06:37 -0800 Subject: [PATCH 01/43] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..12c62c9f --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +containerchan +============= From 674b2e8f1a8b3659d91e2e6e5a337e3134d757a3 Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 01:11:22 -0800 Subject: [PATCH 02/43] add files --- LICENSE.md | 22 +++ README.md | 29 ++- collapse.gif | Bin 0 -> 67 bytes expandvideo.js | 118 ++++++++++++ matroska-elements.txt | 224 +++++++++++++++++++++++ matroska.php | 487 ++++++++++++++++++++++++++++++++++++++++++++++++++ player.php | 14 ++ playersettings.js | 7 + post_reply.html | 125 +++++++++++++ post_thread.html | 174 ++++++++++++++++++ posthandler.php | 46 +++++ settings.js | 46 +++++ videodata.php | 131 ++++++++++++++ 13 files changed, 1421 insertions(+), 2 deletions(-) create mode 100644 LICENSE.md create mode 100644 collapse.gif create mode 100644 expandvideo.js create mode 100644 matroska-elements.txt create mode 100644 matroska.php create mode 100644 player.php create mode 100644 playersettings.js create mode 100644 post_reply.html create mode 100644 post_thread.html create mode 100644 posthandler.php create mode 100644 settings.js create mode 100644 videodata.php diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..74cf71ee --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md index 12c62c9f..ec139b81 100644 --- a/README.md +++ b/README.md @@ -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;} diff --git a/collapse.gif b/collapse.gif new file mode 100644 index 0000000000000000000000000000000000000000..cda6b337dfe24ce0a12e2637bca9f83248917d9e GIT binary patch literal 67 zcmZ?wbhEHbdatatype = $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; +} diff --git a/player.php b/player.php new file mode 100644 index 00000000..ecfaf953 --- /dev/null +++ b/player.php @@ -0,0 +1,14 @@ + + + + + <?php echo htmlspecialchars($_GET["t"]) ?> + + + + + + + diff --git a/playersettings.js b/playersettings.js new file mode 100644 index 00000000..6e503070 --- /dev/null +++ b/playersettings.js @@ -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(); +}; diff --git a/post_reply.html b/post_reply.html new file mode 100644 index 00000000..aaadeefd --- /dev/null +++ b/post_reply.html @@ -0,0 +1,125 @@ +{% filter remove_whitespace %} +{# tabs and new lines will be ignored #} +
+ +

+ + + {% if config.poster_ids %} + ID: {{ post.ip|poster_id(post.thread) }} + {% endif %} + No. + + {{ post.id }} + +

+ {% if post.embed %} + {{ post.embed }} + {% elseif post.file == 'deleted' %} + + {% elseif post.file and post.file %} +

File: {{ post.file }} + ( + {% 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 %} + {{ post.filename|truncate(config.max_filename_display)|bidi_cleanup }} + {% else %} + {{ post.filename|e|bidi_cleanup }} + {% endif %} + {% endif %} + {% if post.thumb != 'file' and config.image_identification %} + , + + io + {% if post.file|extension == 'jpg' %} + e + {% endif %} + g + t + + {% 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' %}>{% else %}alt="" />{% endif %} + + {% endif %} + {{ post.postControls }} +
+ {% 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 %} +
+
+
+{% endfilter %} diff --git a/post_thread.html b/post_thread.html new file mode 100644 index 00000000..1e9c0f27 --- /dev/null +++ b/post_thread.html @@ -0,0 +1,174 @@ +{% filter remove_whitespace %} +{# tabs and new lines will be ignored #} + +
+ +{% if post.embed %} + {{ post.embed }} +{% elseif post.file == 'deleted' %} + +{% elseif post.file and post.file %} +

{% trans %}File:{% endtrans %} {{ post.file }} + ( + {% 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 %} + {{ post.filename|truncate(config.max_filename_display)|bidi_cleanup }} + {% else %} + {{ post.filename|e|bidi_cleanup }} + {% endif %} + {% endif %} + {% if post.thumb != 'file' and config.image_identification %} + , + + io + {% if post.file|extension == 'jpg' %} + e + {% endif %} + g + t + + {% 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' %}>{% else %}alt="" />{% endif %} +{% endif %} +

+ + + {% if config.poster_ids %} + ID: {{ post.ip|poster_id(post.id) }} + {% endif %} + No. + + {{ post.id }} + + {% if post.sticky %} + {% if config.font_awesome %} + + {% else %} + Sticky + {% endif %} + {% endif %} + {% if post.locked %} + {% if config.font_awesome %} + + {% else %} + 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 %} + + {% else %} + Bumplocked + {% endif %} + {% endif %} + {% if index %} + [{% trans %}Reply{% endtrans %}] + {% endif %} + {{ post.postControls }} +

+
+ {% 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 %} +
+ {% if post.omitted or post.omitted_images %} + + {% 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 %} + + {% endif %} +{% if not index %} +{% endif %} +
{% endfilter %} +{% set hr = post.hr %} +{% for post in post.posts %} + {% include 'post_reply.html' %} +{% endfor %} +
{% if hr %}
{% endif %} +
diff --git a/posthandler.php b/posthandler.php new file mode 100644 index 00000000..f2f60291 --- /dev/null +++ b/posthandler.php @@ -0,0 +1,46 @@ +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']; + } + } + } + } +} diff --git a/settings.js b/settings.js new file mode 100644 index 00000000..8dc9950c --- /dev/null +++ b/settings.js @@ -0,0 +1,46 @@ +var settingsPanel = document.createElement("div"); +settingsPanel.innerHTML = '
Settings
' + + '
' + + '
' + + '
' + + '
'; + +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]); +} diff --git a/videodata.php b/videodata.php new file mode 100644 index 00000000..3fe9eeee --- /dev/null +++ b/videodata.php @@ -0,0 +1,131 @@ +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; +} From 6fcf33af61b7fea3e65a7149fcbbf6481b286649 Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 13:40:47 -0800 Subject: [PATCH 03/43] README markdown --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ec139b81..92a0e9b7 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Create a directory named cc at the root of your Tinyboard installation. Upload 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'); + $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;} + video.post-image {display: block; float: left; margin: 10px 20px; border: none;} From 911d38632e222f35702c145193c61b20b0cce328 Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 13:45:09 -0800 Subject: [PATCH 04/43] README markdown, take 2 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 92a0e9b7..1c4a1af6 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Create a directory named cc at the root of your Tinyboard installation. Upload 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'; @@ -24,4 +25,5 @@ Add these lines to inc/instance-config.php: event_handler('post', 'postHandler'); And add this to stylesheets/style.css: + video.post-image {display: block; float: left; margin: 10px 20px; border: none;} From d015d022569aed39578367fd771567e7a70282ac Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 13:47:09 -0800 Subject: [PATCH 05/43] more markdown --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1c4a1af6..03f41790 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The modified Tinyboard templates (post_reply.html and post_thread.html) are subj INSTALLATION +============ Create a directory named cc at the root of your Tinyboard installation. Upload these files into that directory. From f0f6cb99471de2c0597c10aa37cca07c4493056d Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 13:47:49 -0800 Subject: [PATCH 06/43] too large --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03f41790..3b325761 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The modified Tinyboard templates (post_reply.html and post_thread.html) are subj INSTALLATION -============ +------------ Create a directory named cc at the root of your Tinyboard installation. Upload these files into that directory. From 53497883aca95bcd90bb38381a92e49e4e9f9ffe Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 13:50:15 -0800 Subject: [PATCH 07/43] remove caps --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b325761..53cba460 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ 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 +Installation ------------ Create a directory named cc at the root of your Tinyboard installation. Upload these files into that directory. From 338b71498bfb1145369c3fea929351e5eda5271e Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 14:28:49 -0800 Subject: [PATCH 08/43] make hover default to off --- settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.js b/settings.js index 8dc9950c..9456247a 100644 --- a/settings.js +++ b/settings.js @@ -1,7 +1,7 @@ var settingsPanel = document.createElement("div"); settingsPanel.innerHTML = '
Settings
' + '
' - + '
' + + '
' + '
' + '
'; From 81dbba64e2c34b15186f30fbd76cf1cbd44038bc Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sat, 9 Nov 2013 16:35:35 -0800 Subject: [PATCH 09/43] fix size computation for hover --- expandvideo.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/expandvideo.js b/expandvideo.js index 4ecddd60..8d6124b3 100644 --- a/expandvideo.js +++ b/expandvideo.js @@ -59,6 +59,7 @@ function setupVideo(thumb, url) { video.style.position = "static"; video.style.maxWidth = ""; video.style.maxHeight = ""; + video.style.pointerEvents = "auto"; video.style.display = "inline"; videoHide.style.display = "inline"; @@ -78,11 +79,19 @@ function setupVideo(thumb, url) { expanded = false; hovering = true; + var docRight = document.body.parentNode.getBoundingClientRect().right; + var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right; + var maxWidth = docRight - thumbRight - 20; + if (maxWidth < 250) maxWidth = 250; + video.style.position = "fixed"; video.style.right = "0px"; video.style.top = "0px"; - video.style.maxWidth = (document.body.parentNode.getBoundingClientRect().right - thumb.getBoundingClientRect().right) + "px"; + var docRight = document.body.parentNode.getBoundingClientRect().right; + var thumbRight = thumb.querySelector("img, video").getBoundingClientRect().right; + video.style.maxWidth = maxWidth + "px"; video.style.maxHeight = "100%"; + video.style.pointerEvents = "none"; video.style.display = "inline"; videoHide.style.display = "none"; From 2a770f27d1a35e8dc7914f9d230e541a93f38d9b Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sun, 10 Nov 2013 01:56:45 -0800 Subject: [PATCH 10/43] CSS for player.php, make JS more modular --- README.md | 1 + defaults.js | 14 ++++++++++++++ expandvideo.js | 9 ++------- player.php | 3 ++- playersettings.js | 6 ++---- playerstyle.css | 9 +++++++++ settings.js | 34 +++++++++++++++++----------------- 7 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 defaults.js create mode 100644 playerstyle.css diff --git a/README.md b/README.md index 53cba460..2039b318 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Replace the files templates/post_thread.html and templates/post_reply.html with Add these lines to inc/instance-config.php: $config['allowed_ext_files'][] = 'webm'; + $config['additional_javascript'][] = 'cc/defaults.js'; $config['additional_javascript'][] = 'cc/settings.js'; $config['additional_javascript'][] = 'cc/expandvideo.js'; require_once 'cc/posthandler.php'; diff --git a/defaults.js b/defaults.js new file mode 100644 index 00000000..71dade12 --- /dev/null +++ b/defaults.js @@ -0,0 +1,14 @@ +// Scripts obtain settings by calling this function +function setting(name) { + return JSON.parse(localStorage[name]); +} + +// Default settings +function setDefault(name, value) { + if (!(name in localStorage)) { + localStorage[name] = JSON.stringify(value); + } +} +setDefault("videoexpand", true); +setDefault("videohover", false); +setDefault("videomuted", false); diff --git a/expandvideo.js b/expandvideo.js index 8d6124b3..26979bea 100644 --- a/expandvideo.js +++ b/expandvideo.js @@ -106,12 +106,7 @@ function setupVideo(thumb, url) { 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); - +if (window.addEventListener) window.addEventListener("load", function(e) { var thumbs = document.querySelectorAll("a.file"); for (var i = 0; i < thumbs.length; i++) { if (/\.webm$/.test(thumbs[i].pathname)) { @@ -124,4 +119,4 @@ window.onload = function() { } } } -}; +}, false); diff --git a/player.php b/player.php index ecfaf953..150a1087 100644 --- a/player.php +++ b/player.php @@ -3,7 +3,8 @@ <?php echo htmlspecialchars($_GET["t"]) ?> - + + diff --git a/playersettings.js b/playersettings.js index 6e503070..e6099ff4 100644 --- a/playersettings.js +++ b/playersettings.js @@ -1,7 +1,5 @@ -window.onload = function() { - settingsPanel.style.cssFloat = "right"; - document.body.insertBefore(settingsPanel, document.body.firstChild); +if (window.addEventListener) window.addEventListener("load", function(e) { var video = document.getElementsByTagName("video")[0]; video.muted = setting("videomuted"); video.play(); -}; +}, false); diff --git a/playerstyle.css b/playerstyle.css new file mode 100644 index 00000000..d2bba832 --- /dev/null +++ b/playerstyle.css @@ -0,0 +1,9 @@ +body { + background: black; + color: white; +} +video { + display: block; + margin-left: auto; + margin-right: auto; +} diff --git a/settings.js b/settings.js index 9456247a..1a718da9 100644 --- a/settings.js +++ b/settings.js @@ -1,19 +1,19 @@ -var settingsPanel = document.createElement("div"); -settingsPanel.innerHTML = '
Settings
' - + '
' +// Create settings menu +var settingsMenu = document.createElement("div"); +settingsMenu.style.position = "absolute"; +settingsMenu.style.top = "1em"; +settingsMenu.style.right = "1em"; +settingsMenu.innerHTML = '
Settings
' + + '
' + '
' + '
' + '
'; function refreshSettings() { - var settingsItems = settingsPanel.getElementsByTagName("input"); + var settingsItems = settingsMenu.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); - } + box.checked = setting(box.name); } } @@ -24,23 +24,23 @@ function setupCheckbox(box) { } refreshSettings(); -var settingsItems = settingsPanel.getElementsByTagName("input"); +var settingsItems = settingsMenu.getElementsByTagName("input"); for (var i = 0; i < settingsItems.length; i++) { setupCheckbox(settingsItems[i]); } -settingsPanel.onmouseover = function(e) { +settingsMenu.onmouseover = function(e) { refreshSettings(); - var settingsSections = settingsPanel.getElementsByTagName("div"); + var settingsSections = settingsMenu.getElementsByTagName("div"); settingsSections[0].style.fontWeight = "bold"; settingsSections[1].style.display = "block"; }; -settingsPanel.onmouseout = function(e) { - var settingsSections = settingsPanel.getElementsByTagName("div"); +settingsMenu.onmouseout = function(e) { + var settingsSections = settingsMenu.getElementsByTagName("div"); settingsSections[0].style.fontWeight = "normal"; settingsSections[1].style.display = "none"; }; -function setting(name) { - return JSON.parse(localStorage[name]); -} +if (window.addEventListener) window.addEventListener("load", function(e) { + document.body.insertBefore(settingsMenu, document.body.firstChild); +}, false); From 8119058ac9d9e27197f6c8d2e837979b75828a71 Mon Sep 17 00:00:00 2001 From: ccd0 Date: Sun, 10 Nov 2013 04:57:45 -0800 Subject: [PATCH 11/43] loop setting on player page, various other changes --- README.md | 14 +++++++++++--- defaults.js | 14 -------------- expandvideo.js | 20 +++++++++++--------- player.php | 15 +++++++++++---- playersettings.js | 20 ++++++++++++++++++++ playerstyle.css | 10 ++++++++++ post_reply.html | 2 +- post_thread.html | 2 +- settings.js | 56 ++++++++++++++++++++++++++++++++----------------------- 9 files changed, 98 insertions(+), 55 deletions(-) delete mode 100644 defaults.js diff --git a/README.md b/README.md index 2039b318..a00e625f 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,20 @@ Replace the files templates/post_thread.html and templates/post_reply.html with Add these lines to inc/instance-config.php: $config['allowed_ext_files'][] = 'webm'; - $config['additional_javascript'][] = 'cc/defaults.js'; $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;} + video.post-image { + display: block; + float: left; + margin: 10px 20px; + border: none; + } + span.settings { + position: absolute; + top: 1em; + right: 1em; + } diff --git a/defaults.js b/defaults.js deleted file mode 100644 index 71dade12..00000000 --- a/defaults.js +++ /dev/null @@ -1,14 +0,0 @@ -// Scripts obtain settings by calling this function -function setting(name) { - return JSON.parse(localStorage[name]); -} - -// Default settings -function setDefault(name, value) { - if (!(name in localStorage)) { - localStorage[name] = JSON.stringify(value); - } -} -setDefault("videoexpand", true); -setDefault("videohover", false); -setDefault("videomuted", false); diff --git a/expandvideo.js b/expandvideo.js index 26979bea..260ff17d 100644 --- a/expandvideo.js +++ b/expandvideo.js @@ -27,12 +27,12 @@ function setupVideo(thumb, url) { video.src = url; video.loop = true; video.innerText = "Your browser does not support HTML5 video."; - video.onclick = function(e) { + video.addEventListener("click", function(e) { if (e.shiftKey) { unexpand(); e.preventDefault(); } - }; + }, false); videoHide = document.createElement("img"); videoHide.src = configRoot + "cc/collapse.gif"; @@ -40,7 +40,7 @@ function setupVideo(thumb, url) { videoHide.title = "Collapse to thumbnail"; videoHide.style.verticalAlign = "top"; videoHide.style.marginRight = "2px"; - videoHide.onclick = unexpand; + videoHide.addEventListener("click", unexpand, false); videoContainer = document.createElement("div"); videoContainer.style.whiteSpace = "nowrap"; @@ -50,7 +50,7 @@ function setupVideo(thumb, url) { } } - thumb.onclick = function(e) { + thumb.addEventListener("click", function(e) { if (setting("videoexpand") && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { getVideo(); expanded = true; @@ -69,11 +69,11 @@ function setupVideo(thumb, url) { video.muted = setting("videomuted"); video.controls = true; video.play(); - return false; + e.preventDefault(); } - }; + }, false); - thumb.onmouseover = function(e) { + thumb.addEventListener("mouseover", function(e) { if (setting("videohover")) { getVideo(); expanded = false; @@ -101,12 +101,14 @@ function setupVideo(thumb, url) { video.controls = false; video.play(); } - }; + }, false); - thumb.onmouseout = unhover; + thumb.addEventListener("mouseout", unhover, false); } if (window.addEventListener) window.addEventListener("load", function(e) { + document.body.insertBefore(settingsMenu, document.body.firstChild); + var thumbs = document.querySelectorAll("a.file"); for (var i = 0; i < thumbs.length; i++) { if (/\.webm$/.test(thumbs[i].pathname)) { diff --git a/player.php b/player.php index 150a1087..cd0446e2 100644 --- a/player.php +++ b/player.php @@ -1,14 +1,21 @@ - + - <?php echo htmlspecialchars($_GET["t"]) ?> + <?php echo htmlspecialchars($_GET['t']); ?> - + -