From 6c870deb9615d9be6e7a4c04de5dd0c17efaa271 Mon Sep 17 00:00:00 2001 From: dorska Date: Wed, 18 Apr 2018 16:40:11 -0400 Subject: [PATCH 1/6] initial work on quality selector button --- package.json | 1 + static/js/components/VideoPlayer.js | 64 ++++---- static/js/lib/video.js | 5 + static/js/lib/videojs_hls_quality_selector.js | 137 ++++++++++++++++++ webpack.config.shared.js | 1 + yarn.lock | 73 +++++++++- 6 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 static/js/lib/videojs_hls_quality_selector.js diff --git a/package.json b/package.json index 131d2d5e3..c2d717e40 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "victory": "^0.25.6", "video.js": "^5.20.1", "videojs-contrib-hls": "^5.8.3", + "videojs-contrib-quality-levels": "^2.0.4", "videojs-resolution-switcher": "^0.4.2", "videojs-youtube": "^2.4.1", "webpack": "^3.2.0", diff --git a/static/js/components/VideoPlayer.js b/static/js/components/VideoPlayer.js index d634954fe..6f8382812 100644 --- a/static/js/components/VideoPlayer.js +++ b/static/js/components/VideoPlayer.js @@ -47,6 +47,7 @@ const makeConfigForVideo = ( ] : video.sources, plugins: { + hlsQualitySelector: {}, videoJsResolutionSwitcher: { default: "high", dynamicLabel: true @@ -294,18 +295,16 @@ class VideoPlayer extends React.Component<*, void> { if (this.player.tech_.currentTime() < 10) { return _.last(playlists) } - // Return playlist with highest bandwidth <= system bandwidth - return _.last( - R.filter( - rep => - rep.attributes.BANDWIDTH <= - _.max([ - this.player.tech_.hls.systemBandwidth, - playlists[0].attributes.BANDWIDTH - ]), - playlists - ) - ) + // Otherwise use the original selector, which will take into account + // quality settings per the qualityLevels plugin. + return this._originalSelectPlaylist() + } + + selectInitialQualityLevel () { + const sortByBitrate = R.sortBy(R.path(["bitrate"])) + this.player.hlsQualitySelector.selectQualityLevel({ + key: (sortByBitrate(this.player.qualityLevels().levels_).key), + }) } componentDidMount() { @@ -314,7 +313,6 @@ class VideoPlayer extends React.Component<*, void> { const cropVideo = this.cropVideo const createEventHandler = this.createEventHandler const toggleFullscreen = this.toggleFullscreen - const selectPlaylist = this.selectPlaylist if (video.multiangle) { videojs.getComponent( "FullscreenToggle" @@ -324,27 +322,29 @@ class VideoPlayer extends React.Component<*, void> { this.lastMinuteTracked = null this.player = videojs( this.videoNode, - makeConfigForVideo(video, useYouTube, embed), - function onPlayerReady() { - this.enableTouchActivity() - if (video.multiangle) { - setCustomDimension(SETTINGS.ga_dimension_camera, selectedCorner) - this.on("loadeddata", cropVideo) - this.on(FULLSCREEN_API.fullscreenchange, cropVideo) - window.addEventListener("resize", cropVideo) - } - this.on("loadedmetadata", function() { - gaEvents.forEach((event: string) => { - createEventHandler(event, video.key) - }) + makeConfigForVideo(video, useYouTube, embed) + ) + this.player.ready(() => { + this.player.enableTouchActivity() + if (video.multiangle) { + setCustomDimension(SETTINGS.ga_dimension_camera, selectedCorner) + this.player.on("loadeddata", cropVideo) + this.player.on(FULLSCREEN_API.fullscreenchange, cropVideo) + window.addEventListener("resize", cropVideo) + } + this.player.on("loadedmetadata", () => { + gaEvents.forEach((event: string) => { + createEventHandler(event, video.key) }) - if (this.tech_.hls !== undefined) { - this.tech_.hls.selectPlaylist = selectPlaylist - } - const params = new URLSearchParams(window.location.search) - this.currentTime(parseInt(params.get("start")) || 0) + }) + if (this.player.tech_.hls !== undefined) { + // Save the original default playlist selector for later use. + this._originalSelectPlaylist = this.player.tech_.hls.selectPlaylist + this.player.tech_.hls.selectPlaylist = this.selectPlaylist } - ) + const params = new URLSearchParams(window.location.search) + this.player.currentTime(parseInt(params.get("start")) || 0) + }) if (useYouTube) { this.checkYouTube() } diff --git a/static/js/lib/video.js b/static/js/lib/video.js index 07b061933..2aaf0cc1c 100644 --- a/static/js/lib/video.js +++ b/static/js/lib/video.js @@ -16,12 +16,17 @@ import type { Video, VideoFile } from "../flow/videoTypes" import _videojs from "video.js" import { makeVideoFileName, makeVideoFileUrl } from "./urls" +import hlsQualitySelector from "./videojs_hls_quality_selector" + // For this to work properly videojs must be available as a global global.videojs = _videojs require("videojs-contrib-hls") +require("videojs-contrib-quality-levels") require("videojs-resolution-switcher") require("videojs-youtube") +_videojs.plugin("hlsQualitySelector", hlsQualitySelector) + // export here to allow mocking of videojs function export const videojs = _videojs diff --git a/static/js/lib/videojs_hls_quality_selector.js b/static/js/lib/videojs_hls_quality_selector.js new file mode 100644 index 000000000..eef72098e --- /dev/null +++ b/static/js/lib/videojs_hls_quality_selector.js @@ -0,0 +1,137 @@ +/* Adapted from https://github.com/chrisboustead/videojs-hls-quality-selector . + * We use an adapation, rather than the original, because + * we can not register the original due to the way its dependencies are + * organized: the original will register it against its own videojs instance, + * from its own node_modules, rather than our videojs instance. + * This happens regardless of whether we 'require' or 'import' it. + * So, we adapt it here. + */ +import videojs from "video.js" + + +const hlsQualitySelector = function() { + const player = this + player.ready(() => { + player.hlsQualitySelector = new HlsQualitySelectorPlugin(player) + }) +} + + +class HlsQualitySelectorPlugin { + constructor(player) { + this.player = player + this.keyedMenuItems = {} + this.KEY_FOR_AUTO = "__AUTO__" + if (this.player.qualityLevels && this.getHls()) { + this.createQualityMenu() + this.bindPlayerEvents() + } + } + + getHls() { + return this.player.tech({ IWillNotUseThisInPlugins: true }).hls + } + + bindPlayerEvents() { + this.player + .qualityLevels() + .on("addqualitylevel", this.onAddQualityLevel.bind(this)) + } + + createQualityMenu() { + const player = this.player + const videoJsButtonClass = videojs.getComponent("MenuButton") + const concreteButtonClass = videojs.extend(videoJsButtonClass, { + constructor: function() { + videoJsButtonClass.call(this, player, { + title: player.localize("Quality") + }) + }, + createItems: function() { + return [] + } + }) + + this._qualityMenuButton = new concreteButtonClass() + + const placementIndex = player.controlBar.children().length - 2 + const concreteButtonInstance = player.controlBar.addChild( + this._qualityMenuButton, + { componentClass: "qualitySelector" }, + placementIndex + ) + concreteButtonInstance.addClass("vjs-quality-selector") + concreteButtonInstance.addClass("vjs-icon-hd") + concreteButtonInstance.removeClass("vjs-hidden") + } + + createQualityMenuItem(item) { + const player = this.player + const videoJsMenuItemClass = videojs.getComponent("MenuItem") + const concreteMenuItemClass = videojs.extend(videoJsMenuItemClass, { + constructor: function() { + videoJsMenuItemClass.call(this, player, { + label: item.label, + selectable: true, + selected: item.selected || false + }) + this.key = item.key + }, + handleClick: () => { + this.selectQualityLevel({key: item.key}) + this._qualityMenuButton.unpressButton() + } + }) + return new concreteMenuItemClass() + } + + onAddQualityLevel() { + const player = this.player + const qualityList = player.qualityLevels() + const levels = qualityList.levels_ || [] + + const menuItems = [] + for (let i = 0; i < levels.length; ++i) { + const menuItem = this.createQualityMenuItem.call(this, { + key: levels[i].id, + label: `${levels[i].height }p`, + value: levels[i].height + }) + menuItems.push(menuItem) + } + menuItems.push( + this.createQualityMenuItem.call(this, { + key: this.KEY_FOR_AUTO, + label: "Auto", + value: "auto", + selected: true + }) + ) + + if (this._qualityMenuButton) { + this._qualityMenuButton.createItems = () => menuItems + this._qualityMenuButton.update() + } + + this.keyedMenuItems = {} + for (const menuItem of menuItems) { + this.keyedMenuItems[menuItem.key] = menuItem + } + } + + selectQualityLevel ({key}) { + for (let i = 0; i < this._qualityMenuButton.items.length; ++i) { + this._qualityMenuButton.items[i].selected(false) + } + const qualityList = this.player.qualityLevels() + for (let i = 0; i < qualityList.length; ++i) { + const quality = qualityList[i] + quality.enabled = ( + (quality.id === key) || (key === this.KEY_FOR_AUTO) + ) + } + this.keyedMenuItems[key].selected(true) + } +} + +export default hlsQualitySelector diff --git a/webpack.config.shared.js b/webpack.config.shared.js index a2787ebe6..7abeb9bd0 100644 --- a/webpack.config.shared.js +++ b/webpack.config.shared.js @@ -29,6 +29,7 @@ module.exports = { extensions: ['.js', '.jsx'], alias: { 'videojs-contrib-hls': path.resolve(__dirname, 'node_modules/videojs-contrib-hls/dist/videojs-contrib-hls.js'), + 'videojs-contrib-quality-levels': path.resolve(__dirname, 'node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.js'), } }, performance: { diff --git a/yarn.lock b/yarn.lock index 9c99ec8a0..b1a64e040 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1619,6 +1619,13 @@ browserify-sign@^4.0.0: inherits "^2.0.1" parse-asn1 "^5.0.0" +browserify-versionify@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-versionify/-/browserify-versionify-1.0.6.tgz#ab2dc61d6a119e627bec487598d1983b7fdb275e" + dependencies: + find-root "^0.1.1" + through2 "0.6.3" + browserify-zlib@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" @@ -3176,6 +3183,10 @@ find-cache-dir@^1.0.0: make-dir "^1.0.0" pkg-dir "^2.0.0" +find-root@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-0.1.2.tgz#98d2267cff1916ccaf2743b3a0eea81d79d7dcd1" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3453,7 +3464,7 @@ global@4.3.0: min-document "^2.6.1" process "~0.5.1" -global@^4.3.0, global@^4.3.1, global@^4.3.2, global@~4.3.0: +global@4.3.2, global@^4.3.0, global@^4.3.1, global@^4.3.2, global@~4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" dependencies: @@ -5970,6 +5981,15 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" @@ -6730,7 +6750,7 @@ string-width@^2.0.0, string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string_decoder@^0.10.25: +string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -6912,6 +6932,13 @@ text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" +through2@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.3.tgz#795292fde9f254c2a368b38f9cc5d1bd4663afb6" + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -7206,6 +7233,19 @@ victory@^0.25.6: victory-core "^21.0.4" victory-pie "^14.0.1" +"video.js@^5.10.1 || ^6.2.0": + version "6.7.3" + resolved "https://registry.yarnpkg.com/video.js/-/video.js-6.7.3.tgz#616ab015a74bb1bc8b092e9b4b8022519756f7c0" + dependencies: + babel-runtime "^6.9.2" + global "4.3.2" + safe-json-parse "4.0.0" + tsml "1.0.1" + videojs-font "2.1.0" + videojs-ie8 "1.1.2" + videojs-vtt.js "0.12.5" + xhr "2.4.0" + video.js@^5.17.0, "video.js@^5.19.1 || ^6.2.0", video.js@^5.20.1, "video.js@^5.6.0 || ^6.2.8": version "5.20.2" resolved "https://registry.yarnpkg.com/video.js/-/video.js-5.20.2.tgz#9f0b6121a2f841fc4b1c473d8983ef4d10c663ba" @@ -7242,10 +7282,22 @@ videojs-contrib-media-sources@4.4.8: video.js "^5.17.0" webworkify "1.0.2" +videojs-contrib-quality-levels@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.4.tgz#7d124bb5fb0cb0517c48eae1465af8f5b5db0f24" + dependencies: + browserify-versionify "^1.0.6" + global "^4.3.1" + video.js "^5.10.1 || ^6.2.0" + videojs-font@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-2.0.0.tgz#af7461ef9d4b95e0334bffb78b2f2ff0364a9034" +videojs-font@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-2.1.0.tgz#a25930a67f6c9cfbf2bb88dacb8c6b451f093379" + videojs-ie8@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/videojs-ie8/-/videojs-ie8-1.1.2.tgz#a23d3d8608ad7192b69c6077fc4eb848998d35d9" @@ -7266,6 +7318,12 @@ videojs-vtt.js@0.12.4: dependencies: global "^4.3.1" +videojs-vtt.js@0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.12.5.tgz#32852732741c8b4e7a4314caa2cd93646a9c2d40" + dependencies: + global "^4.3.1" + videojs-youtube@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/videojs-youtube/-/videojs-youtube-2.4.1.tgz#4ebd6736a09c636f79fcc324befd9db230780bfc" @@ -7476,11 +7534,20 @@ xhr@2.2.2: parse-headers "^2.0.0" xtend "^4.0.0" +xhr@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.0.tgz#e16e66a45f869861eeefab416d5eff722dc40993" + dependencies: + global "~4.3.0" + is-function "^1.0.1" + parse-headers "^2.0.0" + xtend "^4.0.0" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" -xtend@^4.0.0: +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From 008674654a2e43a98e660602b4006525ce7dd078 Mon Sep 17 00:00:00 2001 From: adorsk Date: Thu, 19 Apr 2018 10:39:51 -0400 Subject: [PATCH 2/6] test/format updates --- static/js/components/VideoPlayer.js | 65 +++++++++-------- static/js/components/VideoPlayer_test.js | 71 ++++++++++++------- static/js/lib/videojs_hls_quality_selector.js | 24 +++---- 3 files changed, 92 insertions(+), 68 deletions(-) diff --git a/static/js/components/VideoPlayer.js b/static/js/components/VideoPlayer.js index 6f8382812..eb5df7bae 100644 --- a/static/js/components/VideoPlayer.js +++ b/static/js/components/VideoPlayer.js @@ -286,24 +286,23 @@ class VideoPlayer extends React.Component<*, void> { }) } - selectPlaylist = () => { - const sortByBandwidth = R.sortBy(R.path(["attributes", "BANDWIDTH"])) - const playlists = sortByBandwidth( - this.player.tech_.hls.playlists.master.playlists - ) + selectPlaylist = (opts: { defaultPlaylistSelector: () => any }) => { + const { defaultPlaylistSelector } = opts // Always start with highest bandwidth for first 10 seconds if (this.player.tech_.currentTime() < 10) { + const sortByBandwidth = R.sortBy(R.path(["attributes", "BANDWIDTH"])) + const playlists = sortByBandwidth( + this.player.tech_.hls.playlists.master.playlists + ) return _.last(playlists) } - // Otherwise use the original selector, which will take into account - // quality settings per the qualityLevels plugin. - return this._originalSelectPlaylist() + return defaultPlaylistSelector() } - selectInitialQualityLevel () { + selectInitialQualityLevel() { const sortByBitrate = R.sortBy(R.path(["bitrate"])) this.player.hlsQualitySelector.selectQualityLevel({ - key: (sortByBitrate(this.player.qualityLevels().levels_).key), + key: sortByBitrate(this.player.qualityLevels().levels_).key }) } @@ -320,31 +319,35 @@ class VideoPlayer extends React.Component<*, void> { } const useYouTube = video.is_public && video.youtube_id !== null this.lastMinuteTracked = null + const self = this this.player = videojs( this.videoNode, - makeConfigForVideo(video, useYouTube, embed) - ) - this.player.ready(() => { - this.player.enableTouchActivity() - if (video.multiangle) { - setCustomDimension(SETTINGS.ga_dimension_camera, selectedCorner) - this.player.on("loadeddata", cropVideo) - this.player.on(FULLSCREEN_API.fullscreenchange, cropVideo) - window.addEventListener("resize", cropVideo) - } - this.player.on("loadedmetadata", () => { - gaEvents.forEach((event: string) => { - createEventHandler(event, video.key) + makeConfigForVideo(video, useYouTube, embed), + function onPlayerReady() { + this.enableTouchActivity() + if (video.multiangle) { + setCustomDimension(SETTINGS.ga_dimension_camera, selectedCorner) + this.on("loadeddata", cropVideo) + this.on(FULLSCREEN_API.fullscreenchange, cropVideo) + window.addEventListener("resize", cropVideo) + } + this.on("loadedmetadata", function() { + gaEvents.forEach((event: string) => { + createEventHandler(event, video.key) + }) }) - }) - if (this.player.tech_.hls !== undefined) { - // Save the original default playlist selector for later use. - this._originalSelectPlaylist = this.player.tech_.hls.selectPlaylist - this.player.tech_.hls.selectPlaylist = this.selectPlaylist + if (this.tech_.hls !== undefined) { + const _originalSelectPlaylist = this.tech_.hls.selectPlaylist + this.tech_.hls.selectPlaylist = () => { + return self.selectPlaylist({ + defaultPlaylistSelector: _originalSelectPlaylist + }) + } + } + const params = new URLSearchParams(window.location.search) + this.currentTime(parseInt(params.get("start")) || 0) } - const params = new URLSearchParams(window.location.search) - this.player.currentTime(parseInt(params.get("start")) || 0) - }) + ) if (useYouTube) { this.checkYouTube() } diff --git a/static/js/components/VideoPlayer_test.js b/static/js/components/VideoPlayer_test.js index dbd1a0334..e82b967a1 100644 --- a/static/js/components/VideoPlayer_test.js +++ b/static/js/components/VideoPlayer_test.js @@ -109,6 +109,7 @@ describe("VideoPlayer", () => { }, playbackRates: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 4.0], plugins: { + hlsQualitySelector: {}, videoJsResolutionSwitcher: { default: "high", dynamicLabel: true @@ -146,7 +147,7 @@ describe("VideoPlayer", () => { }) ;[false, true].forEach(function(embed) { it("video element is rendered with the correct style attributes", () => { - const wrapper = renderPlayer({embed}).find("VideoPlayer") + const wrapper = renderPlayer({ embed }).find("VideoPlayer") const videoProps = wrapper.find("video").props() assert.equal( videoProps.className, @@ -321,33 +322,55 @@ describe("VideoPlayer", () => { } }) }) - ;[5, 15].forEach(videoTime => { - [1000, 2000, 3000, 4000].forEach(bandwidth => { - it(`Returns correct playlist if elapsed time is ${videoTime} secs, bandwidth is ${bandwidth}`, () => { - playerStub.tech_ = { - currentTime: sandbox.stub().returns(videoTime), - hls: { - selectPlaylist: sandbox.stub(), - playlists: { - master: { - playlists: [ - { attributes: { BANDWIDTH: 900 } }, - { attributes: { BANDWIDTH: 1900 } }, - { attributes: { BANDWIDTH: 2900 } }, - { attributes: { BANDWIDTH: 3900 } } - ] - } - }, - systemBandwidth: bandwidth + + describe("selectPlaylist", () => { + describe("when elapsed time is < 10 seconds", () => { + [1000, 2000, 3000, 4000].forEach(bandwidth => { + it(`Returns correct playlist if bandwidth is ${bandwidth}`, () => { + const videoTime = 5 + playerStub.tech_ = { + currentTime: sandbox.stub().returns(videoTime), + hls: { + selectPlaylist: sandbox.stub(), + playlists: { + master: { + playlists: [ + { attributes: { BANDWIDTH: 900 } }, + { attributes: { BANDWIDTH: 1900 } }, + { attributes: { BANDWIDTH: 2900 } }, + { attributes: { BANDWIDTH: 3900 } } + ] + } + }, + systemBandwidth: bandwidth + } } + const wrapper = renderPlayer().find("VideoPlayer") + wrapper.instance().player = playerStub + const bestPlayList = wrapper.instance().selectPlaylist({ + defaultPlaylistSelector: sandbox.stub() + }) + assert.equal( + bestPlayList.attributes.BANDWIDTH, + videoTime < 10 ? 3900 : bandwidth - 100 + ) + }) + }) + }) + + describe("when elapsed time is > 10 secs", () => { + it("proxies to default playlist selector", () => { + const videoTime = 11 + playerStub.tech_ = { + currentTime: sandbox.stub().returns(videoTime) } const wrapper = renderPlayer().find("VideoPlayer") wrapper.instance().player = playerStub - const bestPlayList = wrapper.instance().selectPlaylist() - assert.equal( - bestPlayList.attributes.BANDWIDTH, - videoTime < 10 ? 3900 : bandwidth - 100 - ) + const defaultPlaylistSelector = sandbox.stub() + const selectedPlayList = wrapper.instance().selectPlaylist({ + defaultPlaylistSelector + }) + assert.equal(selectedPlayList, defaultPlaylistSelector.returnValues[0]) }) }) }) diff --git a/static/js/lib/videojs_hls_quality_selector.js b/static/js/lib/videojs_hls_quality_selector.js index eef72098e..77b1fecc0 100644 --- a/static/js/lib/videojs_hls_quality_selector.js +++ b/static/js/lib/videojs_hls_quality_selector.js @@ -1,13 +1,14 @@ /* Adapted from https://github.com/chrisboustead/videojs-hls-quality-selector . - * We use an adapation, rather than the original, because - * we can not register the original due to the way its dependencies are - * organized: the original will register it against its own videojs instance, + * We use our own version rather than the original because + * the original structures its code in a way that makes it difficult to + * register the plugin; + * the original will register the plugin against its own videojs instance, * from its own node_modules, rather than our videojs instance. * This happens regardless of whether we 'require' or 'import' it. - * So, we adapt it here. - */ -import videojs from "video.js" + * So, we use our own version instead. + */ +import videojs from "video.js" const hlsQualitySelector = function() { const player = this @@ -16,7 +17,6 @@ const hlsQualitySelector = function() { }) } - class HlsQualitySelectorPlugin { constructor(player) { this.player = player @@ -78,7 +78,7 @@ class HlsQualitySelectorPlugin { this.key = item.key }, handleClick: () => { - this.selectQualityLevel({key: item.key}) + this.selectQualityLevel({ key: item.key }) this._qualityMenuButton.unpressButton() } }) @@ -94,7 +94,7 @@ class HlsQualitySelectorPlugin { for (let i = 0; i < levels.length; ++i) { const menuItem = this.createQualityMenuItem.call(this, { key: levels[i].id, - label: `${levels[i].height }p`, + label: `${levels[i].height}p`, value: levels[i].height }) menuItems.push(menuItem) @@ -119,16 +119,14 @@ class HlsQualitySelectorPlugin { } } - selectQualityLevel ({key}) { + selectQualityLevel({ key }) { for (let i = 0; i < this._qualityMenuButton.items.length; ++i) { this._qualityMenuButton.items[i].selected(false) } const qualityList = this.player.qualityLevels() for (let i = 0; i < qualityList.length; ++i) { const quality = qualityList[i] - quality.enabled = ( - (quality.id === key) || (key === this.KEY_FOR_AUTO) - ) + quality.enabled = quality.id === key || key === this.KEY_FOR_AUTO } this.keyedMenuItems[key].selected(true) } From 0285493213ae4c624d0592ed335db2b92185042a Mon Sep 17 00:00:00 2001 From: dorska Date: Tue, 17 Apr 2018 15:03:43 -0400 Subject: [PATCH 3/6] fix pip string for pip 10 (which tox force installs >:( ) --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4005a31e5..70f6f7a1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ # --no-binary psycopg2 -git+https://github.com/mitodl/django-elastic-transcoder.git#egg=django-elastic-transcoder -git+https://github.com/Brown-University-Library/django-shibboleth-remoteuser.git#egg=django-shibboleth-remoteuser -git+https://github.com/mitodl/mit-moira.git#egg=mit-moira -git+https://github.com/mitodl/pycaption.git#egg=pycaption +-e git+https://github.com/mitodl/django-elastic-transcoder.git#egg=django-elastic-transcoder +-e git+https://github.com/Brown-University-Library/django-shibboleth-remoteuser.git#egg=django-shibboleth-remoteuser +-e git+https://github.com/mitodl/mit-moira.git#egg=mit-moira +-e git+https://github.com/mitodl/pycaption.git#egg=pycaption amqp==2.2.1 # via kombu appdirs==1.4.3 # via fs, zeep appnope==0.1.0 # via ipython From e8cab7c2471b48505fc149aed689fca544106ea1 Mon Sep 17 00:00:00 2001 From: dorska Date: Mon, 23 Apr 2018 13:58:58 -0400 Subject: [PATCH 4/6] change playlist selector to select highest available active playlist --- static/js/components/VideoPlayer.js | 37 +++++++++------ static/js/components/VideoPlayer_test.js | 57 +++++++++++++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/static/js/components/VideoPlayer.js b/static/js/components/VideoPlayer.js index eb5df7bae..ae776adb9 100644 --- a/static/js/components/VideoPlayer.js +++ b/static/js/components/VideoPlayer.js @@ -286,17 +286,31 @@ class VideoPlayer extends React.Component<*, void> { }) } - selectPlaylist = (opts: { defaultPlaylistSelector: () => any }) => { - const { defaultPlaylistSelector } = opts + selectPlaylist = () => { + const sortByBandwidth = R.sortBy(R.path(["attributes", "BANDWIDTH"])) + const playlists = sortByBandwidth( + this.player.tech_.hls.playlists.master.playlists + ) // Always start with highest bandwidth for first 10 seconds if (this.player.tech_.currentTime() < 10) { - const sortByBandwidth = R.sortBy(R.path(["attributes", "BANDWIDTH"])) - const playlists = sortByBandwidth( - this.player.tech_.hls.playlists.master.playlists - ) return _.last(playlists) } - return defaultPlaylistSelector() + // Return active playlist with highest bandwidth <= system bandwidth, + // or first active playlist otherwise. + const activePlaylists = R.filter((rep) => !rep.disabled, playlists) + return ( + _.last( + R.filter( + ((rep) => { + return rep.attributes.BANDWIDTH <= _.max([ + this.player.tech_.hls.systemBandwidth, + playlists[0].attributes.BANDWIDTH + ]) + }), + activePlaylists + ) + ) || activePlaylists[0] + ) } selectInitialQualityLevel() { @@ -319,7 +333,7 @@ class VideoPlayer extends React.Component<*, void> { } const useYouTube = video.is_public && video.youtube_id !== null this.lastMinuteTracked = null - const self = this + const selectPlaylist = this.selectPlaylist.bind(this) this.player = videojs( this.videoNode, makeConfigForVideo(video, useYouTube, embed), @@ -337,12 +351,7 @@ class VideoPlayer extends React.Component<*, void> { }) }) if (this.tech_.hls !== undefined) { - const _originalSelectPlaylist = this.tech_.hls.selectPlaylist - this.tech_.hls.selectPlaylist = () => { - return self.selectPlaylist({ - defaultPlaylistSelector: _originalSelectPlaylist - }) - } + this.tech_.hls.selectPlaylist = selectPlaylist } const params = new URLSearchParams(window.location.search) this.currentTime(parseInt(params.get("start")) || 0) diff --git a/static/js/components/VideoPlayer_test.js b/static/js/components/VideoPlayer_test.js index e82b967a1..3a8c92b5a 100644 --- a/static/js/components/VideoPlayer_test.js +++ b/static/js/components/VideoPlayer_test.js @@ -347,9 +347,7 @@ describe("VideoPlayer", () => { } const wrapper = renderPlayer().find("VideoPlayer") wrapper.instance().player = playerStub - const bestPlayList = wrapper.instance().selectPlaylist({ - defaultPlaylistSelector: sandbox.stub() - }) + const bestPlayList = wrapper.instance().selectPlaylist() assert.equal( bestPlayList.attributes.BANDWIDTH, videoTime < 10 ? 3900 : bandwidth - 100 @@ -359,21 +357,58 @@ describe("VideoPlayer", () => { }) describe("when elapsed time is > 10 secs", () => { - it("proxies to default playlist selector", () => { - const videoTime = 11 + const videoTime = 11 + + it("selects highest active playlist <= system bandwidth", () => { playerStub.tech_ = { - currentTime: sandbox.stub().returns(videoTime) + currentTime: sandbox.stub().returns(videoTime), + hls: { + selectPlaylist: sandbox.stub(), + playlists: { + master: { + playlists: [ + { attributes: { BANDWIDTH: 900 } }, + { attributes: { BANDWIDTH: 1900 }, disabled: true }, + { attributes: { BANDWIDTH: 2900 } }, + { attributes: { BANDWIDTH: 3900 } } + ] + } + }, + systemBandwidth: 2000, + } } const wrapper = renderPlayer().find("VideoPlayer") wrapper.instance().player = playerStub - const defaultPlaylistSelector = sandbox.stub() - const selectedPlayList = wrapper.instance().selectPlaylist({ - defaultPlaylistSelector - }) - assert.equal(selectedPlayList, defaultPlaylistSelector.returnValues[0]) + const bestPlayList = wrapper.instance().selectPlaylist() + assert.equal(bestPlayList.attributes.BANDWIDTH, 900) + }) + + it("selects lowest playlist if no active playlist <= system bandwidth", () => { + playerStub.tech_ = { + currentTime: sandbox.stub().returns(videoTime), + hls: { + selectPlaylist: sandbox.stub(), + playlists: { + master: { + playlists: [ + { attributes: { BANDWIDTH: 900 }, disabled: true}, + { attributes: { BANDWIDTH: 1900 }, disabled: true }, + { attributes: { BANDWIDTH: 2900 } }, + { attributes: { BANDWIDTH: 3900 } } + ] + } + }, + systemBandwidth: 2000, + } + } + const wrapper = renderPlayer().find("VideoPlayer") + wrapper.instance().player = playerStub + const bestPlayList = wrapper.instance().selectPlaylist() + assert.equal(bestPlayList.attributes.BANDWIDTH, 2900) }) }) }) + ;[false, true].forEach(function(isPublic) { ["asdJ4y", null].forEach(function(youtubeId) { it(`checkYouTube ${expect( From 8b661ea5eee01e99fef71a9c222e379e015e6c99 Mon Sep 17 00:00:00 2001 From: dorska Date: Mon, 23 Apr 2018 15:52:19 -0400 Subject: [PATCH 5/6] remove defunct fn --- static/js/components/VideoPlayer.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/static/js/components/VideoPlayer.js b/static/js/components/VideoPlayer.js index ae776adb9..86f55ecd1 100644 --- a/static/js/components/VideoPlayer.js +++ b/static/js/components/VideoPlayer.js @@ -313,13 +313,6 @@ class VideoPlayer extends React.Component<*, void> { ) } - selectInitialQualityLevel() { - const sortByBitrate = R.sortBy(R.path(["bitrate"])) - this.player.hlsQualitySelector.selectQualityLevel({ - key: sortByBitrate(this.player.qualityLevels().levels_).key - }) - } - componentDidMount() { const { video, selectedCorner, embed } = this.props From 4fdb76f1817697d7a10b07c2e2a793ce99a4bf00 Mon Sep 17 00:00:00 2001 From: dorska Date: Tue, 24 Apr 2018 11:41:31 -0400 Subject: [PATCH 6/6] revert '-e' changes for requirements, no need for '-e' w/ bug fix from pip 10.0.1 --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 70f6f7a1c..4005a31e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ # --no-binary psycopg2 --e git+https://github.com/mitodl/django-elastic-transcoder.git#egg=django-elastic-transcoder --e git+https://github.com/Brown-University-Library/django-shibboleth-remoteuser.git#egg=django-shibboleth-remoteuser --e git+https://github.com/mitodl/mit-moira.git#egg=mit-moira --e git+https://github.com/mitodl/pycaption.git#egg=pycaption +git+https://github.com/mitodl/django-elastic-transcoder.git#egg=django-elastic-transcoder +git+https://github.com/Brown-University-Library/django-shibboleth-remoteuser.git#egg=django-shibboleth-remoteuser +git+https://github.com/mitodl/mit-moira.git#egg=mit-moira +git+https://github.com/mitodl/pycaption.git#egg=pycaption amqp==2.2.1 # via kombu appdirs==1.4.3 # via fs, zeep appnope==0.1.0 # via ipython