diff --git a/src/Collection/VideoProviderCollection.php b/src/Collection/VideoProviderCollection.php index 8da6a79..e0b41e4 100644 --- a/src/Collection/VideoProviderCollection.php +++ b/src/Collection/VideoProviderCollection.php @@ -46,7 +46,7 @@ public function getClassByVideoProvider(string $provider): string return $this->bundleConfig['video_provider'][$provider]['class']; } - throw new \Exception('No configuration exists for given provider.'); + throw new \Exception('No configuration exists for given provider'); } /** diff --git a/src/DependencyInjection/HeimrichHannotVideoExtension.php b/src/DependencyInjection/HeimrichHannotVideoExtension.php index 6fb4cfe..8b9d439 100644 --- a/src/DependencyInjection/HeimrichHannotVideoExtension.php +++ b/src/DependencyInjection/HeimrichHannotVideoExtension.php @@ -9,6 +9,8 @@ namespace HeimrichHannot\VideoBundle\DependencyInjection; use HeimrichHannot\VideoBundle\Video\FileVideo; +use HeimrichHannot\VideoBundle\Video\FileVideoMultipleResolution; +use HeimrichHannot\VideoBundle\Video\RemoteFileVideoMultipleResolution; use HeimrichHannot\VideoBundle\Video\VimeoVideo; use HeimrichHannot\VideoBundle\Video\YouTubeVideo; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -38,6 +40,14 @@ public function load(array $configs, ContainerBuilder $container) $config['video_provider']['file']['class'] = FileVideo::class; } + if(!isset($config['video_provider']['file_multi'])) { + $config['video_provider']['file_multi']['class'] = FileVideoMultipleResolution::class; + } + + if(!isset($config['video_provider']['remote_file_multi'])) { + $config['video_provider']['remote_file_multi']['class'] = RemoteFileVideoMultipleResolution::class; + } + // Support deperecated option // @todo: remove in version 1.0 $config['videoProvider'] = $config['video_provider']; diff --git a/src/EventListener/Dca/ValidateMultipleResolutionFileNames.php b/src/EventListener/Dca/ValidateMultipleResolutionFileNames.php new file mode 100644 index 0000000..4a09f0d --- /dev/null +++ b/src/EventListener/Dca/ValidateMultipleResolutionFileNames.php @@ -0,0 +1,29 @@ +get(ModelUtil::class)->getModelInstanceIfId($file, 'tl_files'); + if(!preg_match(FileVideoMultipleResolution::getFilePathRegExp(), $fileObject->path)) { + throw new \Exception("File name didn't match expectations, expected for example: file_name_720.mp4"); + } + } + + return $value; + } +} \ No newline at end of file diff --git a/src/Generator/DcaFieldGenerator.php b/src/Generator/DcaFieldGenerator.php index 8cf401e..1642de3 100644 --- a/src/Generator/DcaFieldGenerator.php +++ b/src/Generator/DcaFieldGenerator.php @@ -10,6 +10,7 @@ use Contao\Controller; use HeimrichHannot\VideoBundle\EventListener\Dca\ModifiyVideoPaletteListener; +use HeimrichHannot\VideoBundle\Video\RemoteFileVideoMultipleResolution; class DcaFieldGenerator { @@ -81,7 +82,7 @@ public static function getVideoFields(array $dca = []) return \Contao\System::getContainer()->get(\HeimrichHannot\VideoBundle\Collection\VideoProviderCollection::class)->getVideoProvider(); }, 'reference' => &$GLOBALS['TL_LANG']['tl_content']['reference']['videoProvider'], - 'eval' => ['submitOnChange' => true, 'maxlength' => 64, 'tl_class' => 'w50', 'includeBlankOption' => true], + 'eval' => ['submitOnChange' => true, 'maxlength' => 64, 'includeBlankOption' => true], 'sql' => "varchar(64) NOT NULL default ''", ], 'addPreviewImage' => [ @@ -204,6 +205,46 @@ public static function getVideoFields(array $dca = []) ], 'sql' => 'blob NULL', ], + 'multiResolutionVideoSRC' => [ + 'label' => &$GLOBALS['TL_LANG']['tl_content']['multiResolutionVideoSRC_file'], + 'inputType' => 'fileTree', + 'eval' => [ + 'multiple' => true, + 'filesOnly' => true, + 'fieldType' => 'checkbox', + 'mandatory' => true, + 'submitOnChange' => true, + 'groupStyle' => 'width: 48%' + ], + 'sql' => 'blob NULL', + ], + 'videoRemoteFile' => [ + 'label' => &$GLOBALS['TL_LANG']['tl_content']['videoRemoteFile'], + 'exclude' => false, + 'inputType' => 'text', + 'eval' => [ + 'maxlength' => 255, + 'tl_class' => 'w50', + 'mandatory' => true, + 'rgxp' => 'custom', + 'customRgxp' => RemoteFileVideoMultipleResolution::getFilePathRegExp() + ], + 'sql' => "varchar(255) NOT NULL default ''", + ], + 'videoRemoteResolutions' => [ + 'label' => &$GLOBALS['TL_LANG']['tl_content']['videoRemoteResolutions'], + 'exclude' => false, + 'inputType' => 'text', + 'default' => '1080,720,480,360', + 'eval' => [ + 'maxlength' => 255, + 'tl_class' => 'w50 clr', + 'mandatory' => true, + 'rgxp' => 'custom', + 'customRgxp' => RemoteFileVideoMultipleResolution::getResolutionsRegExp() + ], + 'sql' => "varchar(255) NOT NULL default ''", + ], 'videoSubtitles' => [ 'label' => &$GLOBALS['TL_LANG']['tl_content']['videoSubtitles'], 'exclude' => false, diff --git a/src/Resources/assets/js/videos.js b/src/Resources/assets/js/videos.js new file mode 100644 index 0000000..74d2d89 --- /dev/null +++ b/src/Resources/assets/js/videos.js @@ -0,0 +1,23933 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o camelize('background-color') + * < "backgroundColor" + * + * @param {string} string + * @return {string} + */ +function camelize(string) { + return string.replace(_hyphenPattern, function (_, character) { + return character.toUpperCase(); + }); +} + +module.exports = camelize; +},{}],4:[function(require,module,exports){ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @typechecks + */ + +'use strict'; + +var camelize = require('./camelize'); + +var msPattern = /^-ms-/; + +/** + * Camelcases a hyphenated CSS property name, for example: + * + * > camelizeStyleName('background-color') + * < "backgroundColor" + * > camelizeStyleName('-moz-transition') + * < "MozTransition" + * > camelizeStyleName('-ms-transition') + * < "msTransition" + * + * As Andi Smith suggests + * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix + * is converted to lowercase `ms`. + * + * @param {string} string + * @return {string} + */ +function camelizeStyleName(string) { + return camelize(string.replace(msPattern, 'ms-')); +} + +module.exports = camelizeStyleName; +},{"./camelize":3}],5:[function(require,module,exports){ +'use strict'; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * + */ + +var isTextNode = require('./isTextNode'); + +/*eslint-disable no-bitwise */ + +/** + * Checks if a given DOM node contains or is another DOM node. + */ +function containsNode(outerNode, innerNode) { + if (!outerNode || !innerNode) { + return false; + } else if (outerNode === innerNode) { + return true; + } else if (isTextNode(outerNode)) { + return false; + } else if (isTextNode(innerNode)) { + return containsNode(outerNode, innerNode.parentNode); + } else if ('contains' in outerNode) { + return outerNode.contains(innerNode); + } else if (outerNode.compareDocumentPosition) { + return !!(outerNode.compareDocumentPosition(innerNode) & 16); + } else { + return false; + } +} + +module.exports = containsNode; +},{"./isTextNode":18}],6:[function(require,module,exports){ +(function (process){ +'use strict'; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @typechecks + */ + +var invariant = require('./invariant'); + +/** + * Convert array-like objects to arrays. + * + * This API assumes the caller knows the contents of the data type. For less + * well defined inputs use createArrayFromMixed. + * + * @param {object|function|filelist} obj + * @return {array} + */ +function toArray(obj) { + var length = obj.length; + + // Some browsers builtin objects can report typeof 'function' (e.g. NodeList + // in old versions of Safari). + !(!Array.isArray(obj) && (typeof obj === 'object' || typeof obj === 'function')) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Array-like object expected') : invariant(false) : void 0; + + !(typeof length === 'number') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Object needs a length property') : invariant(false) : void 0; + + !(length === 0 || length - 1 in obj) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Object should have keys for indices') : invariant(false) : void 0; + + !(typeof obj.callee !== 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Object can\'t be `arguments`. Use rest params ' + '(function(...args) {}) or Array.from() instead.') : invariant(false) : void 0; + + // Old IE doesn't give collections access to hasOwnProperty. Assume inputs + // without method will throw during the slice call and skip straight to the + // fallback. + if (obj.hasOwnProperty) { + try { + return Array.prototype.slice.call(obj); + } catch (e) { + // IE < 9 does not support Array#slice on collections objects + } + } + + // Fall back to copying key by key. This assumes all keys have a value, + // so will not preserve sparsely populated inputs. + var ret = Array(length); + for (var ii = 0; ii < length; ii++) { + ret[ii] = obj[ii]; + } + return ret; +} + +/** + * Perform a heuristic test to determine if an object is "array-like". + * + * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?" + * Joshu replied: "Mu." + * + * This function determines if its argument has "array nature": it returns + * true if the argument is an actual array, an `arguments' object, or an + * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()). + * + * It will return false for other array-like objects like Filelist. + * + * @param {*} obj + * @return {boolean} + */ +function hasArrayNature(obj) { + return ( + // not null/false + !!obj && ( + // arrays are objects, NodeLists are functions in Safari + typeof obj == 'object' || typeof obj == 'function') && + // quacks like an array + 'length' in obj && + // not window + !('setInterval' in obj) && + // no DOM node should be considered an array-like + // a 'select' element has 'length' and 'item' properties on IE8 + typeof obj.nodeType != 'number' && ( + // a real array + Array.isArray(obj) || + // arguments + 'callee' in obj || + // HTMLCollection/NodeList + 'item' in obj) + ); +} + +/** + * Ensure that the argument is an array by wrapping it in an array if it is not. + * Creates a copy of the argument if it is already an array. + * + * This is mostly useful idiomatically: + * + * var createArrayFromMixed = require('createArrayFromMixed'); + * + * function takesOneOrMoreThings(things) { + * things = createArrayFromMixed(things); + * ... + * } + * + * This allows you to treat `things' as an array, but accept scalars in the API. + * + * If you need to convert an array-like object, like `arguments`, into an array + * use toArray instead. + * + * @param {*} obj + * @return {array} + */ +function createArrayFromMixed(obj) { + if (!hasArrayNature(obj)) { + return [obj]; + } else if (Array.isArray(obj)) { + return obj.slice(); + } else { + return toArray(obj); + } +} + +module.exports = createArrayFromMixed; +}).call(this,require('_process')) +},{"./invariant":16,"_process":187}],7:[function(require,module,exports){ +(function (process){ +'use strict'; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @typechecks + */ + +/*eslint-disable fb-www/unsafe-html*/ + +var ExecutionEnvironment = require('./ExecutionEnvironment'); + +var createArrayFromMixed = require('./createArrayFromMixed'); +var getMarkupWrap = require('./getMarkupWrap'); +var invariant = require('./invariant'); + +/** + * Dummy container used to render all markup. + */ +var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElement('div') : null; + +/** + * Pattern used by `getNodeName`. + */ +var nodeNamePattern = /^\s*<(\w+)/; + +/** + * Extracts the `nodeName` of the first element in a string of markup. + * + * @param {string} markup String of markup. + * @return {?string} Node name of the supplied markup. + */ +function getNodeName(markup) { + var nodeNameMatch = markup.match(nodeNamePattern); + return nodeNameMatch && nodeNameMatch[1].toLowerCase(); +} + +/** + * Creates an array containing the nodes rendered from the supplied markup. The + * optionally supplied `handleScript` function will be invoked once for each + * + diff --git a/src/Video/FileVideo.php b/src/Video/FileVideo.php index e06b362..792ef7b 100644 --- a/src/Video/FileVideo.php +++ b/src/Video/FileVideo.php @@ -93,7 +93,11 @@ public static function getPalette(): string */ public function getSrc(): string { - $path = $this->prepareVideoSource()[0]['file']->path; + $path = null; + $videoSrc = $this->prepareVideoSource(); + if(count($videoSrc)) { + $path = $this->prepareVideoSource()[0]['file']->path; + } if (null === $path) { return ''; diff --git a/src/Video/FileVideoMultipleResolution.php b/src/Video/FileVideoMultipleResolution.php new file mode 100644 index 0000000..2fbd3b6 --- /dev/null +++ b/src/Video/FileVideoMultipleResolution.php @@ -0,0 +1,136 @@ +parseVideoSource(); + return $this->files; + } + + public function getFilesJson(): string + { + return json_encode($this->getFiles()); + } + + public function getResolutionsJson(): string + { + return json_encode($this->getResolutions()); + } + + public function getId(): string + { + $this->parseVideoSource(); + return $this->video_id; + } + + public function getResolutions(): array + { + $this->parseVideoSource(); + return $this->resolutions; + } + public function getResolutionsDesc(): array + { + $desc = $this->getResolutions(); + arsort($desc); + return $desc; + } + static public function getFilePathRegExp(): string + { + return '~^.*/(.*)_([0-9]*)\..*$~'; + } + + protected function parseVideoSource(): void + { + if(count($this->files) && count($this->resolutions)) { + return; + } + $data = StringUtil::deserialize($this->getRawData()['multiResolutionVideoSRC']); + + if (null === $data) { + return; + } + + $this->files = []; + $resolutions = []; + foreach ($data as $i => $file) { + $fileObject = System::getContainer()->get(ModelUtil::class)->getModelInstanceIfId($file, 'tl_files'); + + $matches = []; + if(!preg_match($this->getFilePathRegExp(), $fileObject->path, $matches)) { + //throw new \Exception("File name didn't match expectations, expected for example: file_name_720.mp4"); + \Message::addError("File name didn't match expectations, expected for example: file_name_720.mp4"); + } + if($this->video_id == '') { + $this->video_id = $matches[1]; + } + $resolutions[] = intval($matches[2]); + $this->files[$i] = [ + 'path' => $fileObject->path, + 'name' => $fileObject->name, + 'extension' => $fileObject->extension + ]; + } + + $this->resolutions = $resolutions; + //var_dump($this->files); + } + +} \ No newline at end of file diff --git a/src/Video/RemoteFileVideoMultipleResolution.php b/src/Video/RemoteFileVideoMultipleResolution.php new file mode 100644 index 0000000..7281ce8 --- /dev/null +++ b/src/Video/RemoteFileVideoMultipleResolution.php @@ -0,0 +1,91 @@ +files) && count($this->resolutions)) { + return; + } + $basicUrl = $this->getRawData()['videoRemoteFile']; + + if(!preg_match($this->getResolutionsRegExp(), $this->getRawData()['videoRemoteResolutions'])) { + throw new \Exception("Resolutions not as expected"); + } + $this->resolutions = explode(',',$this->getRawData()['videoRemoteResolutions']); + sort($this->resolutions); + + if (null === $basicUrl || null === $this->resolutions) { + return; + } + + $this->files = []; + $urlParts = []; + if(!preg_match($this->getFilePathRegExp(), $basicUrl, $urlParts)) { + throw new \Exception("File Url not as expected"); + } + + $this->video_id = $urlParts[2]; + foreach ($this->resolutions as $i => $resolution) { + + $this->files[$i] = [ + 'path' => $urlParts[1] . '_' . $resolution . '.' . $urlParts[3], + 'name' => $urlParts[2] . '_' . $resolution, + 'extension' => $urlParts[3] + ]; + } + } + +} \ No newline at end of file