From 48640cd12431fe49e9fd5f9259547a797de5d9e8 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 10 Sep 2025 16:22:14 +0800 Subject: [PATCH 01/11] bugfix --- bricks/build.sh | 2 +- bricks/css/bricks.css | 68 +++++++ bricks/utils.js | 24 +++ bricks/videoplayer.js | 433 ++++++++++++++++++++---------------------- 4 files changed, 297 insertions(+), 230 deletions(-) diff --git a/bricks/build.sh b/bricks/build.sh index ba32dba..b454654 100755 --- a/bricks/build.sh +++ b/bricks/build.sh @@ -10,7 +10,7 @@ SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \ llm_dialog.js llm.js websocket.js datarow.js tabular.js continueaudio.js \ line.js pie.js bar.js gobang.js period.js iconbarpage.js \ keypress.js asr.js webspeech.js countdown.js progressbar.js \ - qaframe.js svg.js " + qaframe.js svg.js videoplayer.js " echo ${SOURCES} cat ${SOURCES} > ../dist/bricks.js # uglifyjs --compress --mangle -- ../dist/bricks.js > ../dist/bricks.min.js diff --git a/bricks/css/bricks.css b/bricks/css/bricks.css index 0231a83..1aac5cb 100755 --- a/bricks/css/bricks.css +++ b/bricks/css/bricks.css @@ -506,3 +506,71 @@ hr { align-self: center; } +.video-container { + position: relative; + width: 80%; + max-width: 1000px; + overflow: hidden; + border-radius: 12px; + background: #000; +} + +.video-element { + width: 100%; + height: auto; + display: block; +} + +.controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); + color: white; + font-size: 14px; + padding: 10px 15px; + transition: opacity 0.3s; + opacity: 0.9; +} + +.controls:hover { + opacity: 1; +} + +.progress-container { + margin-bottom: 10px; +} + +.progress-bar { + width: 100%; + accent-color: #ff0000; +} + +.controls-bottom { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.play-pause, .mute, .fullscreen { + background: none; + border: none; + color: white; + font-size: 18px; + cursor: pointer; +} + +.volume, .playback-speed, .audio-track-select { + font-size: 14px; + padding: 2px; +} + +.time { + font-family: monospace; +} + +.fullscreen { + margin-left: auto; +} diff --git a/bricks/utils.js b/bricks/utils.js index 08dfa3a..eecfade 100644 --- a/bricks/utils.js +++ b/bricks/utils.js @@ -704,6 +704,30 @@ function blobToBase64(blob) { reader.readAsDataURL(blob); }); } +/* +opts = { + css: + id +} +*/ +bricks.dom_create(tag, opts){ + var e = document.createElement(tag); + if (opts.css){ + var arr = css.split(' '); + arr.forEach(c =>{ + e.classList.add(c); + }); + } + if (opts.id){ + e.id = opts.id; + } + return e; +} +bricks.element_from_html(html){ + var e = document.createElement('div'); + e.outerHTML = html; + return e; +} /* // 使用队列 const queue = new Queue(); diff --git a/bricks/videoplayer.js b/bricks/videoplayer.js index bc4bea8..5d1249b 100644 --- a/bricks/videoplayer.js +++ b/bricks/videoplayer.js @@ -1,230 +1,78 @@ - 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 + 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.video = videoElement; + this.set_css('video-container'); + this.video = bricks.element_from_html(``); + this.controls = `
+
+ +
+
+ +
+ + +
+ 00:00 / 00:00 +
+ +
+
+ +
+ +
+
` + this.dom_element.appendChild(this.video); + this.dom_element.appendChild(this.controls); + this.hls = null; this.dashPlayer = null; - this.audioTrackSelect = document.getElementById('audioTrackSelect'); - this.timeDisplay = document.getElementById('timeDisplay'); - /* - this.initEventListeners(); - this.initTrackChangeHandler(); - */ - } - create(){ - this.dom_element = document.createElement('video', {controls: true}); - } - // 播放指定 URL 的视频 - load(src) { - const video = this.video; + this.playPauseBtn = this.controls.querySelector('.play-pause'); + this.muteBtn = this.controls.querySelector('.mute'); + this.volumeInput = this.controls.querySelector('.volume'); + this.progressBar = this.controls.querySelector('.progress-bar'); + 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.destroy(); + this.bind('domon', this.init.bind(this)); + this.bind('domoff', this.destroy.bind(this)); + } - // 判断视频类型 - if (Hls.isSupported() && src.endsWith('.m3u8')) { - this.loadHLS(src); - } else if (src.endsWith('.mpd')) { - this.loadDASH(src); - } else { - // 普通视频(MP4/WebM/Ogg) - video.src = src; - video.load(); - this.setupEventHandlers(); + init() { + this.loadVideo(this.opts.url); // 可替换为 mp4 / m3u8 / mpd + this.bindEvents(); + this.updateUI(); + if (this.opts.autoplay && this.video.paused){ + this.playPauseBtn.click(); } } - loadHLS(src) { - this.hls = new Hls(); - this.hls.loadSource(src); - this.hls.attachMedia(this.video); - this.hls.on(Hls.Events.MANIFEST_PARSED, () => { - this.video.play().catch(e => console.warn("自动播放被阻止", e)); - this.setupEventHandlers(); - this.updateAudioTracks(); - }); - this.hls.on(Hls.Events.ERROR, (event, data) => { - if (data.fatal) { - console.error('HLS 错误:', data); - if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { - this.hls.startLoad(); - } - } - }); - } - - loadDASH(src) { - this.dashPlayer = dashjs.MediaPlayer().create(); - this.dashPlayer.initialize(this.video, src, true); - this.dashPlayer.on('error', (e) => { - console.error('DASH 播放出错:', e); - }); - this.setupEventHandlers(); - setTimeout(() => this.updateAudioTracks(), 1000); // 延迟获取音轨 - } - - setupEventHandlers() { - const video = this.video; - - video.addEventListener('play', () => this.onPlay()); - video.addEventListener('pause', () => this.onPause()); - video.addEventListener('timeupdate', () => this.onTimeUpdate()); - video.addEventListener('loadedmetadata', () => this.onLoadedMetadata()); - video.addEventListener('ended', () => this.onEnded()); - video.addEventListener('error', (e) => this.onError(e)); - } - - // 事件回调 - onPlay() { - console.log('视频开始播放'); - } - - onPause() { - console.log('视频暂停'); - } - - onTimeUpdate() { - const { currentTime, duration } = this.video; - this.timeDisplay.textContent = `${this.formatTime(currentTime)} / ${this.formatTime(duration)}`; - } - - onLoadedMetadata() { - console.log('元数据加载完成'); - this.updateAudioTracks(); - } - - onEnded() { - console.log('视频播放结束'); - } - - onError(e) { - console.error('视频播放错误:', e); - } - - // 音轨切换支持 - updateAudioTracks() { - const select = this.audioTrackSelect; - select.innerHTML = ''; // 清空 - - const tracks = this.getAudioTracks(); - - if (tracks.length === 0) { - const option = document.createElement('option'); - option.textContent = '无可用音轨'; - option.disabled = true; - select.appendChild(option); - select.disabled = true; - return; - } - - select.disabled = false; - - tracks.forEach((track, index) => { - const option = document.createElement('option'); - option.value = index; - option.textContent = track.label || track.language || `音轨 ${index + 1}`; - select.appendChild(option); - }); - - // 默认选中第一个 - select.value = 0; - this.bindAudioTrackChange(); - } - - getAudioTracks() { - const video = this.video; - - // DASH 情况:使用 dash.js API - if (this.dashPlayer) { - const audioTracks = this.dashPlayer.getTracksFor('audio'); - return audioTracks.map(t => ({ - label: t.label || t.lang, - language: t.lang, - _track: t - })); - } - - // HLS 情况:使用 hls.js 的 audio tracks - if (this.hls && this.hls.audioTracks.length > 0) { - return this.hls.audioTracks.map(track => ({ - label: track.name || track.lang, - language: track.lang, - _track: track - })); - } - - // 原生 HTMLTrackElement(较少见,主要用于 ) - const nativeTracks = Array.from(video.audioTracks || []); - if (nativeTracks.length > 0) { - return nativeTracks.map(track => ({ - label: track.label || track.language, - language: track.language, - _track: track - })); - } - - // fallback:返回主音轨 - return [{ label: '主音轨', language: 'und' }]; - } - - bindAudioTrackChange() { - this.audioTrackSelect.removeEventListener('change', this.handleAudioTrackChange.bind(this)); - this.audioTrackSelect.addEventListener('change', this.handleAudioTrackChange.bind(this)); - } - - handleAudioTrackChange() { - const selectedIndex = parseInt(this.audioTrackSelect.value, 10); - const tracks = this.getAudioTracks(); - const selected = tracks[selectedIndex]; - - if (this.hls && selected._track && selected._track.id !== undefined) { - this.hls.currentLevel = -1; // 确保视频流不变 - this.hls.audioTrack = selected._track.id; - } - - if (this.dashPlayer && selected._track) { - this.dashPlayer.setCurrentTrack(selected._track); - } - - // 对于原生音轨(较少见) - if (selected._track && selected._track.enabled !== undefined) { - Array.from(this.video.audioTracks).forEach(t => { - t.enabled = (t === selected._track); - }); - } - - console.log('切换到音轨:', selected); - } - - // 工具函数:格式化时间 - formatTime(seconds) { - const s = Math.floor(seconds % 60).toString().padStart(2, '0'); - const m = Math.floor((seconds / 60) % 60).toString().padStart(2, '0'); - const h = Math.floor(seconds / 3600).toString().padStart(2, '0'); - return seconds >= 3600 ? `${h}:${m}:${s}` : `${m}:${s}`; - } - - // 播放/暂停控制 - togglePlay() { - if (this.video.paused) { - this.video.play().catch(e => console.warn("播放失败:", e)); - } else { - this.video.pause(); - } - } - - // 销毁当前播放器实例 - destroy() { + loadVideo(src) { + // 销毁旧播放器 if (this.hls) { this.hls.destroy(); this.hls = null; @@ -233,25 +81,152 @@ bricks VideoPlayer = class extends bricks.VBox { this.dashPlayer.reset(); this.dashPlayer = null; } + + this.video.src = ''; // 清空 + + if (src.endsWith('.m3u8') || src.includes('m3u8')) { + if (Hls.isSupported()) { + this.hls = new Hls(); + 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', () => { + if (this.video.paused) { + this.video.play(); + } else { + this.video.pause(); + } + }); + + // 静音切换 + this.muteBtn.addEventListener('click', () => { + 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.progressBar.addEventListener('input', (e) => { + const time = e.target.value * this.video.duration; + this.video.currentTime = time; + }); + + // 播放速度 + 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', () => { + if (this.dom_element.requestFullscreen) { + this.dom_element.requestFullscreen(); + } else if (this.dom_element.webkitRequestFullscreen) { + this.dom_element.webkitRequestFullscreen(); + } + }); + + // 视频事件 + 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(); + }); + this.video.addEventListener('seeking', () => { + this.progressBar.value = this.video.currentTime / this.video.duration; + }); + } + + 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.progressBar.value = percent; + 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')}`; + } + } - -// 初始化播放器 -document.addEventListener('DOMContentLoaded', () => { - const video = document.getElementById('videoPlayer'); - const player = new VideoPlayer(video); - - // 绑定播放/暂停按钮 - document.getElementById('playPause').addEventListener('click', () => { - player.togglePlay(); - }); - - // 示例:自动加载一个 m3u8 视频(替换为你的实际链接) - // player.load('https://example.com/stream.m3u8'); - - // 或者加载 DASH - // player.load('https://example.com/stream.mpd'); - - // 或者普通 MP4 - player.load('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'); -}); +bricks.Factory.register('VideoPlayer', bricks.VideoPlayer); From 155734cc078ae6f93fd0b11e591ad2767131468e Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 10 Sep 2025 16:25:29 +0800 Subject: [PATCH 02/11] bugfix --- bricks/header.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bricks/header.tmpl b/bricks/header.tmpl index bebca5b..12c8fc0 100644 --- a/bricks/header.tmpl +++ b/bricks/header.tmpl @@ -34,6 +34,8 @@ ---> + +