Commit 7e3f6373 authored by Marina Glancy's avatar Marina Glancy
Browse files

MDL-38158 media_videojs: Add Video.JS player

parent fab11235
......@@ -56,6 +56,8 @@ lib/amd/src/chartjs-lazy.js
lib/maxmind/GeoIp2/
lib/maxmind/MaxMind/
lib/ltiprovider/
media/player/videojs/amd/src/
media/player/videojs/videojs/
mod/assign/feedback/editpdf/fpdi/
repository/s3/S3.php
theme/boost/scss/bootstrap/
......
......@@ -57,6 +57,8 @@ lib/amd/src/chartjs-lazy.js
lib/maxmind/GeoIp2/
lib/maxmind/MaxMind/
lib/ltiprovider/
media/player/videojs/amd/src/
media/player/videojs/videojs/
mod/assign/feedback/editpdf/fpdi/
repository/s3/S3.php
theme/boost/scss/bootstrap/
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
This diff is collapsed.
This diff is collapsed.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Main class for plugin 'media_videojs'
*
* @package media_videojs
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Player that creates HTML5 <video> tag.
*
* @package media_videojs
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class media_videojs_plugin extends core_media_player_native {
/** @var array caches last moodle_page used to include AMD modules */
protected $loadedonpage = [];
/** @var string language file to use */
protected $language = 'en';
/** @var array caches supported extensions */
protected $extensions = null;
/** @var bool is this a youtube link */
protected $youtube = false;
/**
* Generates code required to embed the player.
*
* @param moodle_url[] $urls
* @param string $name
* @param int $width
* @param int $height
* @param array $options
* @return string
*/
public function embed($urls, $name, $width, $height, $options) {
$sources = array();
$mediamanager = core_media_manager::instance();
$datasetup = [];
$text = null;
$isaudio = null;
$hastracks = false;
$hasposter = false;
if (array_key_exists(core_media_manager::OPTION_ORIGINAL_TEXT, $options) &&
preg_match('/^<(video|audio)\b/i', $options[core_media_manager::OPTION_ORIGINAL_TEXT], $matches)) {
// Original text already had media tag - get some data from it.
$text = $options[core_media_manager::OPTION_ORIGINAL_TEXT];
$isaudio = strtolower($matches[1]) === 'audio';
$hastracks = preg_match('/<track\b/i', $text);
$hasposter = self::get_attribute($text, 'poster') !== null;
}
// Currently Flash in VideoJS does not support responsive layout. If Flash is enabled try to guess
// if HTML5 player will be engaged for the user and then set it to responsive.
$responsive = (get_config('media_videojs', 'useflash') && !$this->youtube) ? null : true;
// Build list of source tags.
foreach ($urls as $url) {
$extension = $mediamanager->get_extension($url);
$mimetype = $mediamanager->get_mimetype($url);
if ($mimetype === 'video/quicktime' && (core_useragent::is_chrome() || core_useragent::is_edge())) {
// Fix for VideoJS/Chrome bug https://github.com/videojs/video.js/issues/423 .
$mimetype = 'video/mp4';
}
$source = html_writer::empty_tag('source', array('src' => $url, 'type' => $mimetype));
$sources[] = $source;
if ($isaudio === null) {
$isaudio = in_array('.' . $extension, file_get_typegroup('extension', 'audio'));
}
if ($responsive === null) {
$responsive = core_useragent::supports_html5($extension);
}
}
$sources = implode("\n", $sources);
// Find the title, prevent double escaping.
$title = $this->get_name($name, $urls);
$title = preg_replace(['/&amp;/', '/&gt;/', '/&lt;/'], ['&', '>', '<'], $title);
// Ensure JS is loaded. This will also load language strings and populate $this->language with the current language.
$this->load_amd_module();
if ($this->youtube) {
$this->load_amd_module('Youtube');
$datasetup[] = '"techOrder": ["youtube"]';
$datasetup[] = '"sources": [{"type": "video/youtube", "src":"' . $urls[0] . '"}]';
$sources = ''; // Do not specify <source> tags - it may confuse browser.
$isaudio = false; // Just in case.
}
// Add a language.
if ($this->language) {
$datasetup[] = '"language": "' . $this->language . '"';
}
// Set responsive option.
if ($responsive) {
$datasetup[] = '"fluid": true';
}
if ($isaudio && !$hastracks) {
// We don't need a full screen toggle for the audios (except when tracks are present).
$datasetup[] = '"controlBar": {"fullscreenToggle": false}';
}
if ($isaudio && !$height && !$hastracks && !$hasposter) {
// Hide poster area for audios without tracks or poster.
// See discussion on https://github.com/videojs/video.js/issues/2777 .
// Maybe TODO: if there are only chapter tracks we still don't need poster area.
$datasetup[] = '"aspectRatio": "1:0"';
}
// Attributes for the video/audio tag.
static $playercounter = 1;
$attributes = [
'data-setup' => '{' . join(', ', $datasetup) . '}',
'id' => 'id_videojs_' . ($playercounter++),
'class' => get_config('media_videojs', $isaudio ? 'audiocssclass' : 'videocssclass')
];
if (!$responsive) {
// Note we ignore limitsize setting if not responsive.
parent::pick_video_size($width, $height);
$attributes += ['width' => $width] + ($height ? ['height' => $height] : []);
}
if ($text !== null) {
// Original text already had media tag - add necessary attributes and replace sources
// with the supported URLs only.
if (($class = self::get_attribute($text, 'class')) !== null) {
$attributes['class'] .= ' ' . $class;
}
$text = self::remove_attributes($text, ['id', 'width', 'height', 'class']);
if (self::get_attribute($text, 'title') === null) {
$attributes['title'] = $title;
}
$text = self::add_attributes($text, $attributes);
$text = self::replace_sources($text, $sources);
} else {
// Create <video> or <audio> tag with necessary attributes and all sources.
$attributes += ['preload' => 'auto', 'controls' => 'true', 'title' => $title];
$text = html_writer::tag($isaudio ? 'audio' : 'video', $sources, $attributes);
}
// Limit the width of the video if width is specified.
// We do not do it in the width attributes of the video because it does not work well
// together with responsive behavior.
if ($responsive) {
self::pick_video_size($width, $height);
if ($width) {
$text = html_writer::div($text, null, ['style' => 'max-width:' . $width . 'px;']);
}
}
return html_writer::div($text, 'mediaplugin mediaplugin_videojs');
}
/**
* Utility function that sets width and height to defaults if not specified
* as a parameter to the function (will be specified either if, (a) the calling
* code passed it, or (b) the URL included it).
* @param int $width Width passed to function (updated with final value)
* @param int $height Height passed to function (updated with final value)
*/
protected static function pick_video_size(&$width, &$height) {
if (!get_config('media_videojs', 'limitsize')) {
return;
}
parent::pick_video_size($width, $height);
}
public function get_supported_extensions() {
if ($this->extensions === null) {
$filetypes = preg_split('/\s*,\s*/',
strtolower(trim(get_config('media_videojs', 'videoextensions') . ',' .
get_config('media_videojs', 'audioextensions'))));
$this->extensions = file_get_typegroup('extension', $filetypes);
if ($this->extensions && !get_config('media_videojs', 'useflash')) {
// If Flash is disabled only return extensions natively supported by browsers.
$nativeextensions = array_merge(file_get_typegroup('extension', 'html_video'),
file_get_typegroup('extension', 'html_audio'));
$this->extensions = array_intersect($this->extensions, $nativeextensions);
}
}
return $this->extensions;
}
public function list_supported_urls(array $urls, array $options = array()) {
$result = [];
// Youtube.
$this->youtube = false;
if (count($urls) == 1 && get_config('media_videojs', 'youtube')) {
$url = reset($urls);
// Check against regex.
if (preg_match($this->get_regex_youtube(), $url->out(false), $this->matches)) {
$this->youtube = true;
return array($url);
}
}
if (!get_config('media_videojs', 'useflash')) {
return parent::list_supported_urls($urls, $options);
}
// If Flash fallback is enabled we can not check if/when browser supports flash.
$extensions = $this->get_supported_extensions();
foreach ($urls as $url) {
$ext = core_media_manager::instance()->get_extension($url);
if (in_array('.' . $ext, $extensions)) {
$result[] = $url;
}
}
return $result;
}
/**
* Default rank
* @return int
*/
public function get_rank() {
return 2000;
}
/**
* Makes sure the player is loaded on the page and the language strings are set.
* We only need to do it once on a page.
*
* @param string $module module to load
*/
protected function load_amd_module($module = 'video') {
global $PAGE;
if (array_key_exists($module, $this->loadedonpage) && $PAGE === $this->loadedonpage[$module]) {
// This is exactly the same page object we used last time.
// Prevent from calling multiple times on the same page.
return;
}
$contents = '';
$alias = '';
if ($module === 'video') {
$alias = 'videojs';
$path = new moodle_url('/media/player/videojs/video-js.swf');
$contents .= $alias . '.options.flash.swf = "' . $path . '";' . "\n";
$contents .= $this->find_language(current_language());
}
$PAGE->requires->js_amd_inline(<<<EOT
require(["media_videojs/$module"], function($alias) {
$contents
});
EOT
);
$this->loadedonpage[$module] = $PAGE;
}
/**
* Tries to match the current language to existing language files
*
* Matched language is stored in $this->language
*
* @return string JS code with a setting
*/
protected function find_language() {
global $CFG;
$this->language = current_language();
$basedir = $CFG->dirroot . '/media/player/videojs/videojs/lang/';
$langfiles = get_directory_list($basedir);
$candidates = [];
foreach ($langfiles as $langfile) {
if (strtolower(pathinfo($langfile, PATHINFO_EXTENSION)) !== 'js') {
continue;
}
$lang = basename($langfile, '.js');
if (strtolower($langfile) === $this->language . '.js') {
// Found an exact match for the language.
$js = file_get_contents($basedir . $langfile);
break;
}
if (substr($this->language, 0, 2) === strtolower(substr($langfile, 0, 2))) {
// Not an exact match but similar, for example "pt_br" is similar to "pt".
$candidates[$lang] = $langfile;
}
}
if (empty($js) && $candidates) {
// Exact match was not found, take the first candidate.
$this->language = key($candidates);
$js = file_get_contents($basedir . $candidates[$this->language]);
}
// Add it as a language for Video.JS.
if (!empty($js)) {
return "$js\n";
}
// Could not match, use default language of video player (English).
$this->language = null;
return "";
}
public function supports($usedextensions = []) {
$supports = parent::supports($usedextensions);
if (get_config('media_videojs', 'youtube')) {
$supports .= ($supports ? '<br>' : '') . get_string('youtube', 'media_videojs');
}
return $supports;
}
public function get_embeddable_markers() {
$markers = parent::get_embeddable_markers();
if (get_config('media_videojs', 'youtube')) {
$markers = array_merge($markers, array('youtube.com', 'youtube-nocookie.com', 'youtu.be', 'y2u.be'));
}
return $markers;
}
/**
* Returns regular expression used to match URLs for single youtube video
* @return string PHP regular expression e.g. '~^https?://example.org/~'
*/
protected function get_regex_youtube() {
// Regex for standard youtube link.
$link = '(youtube(-nocookie)?\.com/(?:watch\?v=|v/))';
// Regex for shortened youtube link.
$shortlink = '((youtu|y2u)\.be/)';
// Initial part of link.
$start = '~^https?://(www\.)?(' . $link . '|' . $shortlink . ')';
// Middle bit: Video key value.
$middle = '([a-z0-9\-_]+)';
return $start . $middle . core_media_player_external::END_LINK_REGEX_PART;
}
}
This diff is collapsed.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for plugin 'media_videojs'
*
* @package media_videojs
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['audiocssclass'] = 'CSS class for audios';
$string['audioextensions'] = 'Audio files extensions';
$string['configaudiocssclass'] = 'CSS class that will be added to &lt;audio&gt; element';
$string['configaudioextensions'] = 'Comma-separated list of supported video file extensions, VideoJS will try to use the browser native video player when available, ' .
'and fall back to flash player for other formats if flash is supported by the browser and flash playback is enabled here.';
$string['configlimitsize'] = 'If width and height are not specified for the video, display with default width/height. If unchecked the videos without specified dimensions will stretch to maximum possible width';
$string['configvideocssclass'] = 'CSS class that will be added to &lt;video&gt; element. For example class "vjs-big-play-centered" will place the play button in the middle. You can also set the custom skin, refer to <a href="http://docs.videojs.com/">VideoJS documentation</a>';
$string['configvideoextensions'] = 'Comma-separated list of supported video file extensions, VideoJS will try to use the browser native video player when available, ' .
'and fall back to flash player for other formats if flash is supported by the browser and flash playback is enabled here.';
$string['configyoutube'] = 'Use Video.JS to play YouTube videos. Youtube playlists are not currently supported by Video.JS';
$string['configuseflash'] = 'Use Flash player if video format is not natively supported by the browser. If enabled, VideoJS will be engaged for any '.
'file extension from the above list without browser check. Please note that Flash is not available in mobile browsers and discouraged in many desktop ones.';
$string['limitsize'] = 'Limit size';
$string['pluginname'] = 'VideoJS player';
$string['pluginname_help'] = 'Javascript wrapper for video files played by browser native video player with fallback to Flash player. (Format support depends on browser.)';
$string['videoextensions'] = 'Video files extensions';
$string['useflash'] = 'Use Flash fallback';
$string['videocssclass'] = 'CSS class for videos';
$string['youtube'] = 'YouTube videos';
VideoJS 5.11.8
--------------
https://github.com/videojs/video.js
Instructions to import VideoJS player into Moodle:
0. Checkout and build videojs source in a separate directory
1. copy 'build/temp/video.js' into 'amd/src/video.js'
2. copy 'build/temp/font/' into 'fonts/' folder
3. copy contens of 'images/' folder into 'pix/' folder
4. copy 'build/temp/video-js.css' into 'styles.css'
Replace
url("font/VideoJS.eot?#iefix")
with
url([[font:media_videojs|VideoJS.eot]])
Search for other relative URLs in this file.
Add stylelint-disable in the beginning.
Add "Modifications of player made by Moodle" to the end of the styles file.
5. copy 'LICENSE' and 'lang/' into 'videojs/' subfolder
6. search source code of video.js for the path to video-js.swf,
download it and store in this folder
Import plugins:
1. Checkout from https://github.com/videojs/videojs-youtube and build in a separate directory
2. copy 'dist/Youtube.js' into 'amd/src/Youtube.js'
In the beginning of the js file replace
define(['videojs']
with
define(['media_videojs/video']
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Settings file for plugin 'media_videojs'
*
* @package media_videojs
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configtext('media_videojs/videoextensions',
new lang_string('videoextensions', 'media_videojs'),
new lang_string('configvideoextensions', 'media_videojs'),
'.mov, .mp4, .m4v, .mpeg, .mpe, .mpg, .ogv, .webm, .flv, .f4v'));
$settings->add(new admin_setting_configtext('media_videojs/audioextensions',
new lang_string('audioextensions', 'media_videojs'),
new lang_string('configaudioextensions', 'media_videojs'),
'.aac, .flac, .mp3, .m4a, .oga, .ogg, .wav'));
$settings->add(new admin_setting_configcheckbox('media_videojs/useflash',
new lang_string('useflash', 'media_videojs'),
new lang_string('configuseflash', 'media_videojs'), 0));
$settings->add(new admin_setting_configcheckbox('media_videojs/youtube',
new lang_string('youtube', 'media_videojs'),
new lang_string('configyoutube', 'media_videojs'), 1));
$settings->add(new admin_setting_configtext('media_videojs/videocssclass',
new lang_string('videocssclass', 'media_videojs'),
new lang_string('configvideocssclass', 'media_videojs'), 'video-js'));
$settings->add(new admin_setting_configtext('media_videojs/audiocssclass',
new lang_string('audiocssclass', 'media_videojs'),
new lang_string('configaudiocssclass', 'media_videojs'), 'video-js'));
$settings->add(new admin_setting_configcheckbox('media_videojs/limitsize',
new lang_string('limitsize', 'media_videojs'),
new lang_string('configlimitsize', 'media_videojs'), 1));
}
This diff is collapsed.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test classes for handling embedded media.
*
* @package media_videojs
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test script for media embedding.
*
* @package media_videojs
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class media_videojs_testcase extends advanced_testcase {
/**
* Pre-test setup. Preserves $CFG.
*/
public function setUp() {
parent::setUp();
// Reset $CFG and $SERVER.
$this->resetAfterTest();
// Consistent initial setup: all players disabled.
\core\plugininfo\media::set_enabled_plugins('videojs');
// Pretend to be using Firefox browser (must support ogg for tests to work).
core_useragent::instance(true, 'Mozilla/5.0 (X11; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0 ');
}
/**
* Test that plugin is returned as enabled media plugin.
*/
public function test_is_installed() {
$sortorder = \core\plugininfo\media::get_enabled_plugins();
$this->assertEquals(['videojs' => 'videojs'], $sortorder);
}
/**
* Test method get_supported_extensions()
*/
public function test_supported_extensions() {
$nativeextensions = array_merge(file_get_typegroup('extension', 'html_video'),
file_get_typegroup('extension', 'html_audio'));
set_config('useflash', 0, 'media_videojs');
// Make sure that the list of extensions from the setting is filtered to HTML5 natively supported extensions.
$player = new media_videojs_plugin();
$this->assertNotEmpty($player->get_supported_extensions());
$this->assertTrue(in_array('.mp3', $player->get_supported_extensions()));
$this->assertEmpty(array_diff($player->get_supported_extensions(), $nativeextensions));
// Try to set the audioextensions to something non-native (.ra) and make sure it is not returned as supported.
set_config('audioextensions', '.mp3,.wav,.ra', 'media_videojs');
$player = new media_videojs_plugin();
$this->assertNotEmpty($player->get_supported_extensions());
$this->assertTrue(in_array('.mp3', $player->get_supported_extensions()));
$this->assertFalse(in_array('.ra', $player->get_supported_extensions()));
$this->assertEmpty(array_diff($player->get_supported_extensions(), $nativeextensions));
}
/**