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// IIFE keeps every helper and component class out of the global scope; only the 8// installer (window.wpstreamInstallQualitySelector) is exported at the end. 9(function (window) { 10 'use strict'; 11 12 // Nothing to do if Video.js was not loaded on this page. 13 if (!window.videojs) { 14 return; 15 } 16 17 // Local handle to the global Video.js factory. 18 const videojs = window.videojs; 19 20 /** 21 * Detect whether a player is backed by VHS (HTTP streaming / HLS-MPD tech). 22 * 23 * @param {Object} player Video.js player instance. 24 * @return {boolean} True when the player exposes qualityLevels(). 25 */ 26 function isHlsPlayer(player) { 27 try { 28 // VHS exposes qualityLevels() on the player when HLS/MPD tech is used 29 return typeof player.qualityLevels === 'function'; 30 } catch (e) { 31 // Any access error means we cannot treat this as an HLS player. 32 return false; 33 } 34 } 35 36 /** 37 * Read the current quality levels off a player as a plain array. 38 * 39 * @param {Object} player Video.js player instance. 40 * @return {Array} Snapshot of quality-level objects (empty on failure). 41 */ 42 function getLevels(player) { 43 try { 44 // The QualityLevelList is array-like but not a real Array. 45 const ql = player.qualityLevels(); 46 if (!ql) return []; 47 // Copy each indexed level into a real array we can slice/sort/iterate. 48 const arr = []; 49 for (let i = 0; i < ql.length; i++) arr.push(ql[i]); 50 return arr; 51 } catch (e) { 52 // Player disposed or plugin missing: behave as if there are no levels. 53 return []; 54 } 55 } 56 57 /** 58 * Build a human-readable label for a single quality level. 59 * 60 * @param {Object} level A quality-level object (may have height/width/bitrate). 61 * @return {string} Label such as "720p", "1280x720", "800 kbps", or fallback. 62 */ 63 function prettyLabel(level) { 64 if (!level) return ''; 65 // Prefer resolution height, then explicit dimensions, then bitrate. 66 const h = level.height; 67 const w = level.width; 68 const br = level.bitrate; 69 if (h) return `${h}p`; // e.g. "720p" 70 if (w && level.height) return `${w}x${level.height}`; // e.g. "1280x720" 71 if (br) return `${Math.round(br / 1000)} kbps`; // e.g. "800 kbps" 72 // Nothing useful to show: generic fallback label. 73 return 'Quality'; 74 } 75 76 /** 77 * Enable every quality level (used by the "Auto" choice so VHS is free to pick). 78 * 79 * @param {Array} levels Quality-level objects to enable. 80 */ 81 function enableAll(levels) { 82 levels.forEach((l) => { 83 try { 84 // Turning all levels on hands adaptive selection back to VHS. 85 l.enabled = true; 86 } catch (e) {} // ignore levels that reject the assignment 87 }); 88 } 89 90 /** 91 * Restrict playback to a single chosen level, disabling all others. 92 * 93 * @param {Array} levels Quality-level objects to toggle. 94 * @param {Object} selected The one level that should stay enabled. 95 */ 96 function enableOnly(levels, selected) { 97 levels.forEach((l) => { 98 try { 99 // Only the selected level stays enabled; everything else is off. 100 l.enabled = l === selected; 101 } catch (e) {} // ignore levels that reject the assignment 102 }); 103 } 104 105 // Base Video.js components we extend to build the quality control. 106 const MenuButton = videojs.getComponent('MenuButton'); 107 const MenuItem = videojs.getComponent('MenuItem'); 108 109 /** 110 * A single clickable row in the quality menu (either "Auto" or one level). 111 */ 112 class WpstreamQualityMenuItem extends MenuItem { 113 /** 114 * @param {Object} player Owning Video.js player. 115 * @param {Object} options Item config: label, wpstreamLevel, wpstreamIsAuto, selected. 116 */ 117 constructor(player, options) { 118 super(player, options); 119 // The concrete quality level this row selects (null for the Auto row). 120 this.wpstreamLevel = options.wpstreamLevel || null; 121 // Flag marking this row as the adaptive "Auto" choice. 122 this.wpstreamIsAuto = !!options.wpstreamIsAuto; 123 // Screen-reader / control text for the row. 124 this.controlText(options.label || ''); 125 // Styling hook for the menu item. 126 this.addClass('vjs-wpstream-quality-item'); 127 // Reflect whether this row is the currently active choice. 128 this.selected(!!options.selected); 129 } 130 131 /** 132 * Apply this row's quality choice when the user clicks it. 133 */ 134 handleClick() { 135 const player = this.player(); 136 // Re-read levels fresh; the list can change as segments load. 137 const levels = getLevels(player); 138 139 // Auto row: re-enable every level and let VHS adapt. 140 if (this.wpstreamIsAuto) { 141 enableAll(levels); 142 try { 143 // Notify the menu button so it rebuilds/repaints the selection. 144 player.trigger('wpstreamqualitychange'); 145 } catch (e) {} 146 return; 147 } 148 149 // Specific level row: lock playback to just that level. 150 if (this.wpstreamLevel) { 151 enableOnly(levels, this.wpstreamLevel); 152 try { 153 // Notify the menu button so it rebuilds/repaints the selection. 154 player.trigger('wpstreamqualitychange'); 155 } catch (e) {} 156 } 157 } 158 } 159 160 /** 161 * The control-bar button that opens the quality menu and keeps it in sync. 162 */ 163 class WpstreamQualityMenuButton extends MenuButton { 164 /** 165 * @param {Object} player Owning Video.js player. 166 * @param {Object} options Standard Video.js component options. 167 */ 168 constructor(player, options) { 169 super(player, options); 170 // Styling hook + accessible label for the button. 171 this.addClass('vjs-wpstream-quality'); 172 this.controlText('Quality'); 173 174 // Rebuild menu when quality levels change. 175 // Metadata/data load and our custom event all warrant a rebuild. 176 player.on('loadedmetadata', () => this.update()); 177 player.on('loadeddata', () => this.update()); 178 player.on('wpstreamqualitychange', () => this.update()); 179
180 try { 181 // Also react to VHS's own quality-level list mutations, if available. 182 const ql = player.qualityLevels && player.qualityLevels(); 183 if (ql && typeof ql.on === 'function') { 184 ql.on('addqualitylevel', () => this.update()); // new rendition appeared 185 ql.on('removequalitylevel', () => this.update()); // rendition dropped 186 ql.on('change', () => this.update()); // active rendition switched 187 } 188 } catch (e) {} // quality-levels plugin absent: static menu is fine 189 } 190 191 /** 192 * Build the list of menu rows (Auto + one row per distinct quality level). 193 * 194 * @return {Array} Array of WpstreamQualityMenuItem instances. 195 */ 196 createItems() { 197 const player = this.player(); 198 const levels = getLevels(player); 199 200 // Always show the menu button, even if there's only one level. 201 // This is useful for debugging / confirming the stream is being interpreted as HLS. 202 try { 203 this.show(); 204 } catch (e) {} 205 206 // Sort a copy highest-quality first: by height, then bitrate as tiebreak. 207 const sorted = levels 208 .slice() 209 .sort((a, b) => (b.height || 0) - (a.height || 0) || (b.bitrate || 0) - (a.bitrate || 0)); 210 211 // Accumulates the menu rows we will return. 212 const items = []; 213 214 // Auto 215 // First row is always "Auto"; selected when every level is currently enabled. 216 items.push( 217 new WpstreamQualityMenuItem(player, { 218 label: 'Auto', 219 wpstreamIsAuto: true, 220 selectable: true, 221 selected: sorted.length > 0 ? sorted.every((l) => l.enabled === true) : true, 222 }) 223 ); 224 225 // If VHS hasn't populated levels yet, keep Auto only (menu still visible). 226 if (sorted.length === 0) { 227 return items; 228 } 229 230 // Dedupe by label (common case: duplicate heights) 231 // Set tracks labels already added so we don't list the same resolution twice. 232 const seen = new Set(); 233 sorted.forEach((level) => { 234 const label = prettyLabel(level); 235 if (!label) return; // skip levels with no usable label 236 if (seen.has(label)) return; // skip duplicates of an already-listed label 237 seen.add(label); 238 239 // One row per distinct level; selected only when it is the sole enabled level. 240 items.push( 241 new WpstreamQualityMenuItem(player, { 242 label, 243 wpstreamLevel: level, 244 selectable: true, 245 selected: level.enabled === true && sorted.filter((l) => l.enabled === true).length === 1, 246 }) 247 ); 248 }); 249 250 return items; 251 } 252 } 253 254 // Register the button so it can be added to a player's control bar by name. 255 videojs.registerComponent('WpstreamQualityMenuButton', WpstreamQualityMenuButton); 256 257 /** 258 * Install the quality menu on a player. 259 * 260 * @param {Object} player Video.js player to attach the quality control to. 261 */ 262 function install(player) { 263 // Skip missing or already-disposed players. 264 if (!player || player.isDisposed && player.isDisposed()) return; 265 266 // Ensure we only install once. 267 // Guard flag prevents adding a second button on repeat calls. 268 if (player.wpstreamQualitySelectorInitialized) return; 269 player.wpstreamQualitySelectorInitialized = true; 270 271 // Only for VHS/HLS players. 272 if (!isHlsPlayer(player)) return; 273 274 // Defer until the player is ready so its control bar exists. 275 player.ready(function () { 276 try { 277 // Locate the control bar we will inject the button into. 278 const cb = player.getChild('controlBar'); 279 if (!cb) return; 280 281 // Avoid duplicates. 282 if (cb.getChild('WpstreamQualityMenuButton')) return; 283 284 // Place near the right side (before fullscreen if available) 285 // Insert two slots from the end so it sits just before the fullscreen toggle. 286 const children = cb.children(); 287 let insertIndex = children ? children.length - 2 : undefined; 288 if (insertIndex < 0) insertIndex = undefined; // fall back to default position 289 290 cb.addChild('WpstreamQualityMenuButton', {}, insertIndex); 291 } catch (e) { 292 // ignore 293 } 294 }); 295 } 296 297 // Expose the installer so player bootstrap code can call it per player. 298 window.wpstreamInstallQualitySelector = install; 299})(window); 300
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.