Add source chooser to media player

The source chooser exposes available video sources of different sizes
and qualities. This allows users to select the quality that best matches
their currently available bandwidth.

Test plan:

 - With the "Prefer HTML5 for videos" feature option ON
   - Visit a page with a video on it
   - Click the starburst icon to see the list of available sources
   - Choose sources and observe that the quality of the video changes
   - Changing sources should preserve the play/pause state, timeline
     position, and playback speed
   - Multiple players on a page should not affect each other
   - You should not see this control show up for audio players
 - With the "Prefer HTML5 for videos" feature OFF:
   - Same as above, except that changing sources should only preserve the
     play/pause state (timeline and playback speed not available in the flash
     player)

Change-Id: I1c99a8e784904eed687f827164eed7e9d7e8c26c
Reviewed-on: https://gerrit.instructure.com/38639
Reviewed-by: Zach Pendleton <zachp@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Steven Shepherd <sshepherd@instructure.com>
Product-Review: Paul Hinze <paulh@instructure.com>
This commit is contained in:
Paul Hinze 2014-08-01 15:27:32 -05:00
parent 365ddcb636
commit 4d84e1cdc2
6 changed files with 205 additions and 3 deletions

View File

@ -32,17 +32,22 @@ define [
# track events in google analytics
mejs.MepDefaults.features.push('googleanalytics')
# enable the playback speed selector
positionAfterSubtitleSelector = mejs.MepDefaults.features.indexOf('tracks') + 1
# enable the source chooser
mejs.MepDefaults.features.splice(positionAfterSubtitleSelector, 0, 'sourcechooser')
# enable the playback speed selector
mejs.MepDefaults.features.splice(positionAfterSubtitleSelector, 0, 'speed')
getSourcesAndTracks = (id) ->
dfd = new $.Deferred
$.getJSON "/media_objects/#{id}/info", (data) ->
# this 'when ...' is because right now in canvas, none of the mp3 urls actually work.
# see: CNVS-12998
sources = for source in data.media_sources when source.content_type isnt 'audio/mp3'
"<source type='#{source.content_type}' src='#{source.url}' />"
"<source type='#{source.content_type}' src='#{source.url}' title='#{source.width}x#{source.height} #{Math.floor(source.bitrate / 1024)} kbps' />"
tracks = _.map data.media_tracks, (track) ->
languageName = mejs.language.codes[track.locale] || track.locale

View File

@ -5,6 +5,16 @@
font-size: 28px;
}
/* wider and more opaque source chooser */
.mejs-sourcechooser-selector {
width: 150px !important;
background: rgba(50, 50, 50, 0.9);
label {
width: inherit !important;
}
}
//
// Playback speed control is based on code from an as-yet-unmerged pull request to mediaelement.js
// See: https://github.com/matthillman/mediaelement/commit/e9efc9473ca38c240b712a11ba4c035651c204d4

View File

