This commit is contained in:
yumoqing 2025-09-15 16:30:34 +08:00
commit 5c55a7b37f
19 changed files with 26860 additions and 140 deletions

3
3parties/dash.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

26174
3parties/hls.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -198,6 +198,7 @@ bricks.buildBind = async function(w, desc){
return;
}
var event = desc.event;
desc.event_widget = widget;
await bricks.buildEventBind(w, widget, event, desc);
}

View File

@ -1,7 +1,7 @@
SOURCES=" page_data_loader.js factory.js uitypesdef.js utils.js uitype.js \
i18n.js widget.js layout.js bricks.js image.js html.js splitter.js \
jsoncall.js myoperator.js scroll.js menu.js popup.js camera.js modal.js running.js \
markdown_viewer.js video.js audio.js toolbar.js tab.js \
markdown_viewer.js audio.js toolbar.js tab.js \
input.js registerfunction.js button.js accordion.js dataviewer.js \
tree.js multiple_state_image.js dynamiccolumn.js form.js message.js conform.js \
paging.js datagrid.js iframe.js cols.js echartsext.js \
@ -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

@ -81,7 +81,8 @@ bricks.Camera = class extends bricks.Popup {
canvas.width = this.video.videoWidth;
const context = canvas.getContext('2d');
context.drawImage(this.video, 0, 0);
this.imgw.set_url(canvas.toDataURL('image/jpeg'));
this.dataurl = canvas.toDataURL('image/jpeg', 0.95);
this.imgw.set_url(this.dataurl);
this.task = schedule_once(this.show_picture.bind(this), this.task_period);
this.show_cnt += 1;
}
@ -118,8 +119,7 @@ bricks.Camera = class extends bricks.Popup {
}
this.task_period = 0;
this.task = null;
var d = this.imgw.base64();
this.dispatch('shot', d);
this.dispatch('shot', this.dataurl);
}
}

View File

