diff --git a/package.json b/package.json
index 7d0aacafef0bf153d54f04a16adc5d477684eb13..54003eadfed67f1a44f845be1f0e787f4b682514 100644
--- a/package.json
+++ b/package.json
@@ -18,22 +18,24 @@
   "author": "",
   "license": "ISC",
   "devDependencies": {
-    "@fortawesome/fontawesome-free": "^5.13.0",
-    "autoprefixer": "^9.8.0",
-    "css-loader": "^3.5.3",
-    "file-loader": "^6.0.0",
+    "@fortawesome/fontawesome-free": "^5.15.1",
+    "autoprefixer": "^9.8.6",
+    "css-loader": "^3.6.0",
+    "file-loader": "^6.1.1",
+    "node-sass": "^4.14.1",
     "postcss-loader": "^3.0.0",
-    "style-loader": "^1.2.1",
-    "url-loader": "^4.1.0",
     "sass-loader": "^8.0.2",
-    "node-sass": "^4.14.1",
-    "webpack": "^4.43.0",
-    "webpack-cli": "^3.3.11"
+    "style-loader": "^1.3.0",
+    "url-loader": "^4.1.1",
+    "webpack": "^4.44.2",
+    "webpack-cli": "^3.3.12"
   },
   "dependencies": {
-    "bootstrap": "^4.5.0",
-    "bootstrap-icons": "^1.0.0-alpha4",
+    "bootstrap": "^4.5.3",
+    "bootstrap-icons": "^1.0.0",
     "jquery": "^3.5.1",
-    "video.js": "^7.8.2"
+    "video.js": "^7.8.4",
+    "videojs-contrib-quality-levels": "^2.0.9",
+    "videojs-hls-quality-selector": "^1.1.4"
   }
 }
