1/** 2 * WpStream quality selector for Video.js (VHS/HLS). 3 * 4 * Avoids external selector plugins that can break on Video.js 8. 5 * Requires: videojs-contrib-quality-levels 6 */ 7(function (window) { 8 'use strict'; 9 10 if (!window.videojs) { 11 return; 12 } 13 14 const videojs = window.videojs; 15 16 function isHlsPlayer(player) { 17 try { 18 // VHS exposes qualityLevels() on the player when HLS/MPD tech is used 19 return typeof player.qualityLevels === 'function'; 20 } catch (e) { 21 return false; 22 } 23 } 24 25 function getLevels(player) { 26 try { 27 const ql = player.qualityLevels(); 28 if (!ql) return []; 29 const arr = []; 30 for (let i = 0; i < ql.length; i++) arr.push(ql[i]); 31 return arr; 32 } catch (e) { 33 return []; 34 } 35 } 36 37 function prettyLabel(level) { 38 if (!level) return ''; 39 const h = level.height; 40 const w = level.width; 41 const br = level.bitrate; 42 if (h) return `${h}p`; 43 if (w && level.height) return `${w}x${level.height}`; 44 if (br) return `${Math.round(br / 1000)} kbps`; 45 return 'Quality'; 46 } 47 48 function enableAll(levels) { 49 levels.forEach((l) => { 50 try { 51 l.enabled = true; 52 } catch (e) {} 53 }); 54 } 55 56 function enableOnly(levels, selected) { 57 levels.forEach((l) => { 58 try { 59 l.enabled = l === selected; 60 } catch (e) {} 61 }); 62 } 63 64 const MenuButton = videojs.getComponent('MenuButton'); 65 const MenuItem = videojs.getComponent('MenuItem'); 66 67 class WpstreamQualityMenuItem extends MenuItem { 68 constructor(player, options) { 69 super(player, options); 70 this.wpstreamLevel = options.wpstreamLevel || null; 71 this.wpstreamIsAuto = !!options.wpstreamIsAuto; 72 this.controlText(options.label || ''); 73 this.addClass('vjs-wpstream-quality-item'); 74 this.selected(!!options.selected); 75 } 76 77 handleClick() { 78 const player = this.player(); 79 const levels = getLevels(player); 80 81 if (this.wpstreamIsAuto) { 82 enableAll(levels); 83 try { 84 player.trigger('wpstreamqualitychange'); 85 } catch (e) {} 86 return; 87 } 88 89 if (this.wpstreamLevel) { 90 enableOnly(levels, this.wpstreamLevel); 91 try { 92 player.trigger('wpstreamqualitychange'); 93 } catch (e) {} 94 } 95 } 96 } 97 98 class WpstreamQualityMenuButton extends MenuButton { 99 constructor(player, options) { 100 super(player, options); 101 this.addClass('vjs-wpstream-quality'); 102 this.controlText('Quality'); 103 104 // Rebuild menu when quality levels change. 105 player.on('loadedmetadata', () => this.update()); 106 player.on('loadeddata', () => this.update()); 107 player.on('wpstreamqualitychange', () => this.update()); 108 109 try { 110 const ql = player.qualityLevels && player.qualityLevels(); 111 if (ql && typeof ql.on === 'function') { 112 ql.on('addqualitylevel', () => this.update()); 113 ql.on('removequalitylevel', () => this.update()); 114 ql.on('change', () => this.update()); 115 } 116 } catch (e) {} 117 } 118 119 createItems() { 120 const player = this.player(); 121 const levels = getLevels(player); 122 123 // Always show the menu button, even if there's only one level. 124 // This is useful for debugging / confirming the stream is being interpreted as HLS. 125 try { 126 this.show(); 127 } catch (e) {} 128 129 const sorted = levels 130 .slice() 131 .sort((a, b) => (b.height || 0) - (a.height || 0) || (b.bitrate || 0) - (a.bitrate || 0)); 132 133 const items = []; 134 135 // Auto 136 items.push( 137 new WpstreamQualityMenuItem(player, { 138 label: 'Auto', 139 wpstreamIsAuto: true, 140 selectable: true, 141 selected: sorted.length > 0 ? sorted.every((l) => l.enabled === true) : true, 142 }) 143 ); 144 145 // If VHS hasn't populated levels yet, keep Auto only (menu still visible). 146 if (sorted.length === 0) { 147 return items; 148 } 149 150 // Dedupe by label (common case: duplicate heights) 151 const seen = new Set(); 152 sorted.forEach((level) => { 153 const label = prettyLabel(level); 154 if (!label) return; 155 if (seen.has(label)) return; 156 seen.add(label); 157 158 items.push( 159 new WpstreamQualityMenuItem(player, { 160 label, 161 wpstreamLevel: level, 162 selectable: true, 163 selected: level.enabled === true && sorted.filter((l) => l.enabled === true).length === 1, 164 }) 165 ); 166 }); 167 168 return items; 169 } 170 } 171 172 videojs.registerComponent('WpstreamQualityMenuButton', WpstreamQualityMenuButton); 173 174 /** 175 * Install the quality menu on a player. 176 */ 177 function install(player) { 178 if (!player || player.isDisposed && player.isDisposed()) return; 179 180 // Ensure we only install once. 181 if (player.wpstreamQualitySelectorInitialized) return; 182 player.wpstreamQualitySelectorInitialized = true; 183 184 // Only for VHS/HLS players. 185 if (!isHlsPlayer(player)) return; 186 187 player.ready(function () {
188 try { 189 const cb = player.getChild('controlBar'); 190 if (!cb) return; 191 192 // Avoid duplicates. 193 if (cb.getChild('WpstreamQualityMenuButton')) return; 194 195 // Place near the right side (before fullscreen if available) 196 const children = cb.children(); 197 let insertIndex = children ? children.length - 2 : undefined; 198 if (insertIndex < 0) insertIndex = undefined; 199 200 cb.addChild('WpstreamQualityMenuButton', {}, insertIndex); 201 } catch (e) { 202 // ignore 203 } 204 }); 205 } 206 207 window.wpstreamInstallQualitySelector = install; 208})(window); 209
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.