torrent-online 1.6.3 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/wtui.js +62 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Stream .torrent / magnet to VLC or browser — torrent streaming for macOS/Linux without a torrent client. Стрим торрентов в VLC или браузер.",
|
|
6
6
|
"keywords": [
|
package/wtui.js
CHANGED
|
@@ -851,11 +851,20 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
851
851
|
const name = mH[3];
|
|
852
852
|
if (name.includes('..')) { res.statusCode = 400; res.end('bad'); return; }
|
|
853
853
|
const a = u.searchParams.get('a') || '0';
|
|
854
|
-
const
|
|
855
|
-
const
|
|
854
|
+
const s1 = u.searchParams.get('si');
|
|
855
|
+
const si = (s1 === null || s1 === '' || !Number.isFinite(Number(s1))) ? -1 : Math.max(0, Math.min(7, Number(s1)));
|
|
856
|
+
const key = mH[1] + '-' + mH[2] + '-' + a + '-' + (si >= 0 ? 's' + si : 'n');
|
|
856
857
|
const dir = path.join(HLS_ROOT, key);
|
|
857
858
|
const fp = path.join(dir, name);
|
|
858
859
|
const hit0 = hlsProcs.get(key); if (hit0) hit0.touched = Date.now();
|
|
860
|
+
const hit1 = hlsProcs.get(key + '-vtt'); if (hit1) hit1.touched = Date.now();
|
|
861
|
+
if (name === 'sub.vtt') {
|
|
862
|
+
let b = null;
|
|
863
|
+
try { b = fs.readFileSync(fp); } catch {}
|
|
864
|
+
res.setHeader('Content-Type', 'text/vtt; charset=utf-8');
|
|
865
|
+
res.end(b || 'WEBVTT\n\n');
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
859
868
|
if (name !== 'master.m3u8') {
|
|
860
869
|
let b = null;
|
|
861
870
|
try { b = fs.readFileSync(fp); } catch {}
|
|
@@ -868,17 +877,20 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
868
877
|
for (const [k, h] of hlsProcs) if (k.startsWith(mH[1] + '-' + mH[2] + '-')) { try { h.proc.kill('SIGKILL'); } catch {} hlsProcs.delete(k); }
|
|
869
878
|
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mH[1] + '/' + mH[2];
|
|
870
879
|
const args = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k'];
|
|
871
|
-
|
|
872
|
-
if (si.length) args.push('-c:s', 'webvtt');
|
|
873
|
-
const vsm = ['v:0', 'a:0']; for (let i = 0; i < si.length; i++) vsm.push('s:' + i);
|
|
874
|
-
const segTpl = si.length ? 'seg-%s-%d.ts' : 'seg-%d.ts';
|
|
875
|
-
const plTpl = si.length ? 'pl-%s.m3u8' : 'pl.m3u8';
|
|
876
|
-
args.push('-f', 'hls', '-hls_time', '4', '-hls_list_size', '0', '-hls_playlist_type', 'event', '-hls_flags', 'independent_segments', '-hls_segment_type', 'mpegts', '-master_pl_name', 'master.m3u8', '-var_stream_map', vsm.join(','), '-hls_segment_filename', dir + '/' + segTpl, dir + '/' + plTpl);
|
|
880
|
+
args.push('-f', 'hls', '-hls_time', '4', '-hls_list_size', '0', '-hls_playlist_type', 'event', '-hls_flags', 'independent_segments', '-hls_segment_type', 'mpegts', '-master_pl_name', 'master.m3u8', '-var_stream_map', 'v:0,a:0', '-hls_segment_filename', dir + '/seg-%d.ts', dir + '/pl.m3u8');
|
|
877
881
|
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
878
882
|
let ffErr = 0;
|
|
879
883
|
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('hls:', String(d).trim().slice(0, 200)); ffErr++; } });
|
|
880
884
|
proc.on('exit', () => { const cur = hlsProcs.get(key); if (cur && cur.proc === proc) hlsProcs.delete(key); });
|
|
881
885
|
hlsProcs.set(key, { proc, dir, touched: Date.now() });
|
|
886
|
+
if (si >= 0) {
|
|
887
|
+
const subArgs = ['-hide_banner', '-loglevel', 'error', '-rw_timeout', '30000000', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '5', '-i', url, '-map', '0:s:' + si, '-c:s', 'webvtt', '-f', 'webvtt', dir + '/sub.vtt'];
|
|
888
|
+
const sp = spawn(ffmpeg, subArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
889
|
+
let sf = 0;
|
|
890
|
+
sp.stderr.on('data', d => { if (sf < 2) { console.error('vtt:', String(d).trim().slice(0, 150)); sf++; } });
|
|
891
|
+
sp.on('exit', () => { const cur = hlsProcs.get(key + '-vtt'); if (cur && cur.proc === sp) hlsProcs.delete(key + '-vtt'); });
|
|
892
|
+
hlsProcs.set(key + '-vtt', { proc: sp, dir, touched: Date.now() });
|
|
893
|
+
}
|
|
882
894
|
}
|
|
883
895
|
for (let i = 0; i < 60; i++) {
|
|
884
896
|
let b = null;
|
|
@@ -897,43 +909,48 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
897
909
|
ensureSel(e, Number(mV[2]));
|
|
898
910
|
const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
|
|
899
911
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
912
|
+
res.end(`<!doctype html><meta charset=utf-8><title>${esc(f.name)}</title>
|
|
913
|
+
<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}
|
|
914
|
+
.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}
|
|
915
|
+
select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}
|
|
916
|
+
video{flex:1;width:100%;min-height:0;background:#000}
|
|
917
|
+
#ov{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.45);cursor:pointer;font-size:72px;color:#fff}</style>
|
|
918
|
+
<div class=bar><b>${esc(f.name)}</b><select id=a style=display:none></select><select id=s style=display:none></select><span id=n></span></div>
|
|
919
|
+
<video id=v controls autoplay playsinline src="${base}/s/${mV[1]}/${mV[2]}"></video>
|
|
920
|
+
<div id=ov>▶</div>
|
|
921
|
+
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
|
|
922
|
+
<script>
|
|
923
|
+
var B=${JSON.stringify(base)},ID=${mV[1]},I=${mV[2]};
|
|
924
|
+
var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");
|
|
925
|
+
ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};
|
|
926
|
+
var tries=0,hls=null,cues=[],subUrl="";
|
|
927
|
+
var cueDiv=document.createElement("div");cueDiv.style.cssText="position:fixed;left:5%;right:5%;bottom:8%;text-align:center;color:#fff;font-size:clamp(14px,2.5vw,28px);text-shadow:0 1px 4px #000;pointer-events:none;z-index:5";document.body.appendChild(cueDiv);
|
|
928
|
+
var parseVtt=function(txt){var NL=String.fromCharCode(10);var C=[];txt.split(NL+NL).forEach(function(b){var m=b.match(/([0-9]+):([0-9]+):([0-9]+).([0-9]+) --> ([0-9]+):([0-9]+):([0-9]+).([0-9]+)/);if(!m)return;var t=function(h,mi,s,ms){return h*3600+mi*60+s+parseFloat("0."+ms)};var x=b.split(NL).filter(function(l){return l&&l.indexOf("-->")<0&&l.indexOf("WEBVTT")<0&&!/^[a-zA-Z-]+:/.test(l)}).join("<br>");C.push([t(+m[1],+m[2],+m[3],m[4]),t(+m[5],+m[6],+m[7],m[8]),x])});cues=C};
|
|
929
|
+
var pollSub=function(){if(!subUrl)return;fetch(subUrl).then(function(r){return r.text()}).then(parseVtt).catch(function(){})};
|
|
930
|
+
v.addEventListener("timeupdate",function(){var t=v.currentTime,a="";for(var i=cues.length-1;i>=0;i--){if(t>=cues[i][0]&&t<=cues[i][1]){a=cues[i][2];break}}cueDiv.innerHTML=a});
|
|
931
|
+
setInterval(pollSub,20000);
|
|
932
|
+
var play=function(u,seek){if(hls){try{hls.destroy()}catch(e){}hls=null}
|
|
933
|
+
if(window.Hls&&Hls.isSupported()){hls=new Hls({startPosition:0,enableWorker:true,lowLatencyMode:false});hls.loadSource(u);hls.attachMedia(v);
|
|
934
|
+
hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){ov.style.display="flex"});if(seek>1){var iv=setInterval(function(){if(v.duration>seek+3){v.currentTime=seek;clearInterval(iv)}},1000);setTimeout(function(){clearInterval(iv)},120000)}});
|
|
935
|
+
hls.on(Hls.Events.ERROR,function(ev,d){if(d.fatal)n.textContent="⚠ HLS: "+d.type})}
|
|
936
|
+
else{v.src=u;v.play().catch(function(){ov.style.display="flex"})}};
|
|
937
|
+
var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){
|
|
938
|
+
if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}
|
|
939
|
+
if(d.error)n.textContent="⚠ "+d.error;
|
|
940
|
+
var A=d.tracks||[],S=d.subs||[];
|
|
941
|
+
if(!d.ffmpeg){return;}
|
|
942
|
+
var SI=S.filter(function(t){return ["subrip","ass","ssa","srt","mov_text","webvtt","text"].indexOf(t.codec)>=0}).map(function(t){return t.s});
|
|
943
|
+
A.forEach(function(t){var o=document.createElement("option");o.value=t.a;o.textContent="♪ "+(t.lang&&t.lang!=="und"?t.lang:"дорожка "+(t.a+1))+(t.title?" — "+t.title:"")+" ["+t.codec+"]";aS.appendChild(o)});
|
|
944
|
+
if(A.length>1){aS.value=A[0].a;aS.style.display=""}
|
|
945
|
+
if(SI.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);
|
|
946
|
+
S.filter(function(t){return SI.indexOf(t.s)>=0}).forEach(function(t){var o=document.createElement("option");o.value=t.s;o.textContent="💬 "+(t.lang&&t.lang!=="und"?t.lang:"субтитры "+(t.s+1))+(t.title?" — "+t.title:"");sS.appendChild(o)});sS.value="";sS.style.display=""}
|
|
947
|
+
var go=function(){var a=aS.style.display!==""?aS.value:0;var s=sS.style.display!==""?sS.value:"";var t=v.currentTime>1?v.currentTime:0;cues=[];cueDiv.innerHTML="";subUrl=s!==""?B+"/hls/"+ID+"/"+I+"/sub.vtt?a="+a+"&si="+s:"";if(subUrl)pollSub();play(B+"/hls/"+ID+"/"+I+"/master.m3u8?a="+a+(s!==""?"&si="+s:""),t)};
|
|
948
|
+
aS.onchange=go;sS.onchange=go;go();
|
|
949
|
+
}).catch(function(){if(tries++<15)setTimeout(load,4000)})};
|
|
950
|
+
v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});
|
|
951
|
+
setInterval(function(){fetch(B+"/api/status").then(function(r){return r.json()}).then(function(l){var t=l.find(function(x){return x.id===ID});if(t&&v.readyState<2)n.textContent="⏬ "+t.speed+" · пиры "+t.peers+(t.progress>0?" · "+t.progress+"%":"")}).catch(function(){})},2000);
|
|
952
|
+
load();
|
|
953
|
+
</script>`);
|
|
937
954
|
return;
|
|
938
955
|
}
|
|
939
956
|
res.statusCode = 404; res.end('not found');
|