@ -52,7 +52,7 @@ task :build_media_element_js do
'public/javascripts/mediaelement/mep-feature-tracks-instructure.js',
# 'mep-feature-contextmenu.js',
'public/javascripts/mediaelement/mep-feature-speed-instructure.js',
# 'mep-feature-sourcechooser.js',
'public/javascripts/mediaelement/mep-feature-sourcechooser-instructure.js',
'mep-feature-googleanalytics.js'
]
mep_chunks = mep_files.map { |path|

View File

@ -0,0 +1,89 @@
// Source Chooser Plugin
(function($) {
$.extend(mejs.MepDefaults, {
sourcechooserText: 'Source Chooser'
});
$.extend(MediaElementPlayer.prototype, {
buildsourcechooser: function(player, controls, layers, media) {
if (!player.isVideo) { return; }
var t = this;
player.sourcechooserButton =
$('<div class="mejs-button mejs-sourcechooser-button">'+
'<button type="button" aria-controls="' + t.id + '" title="' + t.options.sourcechooserText + '" aria-label="' + t.options.sourcechooserText + '"></button>'+
'<div class="mejs-sourcechooser-selector">'+
'<ul>'+
'</ul>'+
'</div>'+
'</div>')
.appendTo(controls)
// hover
.hover(function() {
$(this).find('.mejs-sourcechooser-selector').css('visibility','visible');
}, function() {
$(this).find('.mejs-sourcechooser-selector').css('visibility','hidden');
})
// handle clicks to the language radio buttons
.on('click', 'input[type=radio]', function() {
if (media.currentSrc === this.value) { return; }
var src = this.value;
var currentTime = media.currentTime;
var wasPlaying = !media.paused;
$(media).one('loadedmetadata', function() {
media.setCurrentTime(currentTime);
});
$(media).one('canplay', function() {
if (wasPlaying) {
media.play();
}
});
media.setSrc(src);
media.load();
});
// add to list
for (var i in this.node.children) {
var src = this.node.children[i];
if (src.nodeName === 'SOURCE' && (media.canPlayType(src.type) === 'probably' || media.canPlayType(src.type) === 'maybe')) {
player.addSourceButton(src.src, src.title, src.type, media.src === src.src);
}
}
},
addSourceButton: function(src, label, type, isCurrent) {
var t = this;
if (label === '' || label === undefined) {
label = src;
}
type = type.split('/')[1];
t.sourcechooserButton.find('ul').append(
$('<li>'+
'<label>' +
'<input type="radio" name="' + t.id + '_sourcechooser" value="' + src + '" ' + (isCurrent ? 'checked="checked"' : '') + ' />' +
label + ' (' + type + ')</label>' +
'</li>')
);
t.adjustSourcechooserBox();
},
adjustSourcechooserBox: function() {
var t = this;
// adjust the size of the outer box
t.sourcechooserButton.find('.mejs-sourcechooser-selector').height(
t.sourcechooserButton.find('.mejs-sourcechooser-selector ul').outerHeight(true)
);
}
});
})(mejs.$);

View File

@ -59,6 +59,10 @@
player.playbackspeed = t.options.defaultSpeed;
player.$media.on('loadedmetadata', function() {
media.playbackRate = parseFloat(player.playbackspeed);
});
player.speedButton.on('click', 'input[type=radio]', function() {
player.playbackspeed = $(this).attr('value');
media.playbackRate = parseFloat(player.playbackspeed);

View File

@ -5048,6 +5048,10 @@ if (typeof jQuery != 'undefined') {
player.playbackspeed = t.options.defaultSpeed;
player.$media.on('loadedmetadata', function() {
media.playbackRate = parseFloat(player.playbackspeed);
});
player.speedButton.on('click', 'input[type=radio]', function() {
player.playbackspeed = $(this).attr('value');
media.playbackRate = parseFloat(player.playbackspeed);
@ -5089,6 +5093,96 @@ if (typeof jQuery != 'undefined') {
})(mejs.$);
// Source Chooser Plugin
(function($) {
$.extend(mejs.MepDefaults, {
sourcechooserText: 'Source Chooser'
});
$.extend(MediaElementPlayer.prototype, {
buildsourcechooser: function(player, controls, layers, media) {
if (!player.isVideo) { return; }
var t = this;
player.sourcechooserButton =
$('<div class="mejs-button mejs-sourcechooser-button">'+
'<button type="button" aria-controls="' + t.id + '" title="' + t.options.sourcechooserText + '" aria-label="' + t.options.sourcechooserText + '"></button>'+
'<div class="mejs-sourcechooser-selector">'+
'<ul>'+
'</ul>'+
'</div>'+
'</div>')
.appendTo(controls)
// hover
.hover(function() {
$(this).find('.mejs-sourcechooser-selector').css('visibility','visible');
}, function() {
$(this).find('.mejs-sourcechooser-selector').css('visibility','hidden');
})
// handle clicks to the language radio buttons
.on('click', 'input[type=radio]', function() {
if (media.currentSrc === this.value) { return; }
var src = this.value;
var currentTime = media.currentTime;
var wasPlaying = !media.paused;
$(media).one('loadedmetadata', function() {
media.setCurrentTime(currentTime);
});
$(media).one('canplay', function() {
if (wasPlaying) {
media.play();
}
});
media.setSrc(src);
media.load();
});
// add to list
for (var i in this.node.children) {
var src = this.node.children[i];
if (src.nodeName === 'SOURCE' && (media.canPlayType(src.type) === 'probably' || media.canPlayType(src.type) === 'maybe')) {
player.addSourceButton(src.src, src.title, src.type, media.src === src.src);
}
}
},
addSourceButton: function(src, label, type, isCurrent) {
var t = this;
if (label === '' || label === undefined) {
label = src;
}
type = type.split('/')[1];
t.sourcechooserButton.find('ul').append(
$('<li>'+
'<label>' +
'<input type="radio" name="' + t.id + '_sourcechooser" value="' + src + '" ' + (isCurrent ? 'checked="checked"' : '') + ' />' +
label + ' (' + type + ')</label>' +
'</li>')
);
t.adjustSourcechooserBox();
},
adjustSourcechooserBox: function() {
var t = this;
// adjust the size of the outer box
t.sourcechooserButton.find('.mejs-sourcechooser-selector').height(
t.sourcechooserButton.find('.mejs-sourcechooser-selector ul').outerHeight(true)
);
}
});
})(mejs.$);
/*
* Google Analytics Plugin
* Requires