This commit is contained in:
yumoqing 2025-09-10 16:22:14 +08:00
parent 6384917c61
commit 48640cd124
4 changed files with 297 additions and 230 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -705,6 +705,30 @@ function blobToBase64(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();
queue.enqueue(1);

View File

@ -1,4 +1,3 @@
var bricks = window.bricks || {}
/*
use hls to play m3u8
@ -8,223 +7,72 @@ use dash to play dash
*/
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(`<video id="video" class="video-element"></video>`);
this.controls = `<div class="controls">
<div class="progress-container">
<input type="range" class="progress-bar" value="0" step="0.0001" />
</div>
<div class="controls-bottom">
<button class="play-pause"></button>
<div class="volume-container">
<button class="mute">🔊</button>
<input type="range" class="volume" min="0" max="1" step="0.01" value="1" />
</div>
<span class="time">00:00 / 00:00</span>
<div class="speed-container">
<select class="playback-speed">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
</div>
<div class="audio-tracks">
<select class="audio-track-select"></select>
</div>
<button class="fullscreen"></button>
</div>
</div>`
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较少见主要用于 <track kind="audio">
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());
}
}
}
// 初始化播放器
document.addEventListener('DOMContentLoaded', () => {
const video = document.getElementById('videoPlayer');
const player = new VideoPlayer(video);
onLoaded() {
this.updateAudioTracks();
this.updateUI();
}
// 绑定播放/暂停按钮
document.getElementById('playPause').addEventListener('click', () => {
player.togglePlay();
bindEvents() {
// 播放/暂停
this.playPauseBtn.addEventListener('click', () => {
if (this.video.paused) {
this.video.play();
} else {
this.video.pause();
}
});
// 示例:自动加载一个 m3u8 视频(替换为你的实际链接)
// player.load('https://example.com/stream.m3u8');
// 静音切换
this.muteBtn.addEventListener('click', () => {
this.video.muted = !this.video.muted;
this.updateMuteUI();
});
// 或者加载 DASH
// player.load('https://example.com/stream.mpd');
// 音量变化
this.volumeInput.addEventListener('input', (e) => {
this.video.volume = e.target.value;
this.video.muted = this.video.volume === 0;
this.updateMuteUI();
});
// 或者普通 MP4
player.load('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
});
// 进度条拖动
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')}`;
}
}
bricks.Factory.register('VideoPlayer', bricks.VideoPlayer);