var bricks = window.bricks || {} /* use hls to play m3u8 https://cdn.jsdelivr.net/npm/hls.js@latest use dash to play dash https://cdn.dashjs.org/latest/dash.all.min.js */ bricks.VideoPlayer = class extends bricks.VBox { /* opts: url: video source autoplay:true or false */ constructor(opts) { super(opts); this.set_css('video-container'); this.dom_element.innerHTML = `
00:00 / 00:00
` this.video = this.dom_element.querySelector('.video-element'); this.controls = this.dom_element.querySelector('.controls'); this.hls = null; this.dashPlayer = null; this.playPauseBtn = this.controls.querySelector('.play-pause'); this.muteBtn = this.controls.querySelector('.mute'); this.volumeInput = this.controls.querySelector('.volume'); this.timeDisplay = this.controls.querySelector('.time'); this.speedSelect = this.controls.querySelector('.playback-speed'); this.audioTrackSelect = this.controls.querySelector('.audio-track-select'); this.fullscreenBtn = this.controls.querySelector('.fullscreen'); this.bind('domon', this.init.bind(this)); this.bind('domoff', this.destroy.bind(this)); this.bind('click', this.show_controls.bind(this)); schedule_once(this.hide_controls.bind(this), 40); } show_controls(){ this.controls.style.display = ''; schedule_once(this.hide_controls.bind(this), 40); } hide_controls(){ this.controls.style.display = 'none'; } destroy(){ if (this.hls) { this.hls.destroy(); this.hls = null; } if (this.dashPlayer) { this.dashPlayer.reset(); this.dashPlayer = null; } this.video.src = ''; // 清空 } init() { this.loadVideo(this.opts.url); // 可替换为 mp4 / m3u8 / mpd this.bindEvents(); this.updateUI(); if (this.opts.autoplay && this.video.paused){ this.playPauseBtn.click(); } } loadVideo(src) { // 销毁旧播放器 this.destroy() if (src.endsWith('.m3u8') || src.includes('m3u8')) { if (Hls.isSupported()) { this.hls = new Hls({ enableWebVTT: false, // 不加载 WebVTT enableIMSC1: false, // 不加载 IMSC1/TTML renderTextTracksNatively: false // 不用浏览器原生 track }); this.hls.subtitleTrack = -1; // 关闭字幕轨道 this.hls.loadSource(src); this.hls.attachMedia(this.video); this.hls.on(Hls.Events.MANIFEST_PARSED, () => this.onLoaded()); } else { console.error('HLS not supported'); } } else if (src.endsWith('.mpd') || src.includes('mpd')) { this.dashPlayer = dashjs.MediaPlayer().create(); this.dashPlayer.initialize(this.video, src, true); this.dashPlayer.on('manifestParsed', () => this.onLoaded()); } else { // 普通视频 this.video.src = src; this.video.addEventListener('loadedmetadata', () => this.onLoaded()); } } onLoaded() { this.updateAudioTracks(); this.updateUI(); } bindEvents() { // 播放/暂停 this.playPauseBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); if (this.video.paused) { this.video.play(); } else { this.video.pause(); } }); // 静音切换 this.muteBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); this.video.muted = !this.video.muted; this.updateMuteUI(); }); // 音量变化 this.volumeInput.addEventListener('input', (e) => { this.video.volume = e.target.value; this.video.muted = this.video.volume === 0; this.updateMuteUI(); }); // 播放速度 this.speedSelect.addEventListener('change', (e) => { this.video.playbackRate = parseFloat(e.target.value); }); // 音轨切换 this.audioTrackSelect.addEventListener('change', (e) => { const index = parseInt(e.target.value); if (this.video.audioTracks) { for (let i = 0; i < this.video.audioTracks.length; i++) { this.video.audioTracks[i].enabled = i === index; } } }); // 全屏 this.fullscreenBtn.addEventListener('click', () => { var full_txt='⛶'; var norm_txt = ` `; if (this.dom_element == document.fullscreenElement){ this.fullscreenBtn.textContent = full_txt; if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { // Safari document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { // IE/Edge 旧版 document.msExitFullscreen(); } } else { this.fullscreenBtn.innerHTML = norm_txt; if (this.dom_element.requestFullscreen) { this.dom_element.requestFullscreen(); } else if (this.dom_element.webkitRequestFullscreen) { this.dom_element.webkitRequestFullscreen(); } else if(this.dom_element.msRequestFullscreen){ this.dom_element.msRequestFullscreen(); } } }); // 视频事件 this.video.addEventListener('play', () => this.updatePlayPauseUI()); this.video.addEventListener('pause', () => this.updatePlayPauseUI()); this.video.addEventListener('timeupdate', () => this.updateProgress()); this.video.addEventListener('durationchange', () => this.updateProgress()); this.video.addEventListener('volumechange', () => { this.updateMuteUI(); this.volumeInput.value = this.video.volume; }); this.video.addEventListener('loadedmetadata', () => { this.updateAudioTracks(); }); } updateUI() { this.updatePlayPauseUI(); this.updateMuteUI(); this.updateProgress(); this.volumeInput.value = this.video.volume; } updatePlayPauseUI() { this.playPauseBtn.textContent = this.video.paused ? '▶' : '❚❚'; } updateMuteUI() { this.muteBtn.textContent = this.video.muted || this.video.volume === 0 ? '🔇' : '🔊'; } updateProgress() { const percent = this.video.duration ? this.video.currentTime / this.video.duration : 0; this.timeDisplay.textContent = `${this.formatTime(this.video.currentTime)} / ${this.formatTime(this.video.duration || 0)}`; } updateAudioTracks() { this.audioTrackSelect.innerHTML = ''; if (this.video.audioTracks && this.video.audioTracks.length > 0) { for (let i = 0; i < this.video.audioTracks.length; i++) { const track = this.video.audioTracks[i]; const option = document.createElement('option'); option.value = i; option.textContent = track.label || `音轨 ${i + 1}`; if (track.enabled) option.selected = true; this.audioTrackSelect.appendChild(option); } } else { const option = document.createElement('option'); option.textContent = '无音轨'; option.disabled = true; this.audioTrackSelect.appendChild(option); } } formatTime(seconds) { const s = Math.floor(seconds % 60); const m = Math.floor((seconds / 60) % 60); const h = Math.floor(seconds / 3600); return h > 0 ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` : `${m}:${s.toString().padStart(2, '0')}`; } } bricks.Iptv = class extends bricks.VBox { /* { iptv_data_url: playok_url: playfailed_url: } */ constructor(opts){ super(opts); schedule_once(this.build_subwidgets.bind(this), 0.1); } async build_subwidgets(){ console.log('build_subwidgets called'); if (!this.user_data){ var jc = new bricks.HttpJson(); this.deviceid = bricks.deviceid('iptv') this.user_data = await jc.httpcall(this.iptv_data_url, { params:{ deviceid:this.deviceid }, method:'GET' }); } console.log('this.user_data =', this.user_data); this.video = new bricks.VideoPlayer({ autoplay:true, url:this.user_data.url }); this.title_w = new bricks.Text({text:this.user_data.tv_name, wrap:false}); this.add_widget(this.title_w); this.add_widget(this.video); this.video.bind('play_ok', this.report_play_ok.bind(this)); this.video.bind('play_failed', this.report_play_failed.bind(this)); } async report_play_ok(){ console.log(this.user_data, 'channel playing ...', this.playok_url); if (this.playok_url){ var ht = new bricks.HttpText(); var resp = ht.httpcall(this.playok_url,{ params:{ deviceid:this.deviceid, channelid:this.user_data.id }, method:"GET" }); if (resp != 'Error'){ console.log('report playok ok'); } else { console.log('report playok failed'); } } else { console.log('this.playok_url not defined', this.playok_url); } } async report_play_failed(){ console.log(this.user_data, 'channel play failed ...'); if (this.playfailed_url){ var ht = new bricks.HttpText(); var resp = ht.httpcall(this.playfailed_url,{ params:{ deviceid:this.deviceid, channelid:this.user_data.id }, method:"GET" }); if (resp != 'Error'){ console.log('report playfailed ok'); } else { console.log('report playfailed failed'); } } else { console.log('this.playfailed_url not defined', this.playfailed_url); } } setValue(data){ this.user_data = data; this.title_w.set_text(data.tv_name); this.video.set_url(data.url); } } bricks.Factory.register('Iptv', bricks.Iptv); bricks.Factory.register('VideoPlayer', bricks.VideoPlayer); bricks.Factory.register('Video', bricks.VideoPlayer);