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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/wtui.js +34 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "torrent-online",
3
- "version": "1.5.7",
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', 'ignore'] });
798
- proc.stdout.pipe(res);
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||A.length<2){return;}' +
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
  }