torrent-online 1.5.7 → 1.5.9
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 +34 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "torrent-online",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
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
|
@@ -780,6 +780,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
780
780
|
} catch (e2) { return json(200, { ffmpeg: true, tracks: [], subs: [], retry: true }); }
|
|
781
781
|
}
|
|
782
782
|
const mR = /^\/remux\/(\d+)\/(\d+)$/.exec(p);
|
|
783
|
+
if (mR && req.method === 'HEAD') { res.statusCode = 200; res.setHeader('Content-Type', 'video/mp4'); res.end(); return; }
|
|
783
784
|
if (mR) {
|
|
784
785
|
const e = st.torrents.get(Number(mR[1]));
|
|
785
786
|
const f = e?.t.files[Number(mR[2])];
|
|
@@ -790,12 +791,33 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
790
791
|
const a = u.searchParams.get('a') || '0';
|
|
791
792
|
const sb = u.searchParams.get('s');
|
|
792
793
|
const url = 'http://127.0.0.1:' + st.port + base + '/s/' + mR[1] + '/' + mR[2];
|
|
793
|
-
const args = ['-hide_banner', '-loglevel', 'error', '-i', url, '-map', '0:v:0', '-map', '0:a:' + a, '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-max_muxing_queue_size', '9999'];
|
|
794
|
+
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', '-max_muxing_queue_size', '9999'];
|
|
794
795
|
if (sb !== null && sb !== '') args.push('-map', '0:s:' + sb, '-c:s', 'mov_text');
|
|
795
796
|
args.push('-f', 'mp4', '-movflags', 'frag_keyframe+empty_moov+default_base_moof', '-');
|
|
796
797
|
res.setHeader('Content-Type', 'video/mp4');
|
|
797
|
-
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', '
|
|
798
|
-
|
|
798
|
+
const proc = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
799
|
+
let ffErr = 0;
|
|
800
|
+
proc.stderr.on('data', d => { if (ffErr < 3) { console.error('ffm:', String(d).trim().slice(0, 200)); ffErr++; } });
|
|
801
|
+
// пребуфер: копим ~2 МБ выхода ffmpeg, чтобы браузер стартанул с запасом, а не по кусочкам
|
|
802
|
+
const PRE = 2 * 1048576;
|
|
803
|
+
const chunks = [];
|
|
804
|
+
let buffered = 0, piping = false;
|
|
805
|
+
const startPipe = () => {
|
|
806
|
+
if (piping) return;
|
|
807
|
+
piping = true;
|
|
808
|
+
proc.stdout.removeListener('data', onData);
|
|
809
|
+
for (const c of chunks) res.write(c);
|
|
810
|
+
chunks.length = 0;
|
|
811
|
+
proc.stdout.pipe(res);
|
|
812
|
+
};
|
|
813
|
+
const onData = c => {
|
|
814
|
+
if (piping) return;
|
|
815
|
+
chunks.push(c); buffered += c.length;
|
|
816
|
+
if (buffered >= PRE) startPipe();
|
|
817
|
+
};
|
|
818
|
+
proc.stdout.on('data', onData);
|
|
819
|
+
proc.stdout.on('end', () => { if (!piping) { for (const c of chunks) res.write(c); chunks.length = 0; } try { res.end(); } catch {} });
|
|
820
|
+
setTimeout(startPipe, 5000);
|
|
799
821
|
proc.on('error', () => { try { res.end(); } catch {} });
|
|
800
822
|
req.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
801
823
|
res.on('close', () => { try { proc.kill('SIGKILL'); } catch {} });
|
|
@@ -807,6 +829,7 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
807
829
|
const f = e?.t.files[Number(mV[2])];
|
|
808
830
|
if (!f) { res.statusCode = 404; res.end('not found'); return; }
|
|
809
831
|
ensureSel(e, Number(mV[2]));
|
|
832
|
+
const webFriendly = /\.(mp4|m4v|webm|m4a)$/i.test(f.name);
|
|
810
833
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
811
834
|
res.end('<!doctype html><meta charset=utf-8><title>' + esc(f.name) + '</title>' +
|
|
812
835
|
'<style>body{margin:0;background:#000;color:#ddd;font:14px system-ui;display:flex;flex-direction:column;height:100vh;box-sizing:border-box}' +
|
|
@@ -814,21 +837,25 @@ async function webReq(req, res, client, st, opts, cacheDir, base) {
|
|
|
814
837
|
'select{background:#1b1b1f;color:#ddd;border:1px solid #333;border-radius:6px;padding:4px;max-width:340px}' +
|
|
815
838
|
'video{flex:1;width:100%;min-height:0;background:#000}</style>' +
|
|
816
839
|
'<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>' +
|
|
817
|
-
'<video id=v controls autoplay src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
840
|
+
'<video id=v controls autoplay muted playsinline src="' + base + '/s/' + mV[1] + '/' + mV[2] + '"></video>' +
|
|
818
841
|
'<script>var B=' + JSON.stringify(base) + ',ID=' + mV[1] + ',I=' + mV[2] + ';' +
|
|
819
842
|
'var v=document.getElementById("v"),aS=document.getElementById("a"),sS=document.getElementById("s"),n=document.getElementById("n");' +
|
|
843
|
+
'var MF=' + (webFriendly ? 1 : 0) + ';' +
|
|
820
844
|
'var tries=0;' +
|
|
821
845
|
'var load=function(){fetch(B+"/api/tracks/"+ID+"/"+I).then(function(r){return r.json()}).then(function(d){' +
|
|
822
846
|
'if(d.retry&&tries++<15){n.textContent="⏳ готовлю файл к стриму…";setTimeout(load,4000);return;}' +
|
|
823
847
|
'if(d.error)n.textContent="⚠ "+d.error;' +
|
|
824
848
|
'var A=d.tracks||[],S=d.subs||[];' +
|
|
825
|
-
'if(!d.ffmpeg
|
|
849
|
+
'if(!d.ffmpeg){return;}' +
|
|
850
|
+
'if(A.length<2&&!S.length&&MF){return;}' +
|
|
826
851
|
'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)});' +
|
|
827
|
-
'aS.value=A[0].a;aS.style.display=""
|
|
852
|
+
'if(A.length>1){aS.value=A[0].a;aS.style.display=""}' +
|
|
828
853
|
'if(S.length){var o0=document.createElement("option");o0.value="";o0.textContent="без субтитров";sS.appendChild(o0);' +
|
|
829
854
|
'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=""}' +
|
|
830
|
-
'var go=function(){var t=v.currentTime;v.src=B+"/remux/"+ID+"/"+I+"?a="+aS.value+(sS.value!==""?"&s="+sS.value:"");v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play()};' +
|
|
855
|
+
'var go=function(){var t=v.currentTime;v.src=B+"/remux/"+ID+"/"+I+"?a="+aS.value+(sS.value!==""?"&s="+sS.value:"");v.load();v.addEventListener("loadedmetadata",function once(){v.currentTime=t;v.removeEventListener("loadedmetadata",once)});v.play().catch(function(){})};' +
|
|
831
856
|
'aS.onchange=go;sS.onchange=go;go();' +
|
|
857
|
+
'v.addEventListener("playing",function(){n.textContent=""});v.addEventListener("volumechange",function(){if(!v.muted)n.title="звук включён"});' +
|
|
858
|
+
'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);' +
|
|
832
859
|
'}).catch(function(){if(tries++<15)setTimeout(load,4000)})};load();</script>');
|
|
833
860
|
return;
|
|
834
861
|
}
|