torrent-online 1.5.8 → 1.6.0
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 +63 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
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
|
@@ -776,10 +776,24 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
776
776
|
if (s.codec_type === 'audio') tracks.push({ a: ai++, lang, title, codec: s.codec_name });
|
|
777
777
|
else if (s.codec_type === 'subtitle') subs.push({ s: si++, lang, title, codec: s.codec_name });
|
|
778
778
|
}
|
|
779
|
-
|
|
779
|
+
const vs = (j.streams || []).find(s => s.codec_type === 'video');
|
|
780
|
+
let vc = '';
|
|
781
|
+
if (vs) {
|
|
782
|
+
if (vs.codec_name === 'h264') {
|
|
783
|
+
const P = { Baseline: '42', Main: '4D', Extended: '58', High: '64', 'High 10': '6E', 'High 4:2:2': '76', 'High 4:4:4': 'F4' };
|
|
784
|
+
const idc = P[vs.profile] || '64';
|
|
785
|
+
const lvl = Number.isFinite(vs.level) ? ('0' + Math.round(vs.level / 10).toString(16)).slice(-2) : '28';
|
|
786
|
+
vc = 'avc1.' + idc + '00' + lvl;
|
|
787
|
+
} else if (vs.codec_name === 'hevc') {
|
|
788
|
+
vc = Number.isFinite(vs.level) ? 'hvc1.1.6.L' + Math.round(vs.level / 10) + '.B0' : 'hvc1.1.6.L120.B0';
|
|
789
|
+
} else if (vs.codec_name === 'av1') vc = 'av01.0.04M.08';
|
|
790
|
+
else if (vs.codec_name === 'vp9') vc = 'vp09.00.10.08';
|
|
791
|
+
}
|
|
792
|
+
return json(200, { ffmpeg: true, tracks, subs, vc });
|
|
780
793
|
} catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], retry: true }); }
|
|
781
794
|
}
|
|
782
795
|
const mR = /^\/remux\/(\d+)\/(\d+)$/.exec(p);
|
|
796
|
+
if (mR && req.method === 'HEAD') { res.statusCode = 200; res.setHeader('Content-Type', 'video/mp4'); res.end(); return; }
|
|
783
797
|
if (mR) {
|
|
784
798
|
const e = st.torrents.get(Number(mR[1]));
|
|
785
799
|
const f = e?.t.files[Number(mR[2])];
|
|
@@ -797,7 +811,26 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
797
811
|
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
798
812
|
let ffErr = 0;
|
|
799
813
|
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('ffm:', String(d).trim().slice(0, 200)); ffErr++; } });
|
|
800
|
-
|
|
814
|
+
// пребуфер: копим ~2 МБ выхода ffmpeg, чтобы браузер стартанул с запасом, а не по кусочкам
|
|
815
|
+
const PRE = 2 * 1048576;
|
|
816
|
+
const chunks = [];
|
|
817
|
+
let buffered = 0, piping = false;
|
|
818
|
+
const startPipe = () => {
|
|
819
|
+
if (piping) return;
|
|
820
|
+
piping = true;
|
|
821
|
+
proc.stdout.removeListener('data', onData);
|
|
822
|
+
for (const c of chunks) res.write(c);
|
|
823
|
+
chunks.length = 0;
|
|
824
|
+
proc.stdout.pipe(res);
|
|
825
|
+
};
|
|
826
|
+
const onData = c => {
|
|
827
|
+
if (piping) return;
|
|
828
|
+
chunks.push(c); buffered += c.length;
|
|
829
|
+
if (buffered >= PRE) startPipe();
|
|
830
|
+
};
|
|
831
|
+
proc.stdout.on('data', onData);
|
|
832
|
+
proc.stdout.on('end', () => { if (!piping) { for (const c of chunks) res.write(c); chunks.length = 0; } try { res.end(); } catch {} });
|
|
833
|
+
setTimeout(startPipe, 5000);
|
|
801
834
|
proc.on('error', () => { try { res.end(); } catch {} });
|
|
802
835
|
req.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
803
836
|
res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
@@ -809,31 +842,48 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
809
842
|
const f = e?.t.files[Number(mV[2])];
|
|
810
843
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
811
844
|
ensureSel(e, Number(mV[2]));
|
|
845
|
+
const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
|
|
812
846
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
813
847
|
res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
|
|
814
848
|
'<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
|
|
815
849
|
'.bar{display:flex;gap:10px;align-items:center;padding:6px 10px;flex-wrap:wrap}' +
|
|
816
850
|
'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
|
|
817
|
-
'video{flex:1;width:100%;min-height:0;background:#000}
|
|
851
|
+
'video{flex:1;width:100%;min-height:0;background:#000}' +
|
|
852
|
+
'#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>' +
|
|
818
853
|
'<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>' +
|
|
819
|
-
'<video id=v controls autoplay
|
|
820
|
-
'<
|
|
821
|
-
'var
|
|
822
|
-
'var
|
|
854
|
+
'<video id=v controls autoplay playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
855
|
+
'<div id=ov>▶</div>' +
|
|
856
|
+
'<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ',MF=' + (webFriendly ? 1 : 0) + ';' +
|
|
857
|
+
'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n"),ov=document.getElementById("ov");' +
|
|
858
|
+
'ov.onclick=function(){v.play().catch(function(){});ov.style.display="none"};' +
|
|
859
|
+
'var tries=0,ms=null,VC="";' +
|
|
860
|
+
'var MSEOK=function(){return ("MediaSource" in window)&&VC&&MediaSource.isTypeSupported("video/mp4;codecs=\'" + VC + ",mp4a.40.2\'")};' +
|
|
861
|
+
'var stopMse=function(){if(ms){try{ms.endOfStream()}catch(e){}ms=null}};' +
|
|
862
|
+
'var mse=function(u){stopMse();var m=new MediaSource();v.src=URL.createObjectURL(m);' +
|
|
863
|
+
'm.addEventListener("sourceopen",function(){var sb=m.addSourceBuffer("video/mp4;codecs=\'" + VC + ",mp4a.40.2\'");sb.mode="sequence";var q=[];' +
|
|
864
|
+
'sb.addEventListener("updateend",function(){if(q.length){try{sb.appendBuffer(q.shift())}catch(e){}}});' +
|
|
865
|
+
'fetch(u).then(function(r){var rd=r.body.getReader();var pump=function(){rd.read().then(function(res){' +
|
|
866
|
+
'if(res.done){try{if(!sb.updating&&!q.length)m.endOfStream()}catch(e){}return;}' +
|
|
867
|
+
'q.push(res.value);if(q.length<400&&!sb.updating){try{sb.appendBuffer(q.shift())}catch(e){}}pump()}).catch(function(){})};pump();});' +
|
|
868
|
+
'v.play().catch(function(){ov.style.display="flex"})})};' +
|
|
823
869
|
'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
|
|
824
870
|
'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
|
|
825
871
|
'if(d.error)n.textContent="⚠ "+d.error;' +
|
|
826
|
-
'var A=d.tracks||[],S=d.subs||[];' +
|
|
827
|
-
'if(!d.ffmpeg
|
|
872
|
+
'var A=d.tracks||[],S=d.subs||[];VC=d.vc||"";' +
|
|
873
|
+
'if(!d.ffmpeg){return;}' +
|
|
874
|
+
'if(A.length<2&&!S.length&&MF){return;}' +
|
|
828
875
|
'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)});' +
|
|
829
|
-
'aS.value=A[0].a;aS.style.display=""
|
|
876
|
+
'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
|
|
830
877
|
'if(S.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
|
|
831
878
|
'S.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.style.display=""}' +
|
|
832
|
-
'var go=function(){var t=v.currentTime;
|
|
879
|
+
'var go=function(){var t=v.currentTime;var s=sS.value;' +
|
|
880
|
+
'var u=B+"/remux/"+ID+"/"+I+"?a="+(aS.style.display!==""?aS.value:0)+(s!==""?"&s="+s:"");' +
|
|
881
|
+
'if(s===""&&MSEOK()){mse(u)}else{stopMse();v.src=u;v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play().catch(function(){ov.style.display="flex"})}};' +
|
|
833
882
|
'aS.onchange=go;sS.onchange=go;go();' +
|
|
834
|
-
'
|
|
883
|
+
'}).catch(function(){if(tries++<15)setTimeout(load,4000)})};' +
|
|
884
|
+
'v.addEventListener("playing",function(){n.textContent="";ov.style.display="none"});' +
|
|
835
885
|
'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);' +
|
|
836
|
-
'
|
|
886
|
+
'load();</script>');
|
|
837
887
|
return;
|
|
838
888
|
}
|
|
839
889
|
res.statusCode = 404; res.end('not found');
|