@ -9,6 +9,14 @@ body {
display: flex;
}
.responsive-img {
max-width: 100%; /* 限制最大宽度为容器 */
width: 100%; /* 占满容器宽度 */
height: auto; /* 高度自动,保持比例 */
display: block; /* 避免底部留空隙 */
object-fit: contain; /* 确保完整显示,而不是裁切 */
}
pre {
overflow-x: auto; /* 允许内容超出容器显示 */
background-color: #b5e5e5;
@ -32,6 +40,15 @@ hr {
border-radius: 30%;
background-color: #f5f5f5;
}
.tabular {
border-radius: 8px;
padding: 5px;
margin: 5px;
background-color: #f5f5f5;
border: 1px solid #888888;
}
.card {
border-radius: 8px;
padding: 5px;
@ -39,6 +56,12 @@ hr {
background-color: #f5f5f5;
border: 1px solid #888888;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
border: 2px solid #ff8080;
}
.subcard {
background-color: #eeeeee;
}
@ -403,7 +426,7 @@ hr {
flex-shrink: 0;
}
.tabular-row:nth-child(odd) {
background-color: #5dfdfd;
background-color: #efefef;
}
.tabular-row:nth-child(even) {
background-color: #f9f9f9;
@ -492,3 +515,72 @@ hr {
align-self: center;
}
.video-container {
position: relative;
width: 100%;
height: auto;
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

@ -34,6 +34,8 @@
--->
<script src="{{entire_url('/bricks/3parties/recorder.wav.min.js')}}"></script>
<script src="https://sage.opencomputing.cn/bricks/3parties/hls.js"></script>
<script src="https://sage.opencomputing.cn/bricks/3parties/dash.all.min.js"></script>
<script src="{{entire_url('/bricks/bricks.js')}}"></script>
<script src="{{entire_url('/js/myapp.js')}}"></script>
<script>

View File

@ -10,24 +10,13 @@ bricks.Image = class extends bricks.JsWidget {
constructor(opts){
super(opts);
this.opts = opts;
this.options_parse();
if (this.opts.url){
this.set_url(this.opts.url);
}
}
create(){
this.dom_element = document.createElement('img');
}
options_parse(){
if (this.opts.hasOwnProperty('url')){
this.set_url(this.opts.url);
}
if (this.opts.hasOwnProperty('width')){
this.width = this.opts.width;
this.dom_element.style.width = this.width;
}
if (this.opts.hasOwnProperty('height')){
this.height = this.opts.height;
this.dom_element.style.height = this.height;
}
}
removeBase64Header(base64String) {
return base64String.replace(/^data:[^;]*;base64,/, '');
}
@ -51,8 +40,16 @@ bricks.Image = class extends bricks.JsWidget {
}
set_url(url){
this.url = url;
if (this.opts.default_url) {
this.dom_element.onerror = this.set_default_url.bind(this)
}
this.dom_element.src = url;
}
set_default_url(){
console.log('default_url', this.opts.default_url);
this.dom_element.onerror = null;
this.dom_element.src = this.opts.default_url;
}
}
bricks.Icon = class extends bricks.Image {

1
bricks/imgs/camera.svg Normal file
View File

@ -0,0 +1 @@
<svg t="1756730700908" class="icon" viewBox="0 0 1381 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5410" width="100%" height="100%"><path d="M690.604651 840.453953c-160.148837 0-290.232558-131.143442-290.232558-291.923348C400.372093 387.738791 530.455814 256.595349 690.604651 256.595349s290.232558 131.143442 290.232558 291.935256C980.837209 709.298605 850.753488 840.453953 690.604651 840.453953z m0-511.095069c-119.962791 0-217.6 98.208744-217.6 218.874046 0 120.665302 97.637209 218.874047 217.6 218.874047s217.6-98.208744 217.6-218.874047c0-120.665302-97.637209-218.874047-217.6-218.874046z" fill="#000000" p-id="5411"></path><path d="M1343.702326 1023.106977H37.506977c-19.944186 0-36.316279-16.467349-36.316279-36.530605V183.546047c0-20.063256 16.372093-36.530605 36.316279-36.530605h325.060465l110.437209-132.941395a35.816186 35.816186 0 0 1 27.981396-13.181024H881.116279c10.716279 0 20.837209 4.798512 27.981395 13.181024l110.43721 132.941395h325.060465c19.944186 0 36.316279 16.467349 36.316279 36.530605v803.030325c-0.893023 20.063256-16.967442 36.530605-37.209302 36.530605zM73.823256 950.045767h1233.860465V220.064744H1001.972093c-10.716279 0-20.837209-4.786605-27.981395-13.169116L863.553488 73.954233h-345.897674L407.218605 206.895628a35.816186 35.816186 0 0 1-27.981396 13.169116H73.823256V950.057674z" fill="${color}" p-id="5412"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -516,7 +516,7 @@ bricks.UiImage =class extends bricks.VBox {
"width":"90%"
});
camera.bring_to_top();
camera.bind('shot', this.accept_photo.bind(this));
camera.bind('shot', this.accept_photo.bind(this, camera));
}
accept_photo(camera, event){
camera.dismiss();
@ -527,6 +527,7 @@ bricks.UiImage =class extends bricks.VBox {
url:event.params,
width:'100%'
});
this.value = event.params
this.add_widget(this.imgw);
}
handleFileSelect(event){
@ -546,9 +547,8 @@ bricks.UiImage =class extends bricks.VBox {
this.remove_widget(this.imgw);
}
this.value = e.target.result;
console.log('this.value=', this.value);
this.imgw = new bricks.Image({
url:e.target.result,
url:this.value,
width:'100%'
});
this.add_widget(this.imgw);
@ -560,20 +560,14 @@ bricks.UiImage =class extends bricks.VBox {
fd.append(this.name, this.resultValue());
}
resultValue(){
if (this.imgw){
this.value = this.imgw.base64();
if (this.value){
return this.value;
}
return null;
}
getValue(){
var ret = {}
if (this.imgw){
// ret[this.name] = this.imgw.base64()
ret[this.name] = this.value;
} else {
ret[this.name] = null;
}
var ret = {};
ret[this.name] = this.resultValue();
return ret;
}
}
@ -1104,7 +1098,7 @@ bricks.UiVideo =class extends bricks.UiStr {
url: bricks_resource('imgs/right_arrow.svg')});
this.add_widget(this.icon);
this.icon.bind('click', this.play_audio.bind(this));
this.player = new bricks.VideoPlayer({
this.player = new bricks.Video({
url:this.value
});
if (this.autoplay){

View File

@ -75,7 +75,7 @@ bricks.ModelOutput = class extends bricks.VBox {
this.filler.set_css('filler');
this.content.add_widget(new bricks.BlankIcon({rate:2, flexShrink:0}));
this.content.add_widget(this.filler);
this.content.add_widget(new bricks.BlankIcon({rate:2, flexShrink:0}));
// this.content.add_widget(new bricks.BlankIcon({rate:2, flexShrink:0}));
this.build_estimate_widgets();
}
build_estimate_widgets(){
@ -116,18 +116,19 @@ bricks.ModelOutput = class extends bricks.VBox {
}
async update_data(data){
if (this.run) {
this.received_content = '';
this.received_reasoning_content = '';
this.run.stop_timepass();
this.content.remove_widget(this.run);
if(this.textvoice){
this.upstreaming = new bricks.UpStreaming({
url:this.tts_url
});
this.upstreaming.go();
}
if (data.content){
this.received_content += data.content;
}
if (this.upstreaming){
this.upstreaming.send(data.content);
data.content = bricks.escapeSpecialChars(this.received_content);
if (data.reasoning_content){
this.received_reasoning_content += data.reasoning_content;
}
data.reasoning_content = bricks.escapeSpecialChars(this.received_reasoning_content);
this.run = null;
this.filler.clear_widgets();
if (typeof this.output_view === 'string'){
@ -151,9 +152,7 @@ bricks.ModelOutput = class extends bricks.VBox {
}
}
finish(){
if (this.upstreaming){
this.upstreaming.finish();
}
console.log('finished')
}
}
@ -178,7 +177,6 @@ bricks.LlmModel = class extends bricks.JsWidget {
constructor(llmio, opts){
super(opts);
this.llmio = llmio;
this.messages = [];
}
render_title(){
var w = new bricks.HBox({padding:'15px'});
@ -188,8 +186,8 @@ bricks.LlmModel = class extends bricks.JsWidget {
tip:this.opts.modelname,
url:this.opts.icon||bricks_resource('imgs/llm.svg')
});
// var txt = new bricks.Text({text:this.opts.modelname});
w.add_widget(img);
// var txt = new bricks.Text({text:this.opts.modelname});
// w.add_widget(txt);
return w;
}
@ -203,30 +201,21 @@ bricks.LlmModel = class extends bricks.JsWidget {
} else {
d = objcopy(data);
}
var fmt = this.opts.user_message_format;
if (fmt){
var umsg = bricks.apply_data(fmt, inputdata2dic(data));
this.messages.push(umsg);
}
if (data instanceof FormData){
d.append('model', this.opts.model)
d.append('llmid', this.opts.llmid)
d.append('messages', JSON.stringify(this.messages));
} else {
d.messages = JSON.stringify(this.messages);
d.model = this.opts.model;
d.llmid = this.opts.llmid;
}
return d;
}
async model_inputed(data){
if (!opts.use_session){
this.messages = [];
}
var mout = new bricks.ModelOutput({
textvoice:this.textvoice,
tts_url:this.tts_url,
icon:this.opts.icon,
response_mode: this.opts.response_mode,
model:this.opts.model,
modelname:this.opts.modelname,
estimate_url:this.llmio.estimate_url,
@ -234,26 +223,17 @@ bricks.LlmModel = class extends bricks.JsWidget {
this.llmio.o_w.add_widget(mout);
if (this.response_mode == 'stream' || this.response_mode == 'async') {
var d = this.inputdata2uploaddata(data);
console.log('data_inouted=', data, 'upload_data=', d);
var hr = new bricks.HttpResponseStream();
var resp = await hr.post(this.opts.url, {params:d});
await hr.handle_chunk(resp, this.chunk_response.bind(this, mout));
this.chunk_ended();
} else {
var d = this.inputdata2uploaddata(data);
console.log('data_inouted=', data, 'upload_data=', d);
var hj = new bricks.HttpJson()
var resp = await hj.post(this.opts.url, {params:d});
if (this.response_mode == 'sync'){
resp.content = bricks.escapeSpecialChars(resp.content)
mout.update_data(resp);
if (this.messages){
var msg = this.llm_msg_format();
var lmsg = bricks.apply_data(msg, resp);
this.messages.push(lmsg)
}
} else {
;
}
}
}
is_accept_source(source){
@ -266,25 +246,24 @@ bricks.LlmModel = class extends bricks.JsWidget {
return this.llm_message_format || {role:'assistant', content:"${content}"}
}
chunk_response(mout, l){
l = l.trim();
try {
var d = JSON.parse(l);
if (! d.content || d.content == ''){
} catch(e){
console.log(l, 'is not a json data');
return
}
if (this.opts.response_mode == 'async'){
if(d.status != 'SUCCEEDED'){
console.log('filter all message not successed');
return;
}
d.content = bricks.escapeSpecialChars(d.content);
this.resp_data = d;
}
console.log('l=', l, 'd=', d);
mout.update_data(d);
// console.log('stream data=', d);
}
chunk_ended(){
if (! this.messages) {
console.log('this.messages is null !!!!!!!!!');
return;
}
var msg = this.llm_msg_format();
var txt = bricks.escapeSpecialChars(this.resp_data.content)
this.resp_data.content = txt;
var lmsg = bricks.apply_data(msg, this.resp_data);
this.messages.push(lmsg);
console.log('chunk end');
}
}
bricks.LlmIO = class extends bricks.VBox {
@ -378,8 +357,8 @@ bricks.LlmIO = class extends bricks.VBox {
var sopts = {
data_url:this.list_models_url,
data_params:{
mii:this.models[0].modelinstanceid,
mti:this.models[0].modeltypeid
llmid:this.models[0].llmid,
llmcatelogid:this.models[0].llmcatelogid
},
data_method:'POST',
col_cwidth: 24,
@ -391,10 +370,24 @@ bricks.LlmIO = class extends bricks.VBox {
},
subwidgets:[
{
widgettype:"Title4",
widgettype: "HBox",
options: {
cheight: 2
},
subwidgets: [
{
widgettype:"Svg",
options: {
url:"/appbase/show_icon.dspy?id=${iconid}"
}
},
{
widgettype:"Title6",
options:{
text:"${name}"
}
}
]
},
{
widgettype:"Filler",
@ -437,8 +430,9 @@ bricks.LlmIO = class extends bricks.VBox {
async add_new_model(event){
event.preventDefault();
event.stopPropagation();
this.models.push(event.params);
this.show_added_model(event.params);
var llm = event.params;
this.models.push(llm);
this.show_added_model(llm);
}
async open_input_widget(event){
event.preventDefault();
@ -471,10 +465,7 @@ bricks.LlmIO = class extends bricks.VBox {
await this.show_input(params);
for(var i=0;i<this.llmmodels.length;i++){
var lm = this.llmmodels[i];
if (lm.is_accept_source('userinput')){
schedule_once(lm.model_inputed.bind(lm, params), 0.01);
// await lm.model_inputed(params);
}
};
}
async show_input(params){
@ -485,7 +476,7 @@ bricks.LlmIO = class extends bricks.VBox {
w.set_css(this.msg_css||'user_msg');
w.set_css('filler');
var img = new bricks.Svg({rate:2,url:this.user_icon||bricks_resource('imgs/chat-user.svg')});
box.add_widget(new bricks.BlankIcon({rate:2, flexShrink:0}));
// box.add_widget(new bricks.BlankIcon({rate:2, flexShrink:0}));
box.add_widget(w);
box.add_widget(img);
this.o_w.add_widget(box);

View File

@ -23,21 +23,24 @@ bricks.Menu = class extends bricks.VBox {
return cp;
}
async menu_clicked(event){
console.log(event);
let e = event.target;
let opts = event.params;
var t;
var popts;
if (this.target == 'PopupWindow'){
var target = opts.target || this.target;
var popup_options = opts.popup_options || this.popup_options;
if (target == 'PopupWindow'){
popts = bricks.get_popupwindow_default_options();
bricks.extend(popts, this.popup_options || {});
bricks.extend(popts, popup_options || {});
popts.icon = opts.icon;
popts.title = opts.label || opts.name
t = new bricks.PopupWindow(popts);
} else if (this.target == 'Popup'){
popts = bricks.get_popup_default_options();
bricks.extend(popts, this.popup_options || {});
bricks.extend(popts, popup_options || {});
t = new bricks.Popup(popts);
} else {
t = bricks.getWidgetById(this.target);
t = bricks.getWidgetById(target);
}
if (t){
var desc = {
@ -64,18 +67,41 @@ bricks.Menu = class extends bricks.VBox {
let subw = this.create_menuitem(item);
if (item.hasOwnProperty('items')){
var itw = new bricks.VBox({});
w.add_widget(itw);
let w1 = this.create_submenu_container();
itw.add_widget(subw);
itw.add_widget(w1);
this.create_children(w1, item.items);
subw.bind('click', this.items_toggle_hide.bind(this, w1));
w.add_widget(itw);
} else if(item.submenu){
var itw = new bricks.VBox({});
let w1 = this.create_submenu_container();
w1.submenu_loaded = false;
w1.submenu_url = item.submenu;
itw.add_widget(subw);
itw.add_widget(w1);
w.add_widget(itw);
subw.bind('click', this.load_submenu.bind(this, w1));
} else {
subw.bind('click', this.regen_menuitem_event.bind(this, item))
w.add_widget(subw);
}
}
}
async get_submenu_items(url){
var jc = new bricks.HttpJson();
var d = await jc.get(url);
return d.options.items;
}
async load_submenu(container, event){
event.stopPropagation();
if (! container.submenu_loaded){
var items = await this.get_submenu_items(container.submenu_url);
this.create_children(container, items);
container.submenu_loaded = true;
}
container.toggle_hide();
}
items_toggle_hide(w, event){
w.toggle_hide();
event.stopPropagation();

View File

@ -105,7 +105,7 @@ bricks.QAFrame = class extends bricks.VBox {
console.log('show_courseware(), d=', d);
switch(d.type){
case 'video':
w = new bricks.VideoPlayer({
w = new bricks.Video({
width:'100%',
height:'100%',
url:d.url,

View File

@ -24,7 +24,7 @@ bricks.HScrollPanel = class extends bricks.HBox {
constructor(opts){
opts.width = '100%';
opts.height = '100%';
opts.css = 'scrollpanel';
opts.css = opts.css + ' scrollpanel';
opts.overflow = 'auto';
super(opts);
this.min_threshold = opts.min_threshold || 0.01;
@ -66,7 +66,7 @@ bricks.VScrollPanel = class extends bricks.VBox {
constructor(opts){
opts.width = '100%';
opts.height = '100%';
opts.css = 'scrollpanel';
opts.css = opts.css + ' scrollpanel';
opts.overflow = 'auto';
super(opts);
this.min_threshold = opts.min_threshold || 0.02;

View File

@ -289,17 +289,29 @@ bricks.Tree = class extends bricks.VScrollPanel {
this.update_node();
break;
default:
if ((opts.selected_data || opts.checked_data) && ! this.selected_node){
if (opts.selected_data && ! this.selected_node){
var w = new bricks.Error({title:'Error', message:'No selected node found'});
w.open();
return;
}
console.log('opts=', opts);
var d = null;
if (opts.checked_data){
d = this.checked_data
} else if (opts.selected_data){
d = this.selected_node.user_data
if (opts.checked_data && this.checked_data.length == 0){
var w = new bricks.Error({title:'Error', message:'No checked node found'});
w.open();
return;
}
var d = {};
if (opts.selected_data){
d = this.selected_node.user_data;
} else if (opts.checked_data){
d = this.checked_data;
} else {
if (this.selected_node){
d = this.selected_node.user_data;
} else if (this.checked_data.length>0) {
d = this.checked_data[0];
} else {
d = this.opts.params;
}
}
d.meta_data = {
referer: this.id,

View File

@ -1,6 +1,55 @@
var bricks = window.bricks || {};
bricks.bug = false;
/*
We use ResizeObserver to implements dom object resize event
*/
bricks.resize_observer = new ResizeObserver(entries => {
for (let entry of entries){
const cr = entry.contentRect;
const ele = entry.target;
const w = ele.bricks_widget;
// console.log('size=', cr, 'element=', ele, w);
if (w){
w.dispatch('element_resize', cr);
}
}
});
/* MutationObserver for add to DOM or remove from DOM
event:
domon: add to dom
domoff: remove from dom
*/
bricks.dom_on_off_observer=new MutationObserver((mutations)=>{
function handleRemoved(node) {
if (node.nodeType !== 1) return; // 只处理元素节点
if (node.bricks_widget) {
console.log('**** widget removed', node.bricks_widget);
node.bricks_widget.dispatch('domoff');
}
// 遍历后代
for (let child of node.querySelectorAll('*')) {
if (child.bricks_widget) {
console.log('**** widget removed (descendant)', child.bricks_widget);
child.bricks_widget.dispatch('domoff');
}
}
}
for (let m of mutations) {
for (let n of m.removedNodes) {
handleRemoved(n);
}
for (let n of m.addedNodes) {
if (n.bricks_widget){
var w = n.bricks_widget;
w.dispatch('domon');
}
}
}
});
bricks.dom_on_off_observer.observe(document.body,
{ childList: true, subtree: true });
function addParamsToUrl(url, params, widget) {
const urlObj = new URL(url, window.baseURI); // 处理相对和绝对路径
Object.keys(params).forEach(key => {
@ -665,6 +714,30 @@ function blobToBase64(blob) {
});
}
/*
opts = {
css:
id
}
*/
bricks.dom_create = function(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 = function(html){
var e = document.createElement('div');
e.outerHTML = html;
return e;
}
/*
// 使用队列
const queue = new Queue();
queue.enqueue(1);

322
bricks/videoplayer.js Normal file
View File

@ -0,0 +1,322 @@
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 = `<video id="video" class="video-element"></video>
<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.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.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.bind('domon', this.init.bind(this));
this.bind('domoff', this.destroy.bind(this));
}
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', () => {
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')}`;
}
}
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);

View File

@ -1,18 +1,4 @@
var bricks = window.bricks || {};
/*
We use ResizeObserver to implements dom object resize event
*/
bricks.resize_observer = new ResizeObserver(entries => {
for (let entry of entries){
const cr = entry.contentRect;
const ele = entry.target;
const w = ele.bricks_widget;
// console.log('size=', cr, 'element=', ele, w);
if (w){
w.dispatch('element_resize', cr);
}
}
});
bricks.JsWidget = class {
/*
@ -255,20 +241,20 @@ bricks.JsWidget = class {
this.dom_element.style[k] = v;
}
set_csses(csses, remove_flg){
var arr = csses.split(' ');
arr.forEach(c =>{
this.set_css(c, remove_flg);
})
this.set_css(csses, remove_flg);
}
unset_css(css){
this.dom_element.classList.remove(css);
}
set_css(css, remove_flg){
var arr = css.split(' ');
arr.forEach(c => {
if (!remove_flg){
this.dom_element.classList.add(css);
this.dom_element.classList.add(c);
} else {
this.dom_element.classList.remove(css);
this.dom_element.classList.remove(c);
}
});
}
set_cssObject(cssobj){
bricks.extend(this.dom_element.style, cssobj);

View File

@ -14,12 +14,61 @@ bricks.Wterm = class extends bricks.JsWidget {
this.socket = null;
this.ping_timeout = opts.ping_timeout || 19;
schedule_once(this.open.bind(this), 1);
this.bind('domon', this.send_term_size.bind(this));
this.bind('domoff', this.destroy.bind(this));
}
close_websocket(){
try {
console.log('socket alive, destroy it');
// this.socket.send(JSON.stringify({ type: "close"}));
this.socket.close(1000,'close now');
this.socket.onopen = null;
this.socket.onmessage = null;
this.socket.onerror = null;
this.socket.onclose = null;
this.socket = null;
} catch(e) {
this.socket = null;
console.log('e=', e);
}
}
close_terminal(){
try {
this.fitAddon.dispose();
this.term.dispose();
this.term = null;
} catch(e){
this.term = null;
console.log('e=', e);
}
}
destroy(){
console.debug('------domoff event, destory this widget');
try {
if (this.heartbeat_task){
this.heartbeat_task.cancel();
this.heartbeat_task = null;
}
this.unbind('element_resize', this.term_resize.bind(this))
} catch(e) {
console.log('error ', e);
}
console.debug('---1--domoff event, destory this widget');
if (this.socket){
this.close_websocket();
}
console.debug('---2--domoff event, destory this widget');
if (this.term){
this.close_terminal();
}
console.debug('---3--domoff event, destory this widget');
}
charsize_sizing(){
var cs = bricks.app.charsize;
this.term.setOption('fontSize', cs);
}
send_term_size(){
console.debug('------domon event, send the terminal size to server');
try {
console.log('resize():rows=', this.term.rows, this.term.cols);
this.socket.send(JSON.stringify({ type: "resize",
@ -44,10 +93,6 @@ bricks.Wterm = class extends bricks.JsWidget {
this.heartbeat_task = schedule_once(this.heartbeat.bind(this),
this.ping_timeout);
}
async close(){
this.term.close();
this.socket.close();
}
async open(){
var term_options = bricks.extend({width: "100%", height: "100%"}, this.term_options);
var term = new Terminal(term_options);
@ -93,6 +138,7 @@ bricks.Wterm = class extends bricks.JsWidget {
term.onResize(({cols, rows}) =>{
this.send_term_size();
});
this.send_term_size();
term.focus();
}
term_resize(){