Buffer bounded recording segments for stable browser playback

This commit is contained in:
Codex
2026-10-06 23:00:28 +08:00
parent 12556bf10f
commit f7d3fea92f
12 changed files with 187 additions and 26 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ const RecordingRanges=(()=>{
.filter(s=>Number.isFinite(s.start)&&Number.isFinite(s.end)&&s.end>s.start).sort((a,b)=>a.start-b.start);
const result=[];for(const r of ranges){const last=result.at(-1);if(last&&r.start<=last.end)last.end=Math.max(last.end,r.end);else result.push({...r});}return result;
}
function at(ranges,time){const t=+time,s=ranges.find(s=>t>=s.start&&t<s.end);return s?{start:t,end:Math.min(s.end,t+3600000)}:null;}
function at(ranges,time){const t=+time,s=ranges.find(s=>t>=s.start&&t<s.end);return s?{start:t,end:Math.min(s.end,t+300000)}:null;}
function next(ranges,end){const s=ranges.find(s=>s.end>+end);return s?at(ranges,Math.max(s.start,+end)):null;}
function clip(ranges,start,end){
const a=+start,b=+end;
+14 -4
View File
@@ -1,5 +1,5 @@
'use strict';
let playbackRange=null;
let playbackRange=null,playbackPausedAt=0;
function playbackSourceHint(){renderRecordingStatus();if($('#archive-source').value==='recorder')$('#play-info').textContent='录像机原有录像待接入;查看本系统保存的录像请选择“小电脑本机录像”。';}
function renderRecordingStatus(){
@@ -15,7 +15,7 @@ function renderRecordingStatus(){
function localDateTime(value){const d=new Date(value);return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${d.toTimeString().slice(0,8)}`;}
function resetPlayback(){
playbackGeneration++;playbackRange=null;const v=$('#play-video');v.pause();v.removeAttribute('src');v.load();
playbackGeneration++;playbackRange=null;playbackPausedAt=0;const v=$('#play-video');v.pause();v.removeAttribute('src');v.load();
$('#play-empty').hidden=false;$('#download').hidden=true;$('#clip-download').hidden=true;$('#clip-status').textContent='';$('#clip-form').reset();
spans=[];queryCamera='';queryStart=null;queryEnd=null;$('#play-info').textContent='选择日期,查看实际录像时段';$('#play-position').textContent='';drawTimeline();$('#recording-list').innerHTML='';
}
@@ -42,8 +42,8 @@ function drawTimeline(){
function playbackParams(range){return new URLSearchParams({camera:queryCamera,start:new Date(range.start).toISOString(),duration:String((range.end-range.start)/1000)});}
function playAt(date,preserveClip=false){
const range=RecordingRanges.at(spans,date);if(!range){toast('这个时间没有录像,请选择绿色时段');return;}
playbackRange=range;const params=playbackParams(range),v=$('#play-video');v.src='/media/playback?'+params;v.play().catch(()=>{});
$('#play-info').textContent='回放起点 '+new Date(range.start).toLocaleString()+' · 每次最多一小时,结束后按续播选项处理';$('#play-empty').hidden=true;
playbackPausedAt=0;playbackRange=range;const params=playbackParams(range),v=$('#play-video');v.src='/media/playback?'+params;v.play().catch(()=>{});
$('#play-info').textContent='回放起点 '+new Date(range.start).toLocaleString()+' · 每段最多 5 分钟,按续播选项连续播放';$('#play-empty').hidden=true;
$('#download').href='/media/playback?'+params+'&download=1';$('#download').setAttribute('download','recording.mp4');$('#download').hidden=false;
if(!preserveClip){$('#clip-start').value=localDateTime(range.start);$('#clip-end').value=localDateTime(range.end);$('#clip-download').hidden=true;$('#clip-status').textContent='可修改起止时间后生成下载链接';}
}
@@ -55,6 +55,15 @@ function continuePlayback(){
if(!next){$('#play-info').textContent='所选日期的已查询录像播放完毕';return;}
playAt(new Date(next.start),true);if(next.start>end)$('#play-info').textContent=`已跳过无录像时段,续播 ${new Date(next.start).toLocaleString()}`;
}
function pausePlayback(){
const v=$('#play-video');if(playbackRange&&!v.ended)playbackPausedAt=Date.now();
}
function resumePlayback(){
const elapsed=playbackPausedAt?Date.now()-playbackPausedAt:0;playbackPausedAt=0;
if(elapsed<12000||!loggedIn||activePage!=='playback'||!playbackRange)return;
const time=playbackRange.start+$('#play-video').currentTime*1000;
if(time<playbackRange.end)playAt(new Date(time),true);
}
function makeClip(){
$('#clip-download').hidden=true;
try{if(!queryCamera)throw Error('请先查询录像');const range=RecordingRanges.clip(spans,new Date($('#clip-start').value),new Date($('#clip-end').value));
@@ -67,6 +76,7 @@ $('#seek-button').onclick=()=>{if(!queryCamera){toast('请先查询录像');retu
$('#timeline').onclick=e=>{if(!queryStart)return;const r=e.currentTarget.getBoundingClientRect();playAt(new Date(+queryStart+(queryEnd-queryStart)*(e.clientX-r.left)/r.width));};
$('#play-video').onerror=()=>{if($('#play-video').getAttribute('src'))$('#play-info').textContent='回放失败:请稍后重试,或下载原始片段;服务器最多同时转换两个回放。';};
$('#play-video').onplaying=()=>{$('#play-empty').hidden=true;};$('#play-video').onended=continuePlayback;
$('#play-video').onpause=pausePlayback;$('#play-video').onplay=resumePlayback;
$('#play-video').ontimeupdate=()=>{if(playbackRange)$('#play-position').textContent='录像时间 '+new Date(Math.min(playbackRange.end,playbackRange.start+$('#play-video').currentTime*1000)).toLocaleString();};
$('#clip-form').onsubmit=e=>{e.preventDefault();makeClip();};
for(const id of ['clip-start','clip-end'])$('#'+id).oninput=()=>{$('#clip-download').hidden=true;$('#clip-status').textContent='起止时间已修改,请重新生成链接';};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-BN-llATb.js"></script>
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-F3a-FbDz.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-B2d9n-y0.css">
</head><body><div id="app"></div></body></html>