UNPKG

express-bitrate-streamer

Version:

Express-based HLS multi-bitrate streaming backend + tiny frontend player.

124 lines (113 loc) 4.06 kB
/* global Hls */ export default class BitratePlayer { /** * @param {HTMLVideoElement} video * @param {{log?:(msg:string)=>void}} opts */ constructor(video, opts = {}) { this.video = video; this.log = opts.log || (() => { }); this.hls = null; this.source = null; } _canPlayNatively(url) { return this.video.canPlayType('application/vnd.apple.mpegurl') || url.endsWith('.m3u8'); } async load(url) { this.destroy(); this.source = url; if (this._canPlayNatively(url) && !window.Hls) { this.video.src = url; await this.video.play().catch(() => { }); this._attachNativeEvents(); this.log('Using native HLS'); } else { if (!window.Hls) { throw new Error('hls.js not found. Include it via <script> tag.'); } if (!window.Hls.isSupported()) { throw new Error('hls.js is not supported in this browser'); } this.hls = new window.Hls({ // mild defaults; tweak for latency / stability maxBufferLength: 30, liveSyncDurationCount: 3, enableWorker: true, }); this.hls.attachMedia(this.video); this.hls.on(window.Hls.Events.MEDIA_ATTACHED, () => { this.hls.loadSource(url); }); this._attachHlsEvents(); await this.video.play().catch(() => { }); this.log('Using hls.js'); } } destroy() { if (this.hls) { this.hls.destroy(); this.hls = null; } if (this.video) { this.video.src = ''; this.video.removeAttribute('src'); this.video.load(); } } getLevels() { if (this.hls) { // hls.js provides detailed level info return (this.hls.levels || []).map(l => ({ bitrate: l.bitrate, width: l.width, height: l.height, name: l.name })); } // native: we can’t read levels; return empty return []; } setLevel(index) { if (this.hls) { // -1 => auto this.hls.nextLevel = index; } else { this.log('Native HLS: cannot set fixed level programmatically.'); } } _attachHlsEvents() { const H = window.Hls; this.hls.on(H.Events.ERROR, (evt, data) => { this.log(`[hls.js] ERROR: ${data.type} | ${data.details} | ${data.reason || ''}`); if (data.fatal) { switch (data.type) { case H.ErrorTypes.NETWORK_ERROR: this.hls.startLoad(); break; case H.ErrorTypes.MEDIA_ERROR: this.hls.recoverMediaError(); break; default: this.hls.destroy(); break; } } }); this.hls.on(H.Events.LEVEL_SWITCHED, (_, data) => { const lvl = this.hls.levels[data.level]; if (lvl) this.log(`Switched to: ${lvl.height}p (${Math.round(lvl.bitrate / 1000)} kbps)`); }); this.hls.on(H.Events.MANIFEST_PARSED, () => { this.log('Manifest parsed'); this.video.play().catch(() => { }); }); } _attachNativeEvents() { this.video.addEventListener('error', () => { const err = this.video.error; this.log(`[native] video error: ${err && err.message}`); }); this.video.addEventListener('loadedmetadata', () => this.log('[native] metadata loaded')); this.video.addEventListener('canplay', () => this.log('[native] canplay')); } }