diff --git a/public/js/videojs-contrib-quality-levels.min.js b/public/js/videojs-contrib-quality-levels.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..f0015b89c0fdb560c782f1f444cf2b987d2067ba
--- /dev/null
+++ b/public/js/videojs-contrib-quality-levels.min.js
@@ -0,0 +1,2 @@
+/*! @name videojs-contrib-quality-levels @version 2.0.9 @license Apache-2.0 */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("video.js"),require("global/document")):"function"==typeof define&&define.amd?define(["video.js","global/document"],t):e.videojsContribQualityLevels=t(e.videojs,e.document)}(this,function(e,t){"use strict";function n(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var r=function(r){var i,l;function o(){var i,l=n(n(i=r.call(this)||this));if(e.browser.IS_IE8)for(var s in l=t.createElement("custom"),o.prototype)"constructor"!==s&&(l[s]=o.prototype[s]);return l.levels_=[],l.selectedIndex_=-1,Object.defineProperty(l,"selectedIndex",{get:function(){return l.selectedIndex_}}),Object.defineProperty(l,"length",{get:function(){return l.levels_.length}}),l||n(i)}l=r,(i=o).prototype=Object.create(l.prototype),i.prototype.constructor=i,i.__proto__=l;var s=o.prototype;return s.addQualityLevel=function(n){var r=this.getQualityLevelById(n.id);if(r)return r;var i=this.levels_.length;return r=new function n(r){var i=this;if(e.browser.IS_IE8)for(var l in i=t.createElement("custom"),n.prototype)"constructor"!==l&&(i[l]=n.prototype[l]);return i.id=r.id,i.label=i.id,i.width=r.width,i.height=r.height,i.bitrate=r.bandwidth,i.enabled_=r.enabled,Object.defineProperty(i,"enabled",{get:function(){return i.enabled_()},set:function(e){i.enabled_(e)}}),i}(n),""+i in this||Object.defineProperty(this,i,{get:function(){return this.levels_[i]}}),this.levels_.push(r),this.trigger({qualityLevel:r,type:"addqualitylevel"}),r},s.removeQualityLevel=function(e){for(var t=null,n=0,r=this.length;n<r;n++)if(this[n]===e){t=this.levels_.splice(n,1)[0],this.selectedIndex_===n?this.selectedIndex_=-1:this.selectedIndex_>n&&this.selectedIndex_--;break}return t&&this.trigger({qualityLevel:e,type:"removequalitylevel"}),t},s.getQualityLevelById=function(e){for(var t=0,n=this.length;t<n;t++){var r=this[t];if(r.id===e)return r}return null},s.dispose=function(){this.selectedIndex_=-1,this.levels_.length=0},o}(e.EventTarget);for(var i in r.prototype.allowedEvents_={change:"change",addqualitylevel:"addqualitylevel",removequalitylevel:"removequalitylevel"},r.prototype.allowedEvents_)r.prototype["on"+i]=null;var l=function(t){return n=this,e.mergeOptions({},t),i=n.qualityLevels,l=new r,n.on("dispose",function e(){l.dispose(),n.qualityLevels=i,n.off("dispose",e)}),n.qualityLevels=function(){return l},n.qualityLevels.VERSION="2.0.9",l;var n,i,l};return(e.registerPlugin||e.plugin)("qualityLevels",l),l.VERSION="2.0.9",l});
diff --git a/public/js/videojs-hls-quality-selector.min.js b/public/js/videojs-hls-quality-selector.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..480cc54309663a9297628c32236f4d161eb9a7a6
--- /dev/null
+++ b/public/js/videojs-hls-quality-selector.min.js
@@ -0,0 +1,7 @@
+/**
+ * videojs-hls-quality-selector
+ * @version 1.1.4
+ * @copyright 2020 Chris Boustead (chris@forgemotion.com)
+ * @license MIT
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],e):t.videojsHlsQualitySelector=e(t.videojs)}(this,function(o){"use strict";o=o&&o.hasOwnProperty("default")?o.default:o;var u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},l=(function(){function a(t){this.value=t}function t(o){var r,u;function l(t,e){try{var n=o[t](e),i=n.value;i instanceof a?Promise.resolve(i.value).then(function(t){l("next",t)},function(t){l("throw",t)}):s(n.done?"return":"normal",n.value)}catch(t){s("throw",t)}}function s(t,e){switch(t){case"return":r.resolve({value:e,done:!0});break;case"throw":r.reject(e);break;default:r.resolve({value:e,done:!1})}(r=r.next)?l(r.key,r.arg):u=null}this._invoke=function(i,o){return new Promise(function(t,e){var n={key:i,arg:o,resolve:t,reject:e,next:null};u?u=u.next=n:(r=u=n,l(i,o))})},"function"!=typeof o.return&&(this.return=void 0)}"function"==typeof Symbol&&Symbol.asyncIterator&&(t.prototype[Symbol.asyncIterator]=function(){return this}),t.prototype.next=function(t){return this._invoke("next",t)},t.prototype.throw=function(t){return this._invoke("throw",t)},t.prototype.return=function(t){return this._invoke("return",t)}}(),function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}),t=function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)},s=function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e},e=o.getComponent("MenuButton"),r=o.getComponent("Menu"),a=o.getComponent("Component"),c=o.dom;var h=function(e){function n(t){return l(this,n),s(this,e.call(this,t,{title:t.localize("Quality"),name:"QualityButton"}))}return t(n,e),n.prototype.createItems=function(){return[]},n.prototype.createMenu=function(){var t,e=new r(this.player_,{menuButton:this});if(this.hideThreshold_=0,this.options_.title){var n=c.createEl("li",{className:"vjs-menu-title",innerHTML:(t=this.options_.title,"string"!=typeof t?t:t.charAt(0).toUpperCase()+t.slice(1)),tabIndex:-1}),i=new a(this.player_,{el:n});this.hideThreshold_+=1,e.addItem(i)}if(this.items=this.createItems(),this.items)for(var o=0;o<this.items.length;o++)e.addItem(this.items[o]);return e},n}(e),i=function(r){function u(t,e,n,i){l(this,u);var o=s(this,r.call(this,t,{label:e.label,selectable:!0,selected:e.selected||!1}));return o.item=e,o.qualityButton=n,o.plugin=i,o}return t(u,r),u.prototype.handleClick=function(){for(var t=0;t<this.qualityButton.items.length;++t)this.qualityButton.items[t].selected(!1);this.plugin.setQuality(this.item.value),this.selected(!0)},u}(o.getComponent("MenuItem")),y={},n=o.registerPlugin||o.plugin,f=function(){function n(t,e){l(this,n),this.player=t,this.config=e,this.player.qualityLevels&&this.getHls()&&(this.createQualityButton(),this.bindPlayerEvents())}return n.prototype.getHls=function(){return this.player.tech({IWillNotUseThisInPlugins:!0}).hls},n.prototype.bindPlayerEvents=function(){this.player.qualityLevels().on("addqualitylevel",this.onAddQualityLevel.bind(this))},n.prototype.createQualityButton=function(){var t=this.player;this._qualityButton=new h(t);var e=t.controlBar.children().length-2,n=t.controlBar.addChild(this._qualityButton,{componentClass:"qualitySelector"},this.config.placementIndex||e);if(n.addClass("vjs-quality-selector"),this.config.displayCurrentQuality)this.setButtonInnerText("auto");else{var i=" "+(this.config.vjsIconClass||"vjs-icon-hd");n.menuButton_.$(".vjs-icon-placeholder").className+=i}n.removeClass("vjs-hidden")},n.prototype.setButtonInnerText=function(t){this._qualityButton.menuButton_.$(".vjs-icon-placeholder").innerHTML=t},n.prototype.getQualityMenuItem=function(t){var e=this.player;return new i(e,t,this._qualityButton,this)},n.prototype.onAddQualityLevel=function(){for(var n=this,t=this.player,i=t.qualityLevels().levels_||[],o=[],e=function(e){if(!o.filter(function(t){return t.item&&t.item.value===i[e].height}).length){var t=n.getQualityMenuItem.call(n,{label:i[e].height+"p",value:i[e].height});o.push(t)}},r=0;r<i.length;++r)e(r);o.sort(function(t,e){return"object"!==(void 0===t?"undefined":u(t))||"object"!==(void 0===e?"undefined":u(e))?-1:t.item.value<e.item.value?-1:t.item.value>e.item.value?1:0}),o.push(this.getQualityMenuItem.call(this,{label:t.localize("Auto"),value:"auto",selected:!0})),this._qualityButton&&(this._qualityButton.createItems=function(){return o},this._qualityButton.update())},n.prototype.setQuality=function(t){var e=this.player.qualityLevels();this._currentQuality=t,this.config.displayCurrentQuality&&this.setButtonInnerText("auto"===t?t:t+"p");for(var n=0;n<e.length;++n){var i=e[n];i.enabled=i.height===t||"auto"===t}this._qualityButton.unpressButton()},n.prototype.getCurrentQuality=function(){return this._currentQuality||"auto"},n}(),p=function(n){var i=this;this.ready(function(){var t,e;t=i,e=o.mergeOptions(y,n),t.addClass("vjs-hls-quality-selector"),t.hlsQualitySelector=new f(t,e)})};return n("hlsQualitySelector",p),p.VERSION="1.1.4",p});
\ No newline at end of file
diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep
index eee97b7b0e6935b6b64915f21d318186450b3abd..b1f60cefdeedb34e243924959805004fd99a7251 100644
--- a/templates/layouts/default.html.ep
+++ b/templates/layouts/default.html.ep
@@ -8,6 +8,8 @@
     <link rel="shortcut icon" href="/favicon.ico">
     <script src="/bundle.js"></script>
     <script src="/js/video.min.js"></script>
+    <script src="/js/videojs-contrib-quality-levels.min.js"></script>
+    <script src="/js/videojs-hls-quality-selector.min.js"></script>
   </head>
 
   <body>
diff --git a/templates/stream/player.html.ep b/templates/stream/player.html.ep
index a50f6bc53bd76542302c289347ac1a2b4607238e..6d62d35ba6f26947d6e3159069b766e32805b026 100644
--- a/templates/stream/player.html.ep
+++ b/templates/stream/player.html.ep
@@ -24,6 +24,7 @@ var player = videojs('#player',{
     },
   },
 });
+player.hlsQualitySelector();
 player.play();
 </script>
 
diff --git a/webpack.config.js b/webpack.config.js
index da8fb0051b051707fe4195b7c4d1f64aeea23c14..4c9112ca6bc78dd3f52e0e03ec60e92a9274ba67 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -10,7 +10,11 @@ module.exports = {
         path: path.resolve(__dirname, 'public')
     },
     module: {
-        noParse: [ /node_modules\/video.js\/dist\/video.min.js/ ],
+        noParse: [
+            /node_modules\/video.js\/dist\/video.min.js/,
+            /node_modules\/videojs-contrib-quality-levels\/dist\/videojs-contrib-quality-levels.min.js/,
+            /node_modules\/videojs-hls-quality-selector\/dist\/videojs-hls-quality-selector.min.js/
+        ],
 
         rules: [
             {