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(较少见,主要用于