From b54b5d9112a480277a4e179d2f55f053b777679c Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 14 Aug 2020 23:31:28 -0400 Subject: [PATCH] * Fix Plex mobile apps spamming a new notification every time a video plays. * Loading Screen. * Minor log improvements. * Minor defaults improvements. * FFMPEG concat process to use 1 thread. * Fix av1 bug when plex has to transcode the audio. * /m3u8 endpoint --- index.js | 20 +++++-- resources/loading-screen.png | Bin 0 -> 21329 bytes src/database-migration.js | 20 ++++++- src/ffmpeg.js | 29 +++++++--- src/offline-player.js | 5 ++ src/plex-player.js | 12 +++- src/plexTranscoder.js | 22 +++++-- src/program-player.js | 5 ++ src/svg/loading-screen.svg | 107 +++++++++++++++++++++++++++++++++++ src/video.js | 58 ++++++++++++++++++- 10 files changed, 254 insertions(+), 24 deletions(-) create mode 100644 resources/loading-screen.png create mode 100644 src/svg/loading-screen.svg diff --git a/index.js b/index.js index f7dae1d..b0150da 100644 --- a/index.js +++ b/index.js @@ -15,7 +15,16 @@ const Plex = require('./src/plex'); const channelCache = require('./src/channel-cache'); const constants = require('./src/constants') -console.log("dizqueTV Version: " + constants.VERSION_NAME) +console.log( +` \\ + dizqueTV ${constants.VERSION_NAME} +.------------. +|###:::||| o | +|###:::||| | +'###:::||| o | +'------------' +`); + for (let i = 0, l = process.argv.length; i < l; i++) { if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l) @@ -37,7 +46,7 @@ if (!fs.existsSync(process.env.DATABASE)) { if(!fs.existsSync(path.join(process.env.DATABASE, 'images'))) fs.mkdirSync(path.join(process.env.DATABASE, 'images')) -db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version']) +db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id']) initDB(db) @@ -65,7 +74,7 @@ let xmltvInterval = { if (plexServers[i].arChannels && channels.length !== 0) plex.RefreshChannels(channels, dvrs).then(() => { }, (err) => { console.error(err, i) }) }).catch( (err) => { - console.error("There was an error when fetching Plex DVRs. This means dizqueTV couldn't trigger Plex to update its TV guide." + err); + console.log("Couldn't tell Plex to refresh channels for some reason."); }); } }, (err) => { @@ -123,6 +132,9 @@ function initDB(db) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-offline-screen.png'))) fs.writeFileSync(process.env.DATABASE + '/images/generic-offline-screen.png', data) } - + if (!fs.existsSync(process.env.DATABASE + '/images/loading-screen.png')) { + let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png'))) + fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data) + } } diff --git a/resources/loading-screen.png b/resources/loading-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..8bfda9a6f498f01a18f1f55ce3c808a48dc26eb4 GIT binary patch literal 21329 zcmeHvc{G*n_xEiGrAR7Ms3?^NBJ2kfPEQjdl|&(xN|}x!lIff> zC1XkELr$4FWac>DeceZWzrXeV^R9QT_m6k2&U(Jb>Avsly7vC;&;IOvUFWQC$lnIK zt5yiFz%Xo;-XX1jFpQIcVeCp=9B`$t;oU*_!|riF?*tdT{JBmC!@rli9WwL4FrHHM zKb9zI9uc@GmwL-l!Fw->WdSPN{9j4Mz4FWy#B4G7`fd ztAzNw4*gB)96xa`BUJS0ksa(eNXkh`l1E@^^fE1)ZRKTt#K>G4%;CG*qY2QY*Iyte(Bj*J6=VSljuLzF*R!Ayof zitDXFok=XDw`JhL@DC5kN+^=caFUFRwcli7koqs`} z?4GcNW*&ZM~|G*P5%(>@Hjj9Gj-E zt+!?j^^e`FsE7`#YjB_K)+AB)_L3NW)BTIn6EQ#fsRBVkGj9v0PXFxaz3nGA%=mu# zr`N#Tr*c~fgoV{Kl1k2Bt(-o^!!%*n-~m?wZ{Y`Z(TSMeTasVV~Wx z@b9tlOPcK6;_p504wm;vOq<;ryxFkWS^2%7Zqu{_CB6If1rZc0A8BU9^s{5+?oxl| z)C^b?bISXpyMKyn1SuPk*5Q{oBY`smHohQS#tsEaFEOyF5U{vGovwW^WrNc5_wNMLt4BH-3Hn z()9;C*DrkRX^kKY!E!s7taJPqro5}fqW;h>7a2F|M&(vngGZB_8O>^u%V#AkB3E_o zb7#G@(+|JJhFe$4F?z-z(i0~(ZP%Pn zvH7UosA055z@XH7%h!OmbhA&Ki6dA=-{^=d6aOrDbCnIa@K0bVB!MJ2E3Yf{*u?(9 z-3SW_`k|@uVJ026H%I^-kni`wLF&p0Q#^N~_o|~vx?mAK^5=k6asAX8h}ErT;wG*|9JuQf`}@!sNXvd zhQ{8A=L-a!z`mvB)+~7MJmP{8!&h}>Zg4=c$qVa;Ne|uCe{toB5=)T=>!|4z5=)S&L*p(DJpvQkNa`$W&i`vbGEIsWZKnTgt^#OXEw?7L3InSU$ zRh7HL6T90gz1LxwQUU@B!7iEPlD@!olFTvc zPfj7{FLz~>Q3&r=9wFRt2ZA2K#6odOtl5gV)0A(fXMLvJ7|ui-cBr{M3vAq80hHdhgT;z z{Hz&5w_;T0R%mdZG0J*3BnNqfN<665FRs{bIEN9xmV|4*l!d8bnJx5SPCVfGpJmv) zCNDJ$QFS*X&52%er)7DcEf;z42m6F+908$bdjaLVWMsdMV3H5#sCf$}Rz=}J9ASyX zxxKvaj#?XLwF_d6t5M$7rR7DI`K*-!$k@9w+iMG4;v6vyt37=a-tRVJVj-)#CNF?S z9Dgy?+vcnZOJ(fEk<;5-vto1`@V*F0QXzy$a&qZLEYEdYxX#`+Hna>Os{b46F&=QH z-9ULZi|0oMPPy?VCGcXsB&a}Ws_|P=IJe!$f0?~uS(lLq$EBS(vpX|ezxF4PCNa^3@xqtKbMw#;3!Q_ zRCP1G8}B{V$z`1~m;3mx5r&*|1_Y@nbpVEyGW}-kYvXIzW@~%K>YN>m}teo2VVR$LYVpY3J zeLA7I8uF`oXsHEYu7mXK{dQJ2w%_W=!;i=X5r#ze(LmDf&jyK!r&zCyEJARNz~c%V zw!ABt+Wit->8#FCP5+TbCc@ukuf z{7VPdvbu5pZ&nludvR5ik@`3QkRLMKh_!iw0xuCF`N!;MIe8+8lBQ__2}IjhDuy6Q zK}yDwX0ah@(m|c$)p396s!aD4t)HW+Y8KH1gbCw@#-AA{CcJ0LYAOx1O;XTzj$W%- z0gR0|58S>Nn)R6_ZiU3-@l&V~lQAK zMQFWzz!JYol-%DpnDMMGL>(zgWfp?!>B~3ytS=6m&Dtg%;B>QcSTAbm6UK&N`u|43 z`)b@D73P#VFSSi91%M7RvdE5&PS@C#1Xf8rj{Ma5ZCXLGqpX^_B8F_*zt)(e<&45I z@~e85IB^yI0iP2S0N+^})8 z!s~h8o@WJ=#sTQd2%?0j&Cuwwdv5)Bc-qw|>~d`=A0oqP)9uK0IW`q;)~*RI!uVzk z#BFQIVVDnA4%s`9w)^tA)S&CulsM0(VN`7OtI^Hihf#m6|LEYCa>PbtkXB!mz>n+w zX`gEFTg#B^&IMTPQWyci|McG`tj@)1ukh9P^I^TutGJ+U*{n%j3wr`T%<-w&a>8*t z-mXHXPuG#ftM^Gf&aQts#1i+gSm?k)92XA#YE+C>2yzf9^?-PWkAzgmMI0m92w!ma zWed9!cX)DI8eHR#4?)V(EM=Ndhw(pOHrZ9tWOli-gIiF}*m~k|iuJ3_H?r&yQ z;#DCuG&J=`EB=nAz6YW%P*J}bY@NbtBQV6oVBEKBeD!AAmXRy_U5UCsniQowCTnpD z{AZ9LOZ+1TKH2&n0VOb$nGG+Lk*g?_4-H)}E_dN9-``t?Be~f)1Z4w&eju@hGO(tZ zhuwJK4nJ@W|1AKxl+KOt8#CJiLD$*u2$X#|=V;p6`z$4c6LCx~%^Uf*_!RqM2#731Qv9 z8n+A-YG80eHI5%o1~K%$dY|-_??#59$v>|cL_s{8krE0Z%DQA`>D z-p}dAA*{0>pB)$2XJBdAJ0}*X`*7>y8E41>-38J78IONL&8v5+m|Mc6GDn%rv1RxC zc^Fg{CJ!7!9aUJ_3MQFZM9F96SXDHq(8j0y6mEkdkj%1WEtceR(2wt^G(8m;oJ>E`}O30m@Ht51u0$&F1lww4y?vyna$n^ zWvV0GWa4I;$^+I*JcbE}L12gVvAF1wd>rfe#!WbTIDkDwQpuhR224claM6RLC5w^! z2V*yVuM5FLV9HKjf>b(|wd>386wEXg1)Jgd$Eob(SAz+*AxjdCuT9uCu%_!vl&zZE z0BEZK&X@J*{V|&o8_?X5-O~z9OlCg)(o9J57#6323NKE`oyIrNs@K+ceOXRuNP>#V z> z<2;_?xf71U1U2Gv7Sh-l)&jV*ti23nkc;upMxMDiYlXA$G24J;Kt5hb z#r#3bwB2A&%yncIL54-;oIMQX20uQ7Q06|=9EE6I&qm|I2nHuxT!7R1e4^ypegzKb zx&+_hSU%*d*Mme5hs7E!t6eTG2S?>`J0P{<`08i-XrmPy-u5TsrRbYl4=nlA!Tz>9 zOw`mE`??_Zjrf$tDD4f5Z;X*KRYK-dr^ET5#7nh^M#8QDDdHAP4Y%x zsFM@xS-ax!b|&n*A=q|0y`D8r4Zyy1>MREyrw|+9xN;L9ymZeA1^LONWD=NL7Iqns zzs@vwMasY5qoVlgmHW3ZBfst6fqP%)^99oVK}tVnU?l`9{*2fnZge7T_m%BmcA=UZY^}qby>h{SxX2nW#h8 zR0-J`7nv-(y67m2o52vunw48{n;TPC+--u~kv6j1_?4TGy_C*Zb=6TQ0gI-vONcPw zw>Tc;C(IzDJ7;p=0C<1Hs?h$K_3u!zz+0SfR01}7=As2wVpa&d9j|Vd6Nt zfDT7UaVNqrn_452fA=1xopT6(r!`&AjEwl;nbZ^E*VPGf$BFAc8CIXUD1e%lUt zemXs4$075xbPq3uaKcxXI1#(Yz?YgOhn-+gW312y03Cmo{B(!Nr-rpyuMSEjm#x3! z@Re(RvF921r=@i)`%PJt3?v!M?;th_rK1ND*diM-&nS_PkI%j_Y*kJ8uJr4#zE}#N zk`cSaq;-IMsv$U0*~8NnEW@E9XSvj{zgW)1k<+d9A!}FaiRYM-$?qO{dBi2qZ;ySe z$a>IeL_6)r;-v9-*WmGf`<|=s*}f7nvhu5sm*S(lYBY+ ziukm`yoKrV<7sWUY9I5(ISwq%&0=OpvaBb&rz^L#ZL$!OpcLap0KzP)gdt~ zX--US{+;Z<+_4oUL)-8?@vGRmeLV#N-I?rIaGdc-u2m- zR9g{?Nu_PaYCo=Laf3g2-?aZK!X^uuEDY=kTS@SXUD%a;)fjKH_?V{I8r^>V0>A=9 zX)J{luD(>y7GxGPgNe{mGS_w4;6*t;mHTpy`ICZl^Z?UmL~&-7eBxvSB_9;{v!y4q zYZPWGA$kNUNAiUkt~1d4;2XJ(2M`BaQqHm%m}jrSdY?Jni+biG_C>{fvtuVa3T(nGj&!rrd=tvVF{ zbLd#}bRDyX0h03I-gBq0K1_jCMU|Os7kcrc2=(jra>DrDz1YaPDcyBOp~+9y<%biX z1`RPK9~x34BkdELy6P%FV0kxp%sqrqT{o^rn#j}%=&bUEpUf*NHj>Hf6XbvdQ=mf_ zQ?5Wmq^f3mt4|o6sJRe&Y#Pu!{7Vwo^WzoCY0}t)Jhd72sUODbVkhM<0_s&_AXm55 zf2j=AcV9k?-xl?U8;kKjPR9cLCY({++5E1@jE5*E>2 z@F((g;kya!7|0t)6YNQhm=by>m9KuVin2ZZ>TnwA?WRky3!fT>LLWI;PuyR~OmE>= zx3}I&^DA!r0ch`H>MPW*^u34xrV@{VxW`W}r?!}`DxJ$x{2Ha6`9xUItGKvDWbh-! z)OxBk-@(J4sZO?x-G>ar4yjNt;F=hHC-?`L9O7`!Hk!>o?hV{`&6U|rgH!L`7 z8-~^{FYYz9knr~W`c$KKdPtBO@bzZ!1M1a5OVxF>p@7<#Wns5Ik$UHx?1uhH4b$<| z64KT(D!uO16SFVFF6HB=3fAd+s$Q5{)}Ytx%n#IXeXHZ?5!dfj0`GMY@5x?Gf-j%S z{(Wn9a?-h+VP4v28LnSG&gWhiW1W`ct@TO6uAxHCK`SjlXYH`4VH^GJl+eLqimVSK zo9KFJp{Gfw^d+&A-g?x!({p%~*1?xEH5DBE(tEftd~wy!Pa9$aMahZb`eMrKeGBhY zrU*MH3h8VGhPbRk#M}QO`SL159+zg%&uy*dI<1esO`IWiOiX5$^grqJ%TLCcMN{P> zAUwAUhN@ATzD6|+1?_;nlRnaIEXL~}t|VRV=o`g>vDX^pR1&{_6>Ajq!$^&${GHbG zrW5k>Pm_isCTkw`ZJ$p}Y7r=^DL%Tm(C>RJyUFSeghMsY{Hl>Cd>HC;0te6W)WEDC z?xUnfuZ?`#DcsDDm`xk5p6FkacqX7vw{d|%pZ0G?fa0%^KM#U-q1XQdc!G0{1<=&A zFNMD2-HGB0eOa>*Cv((U2TV4uH-@ah+VBHZ!uJK*BEyyb zPMrQ%;m?oKW1}U!6tq1q(`OohaIQ>8x{D2OrZ?MYjh zYTIL=KKVtdC&um+rO-FC?Rv<^ydX*X2hFhajrSzNo$7f!AV4#wh793 zPfbekcsy+-B`7tKNF?U}xNngRJUBW~eD2)2#<#tXEEWby4CltqPR(x%otFmF2I%8; z37#E(BX1oiCy7A;@BM-XEjp7SUc`ZCkp{w3v)ZBS!+mK_rs)lu3k|Zw`2x~>!Mnj) z<=(g*o0E?lC_gtZ_Mg2soAP>Gp1!QJQf| zPi?&F%&a(L^v15mpyvNLv!Q{8cM!K@B9Cb(7f+_SmYpvcR``~qZCFZNoFi6jPl{V3 zKKntp`&H5-;psWVi1T^huJ;WLJUp>Wb@=sh{~yxUAy2n;zlc3AJlz^F)gm=gG)+;0 z;7JwI9ymS{;$FWqn0t4g%|90lkKYgv^f_U)?nw0RaPu^UZRono-Pqf@fZaUG-)|#6ZEz#AxX<+`Rah@hTI(i$3TER~lVS9@G8|GC z!*k0kG^R51W_w~2jZ?~w>?v9see#0d2KX7Bd)H;oa>hJOTq@4(0R4wG!+v6Na#gVd zSnoboCJ^D*iIQw2k9y1Y8%-iYZOxJUJ42=md(~yVN7|JjAF1OBi~ef!L)m5 zx26|OwGNN#Ky5m7xT@{8D3h?jTZsxgiwMGbXXlhhKHVX<)c878TA{nO$0u!pRU(S? z4qBl1dz#10w;LwC@!nSZJbJ3#iStFB^F_APjG9h*jVy6tB1uzys8yWa#Yw&OTmy0| zr7^|CRd%;8Oib}_N6J^+X-zM)PW_r@q3E+vwTE=F(J4nSC}L00Y?V=wmH1@q)YQ}f zq-guotrCy_V_b=BEB@W&Mn1tHbU$_QqP+Fo=jc-lbCXwkE5!VUVw!kFf1JE;ztb#BlHxxOb_an|)U2xA zBGdD((yqm`4)k(YdU+mgx^r=%lX~CcypUP~M6fzm(e;ZnuTmF5X}nsFvn~N(%IkX? z`gCWNAyHgVFu2aYM=sN)CCht;2Nd%Iz7gHXm%x}v;?x}J?++o(gpy*u4DK!%$~q%2 zoJi^I{nXT?=yL2EWQ60FL15o>*`^yC_WA?@!sS1IR7n*s9#!mEtFErD8T2G_M#H%& zJ$^L5fL83+JPUfKTQpKQUMrHDFe*TrDYIQTW%+bo|fC)Q9zN0H9 zBq}O;r8h!juFtrLf&NYRiMI3-^6($~Dl8}{NcTC@H262S!0rv%_e0cXzemziGxT;; z?R^wD*RN-(nkDi!JKH1F8KicD@Ao@bdaLg55}9(9f5c!gd;l=1i@RXCStsX3h7qLWY)Ir!n@9H&Eu5Qg?8SW>DZzW`!91=clEss|$5^ zx8Yjtq5Q#g{y-W?lHVvb$GNF|kVw+#&$ssZc1_QE-XQhl+Y7N?9SG|Z#?joV-ihwe zJ>~x{CNA*Ot|3nmYBJ z>8rezMsacO?EPv^3S3Yq>I5?E9rAbXq~%{6%(YjB`6LgaUF~hc6KaCQX5sL1>FDUNn4G&4 z{|XwG*6xRU{J(LRyzh**4pc_+mIpMVf;xFURRw_jx$t>4|2mgaZ*u}VkykSV2g zx{X39-c<$R*!UN#4RqG*sOKoDo$d@;OuMx3VGva`+g?zPkj{mUPEoZWAXoSI$YZy_ z{#$;Xxw#?H@dqn5(jY#D#HPfZRh9ROwB6J@(x4V6C*L$rThGUr?NHO2+{7yy4JSxg z;_R%VGb?->^W**=YmFg)XimU^Yuo}nfso;9lV6UP6bC|guu_D3K`Q8{6#sP+A0HIc z-m6V&)9oHt#JxQ|OD}VZDLuF=vAO&46`qF^ISuasweD*IR)T7p-)|qx9hmg!t?l@V zejswrQc+IV%>YZj-7uIoRWd`n*h)g9zj_wmGSj0- zQl1$D0|iO+Dw5i(rg$*EM2C;JsLnxK)_=CyMFA=(svUa6pXTci@|J<^9CezrmB-GH zE$GNO*2XD8Vo4RA&!3$qEzZA-Xlrfl1|r9P8LR~dVw634rgp%#grJ3Qg4Vc?*CMj65#RPaY<4CVl?T+I@&hE8?V4@Pai$JhFV3xRN;Azo)jZ3oF?!jZ=0|4XEKOKw zNjUmq$Y(I^P4`FvO-#)?&&`%T<+?bPM|{=jb24TUG->jwqeHzX zCxQN5ii#TRZq#5&(S;yJ^}?!@!_dZV{3|{_9%a5;idDA%>`$NJoFoSNRK z5OPZ?`i;Khyb*8h`y*;>D7&saB8X14O&~T6x`QTFG@4k8W;@p1(n*wkb$jY()t(5s zS|te;RaKzZKnn5J$jC?@w6&-aElH1;b2K;!^PrK(d^-DI5{Y|IamX9VAKZMZL8B50 ztA96tLNMr+OXwhUyv;KMCM026;Bvot^+Bg85i{&PnbJFAMRufXVi6EsM?ij^X}igu79~<={?=@k1w6X|v6o*hEEeqUIZg3hmJn3kpEkN`2j65E^_`w1HP+#)jS$v|xg& z&8LP2e$-d*G=F`@P5ei+)urvw9b_u?#(8WlT%7M*WZ3pXaSD4N=cr?C-I-BGgTh|U&t)IS5qAA$e@ptZ*6OnICWfc^e;|P z`GLuWg@v(ed;B-0lNfXEwE2)@{+k*735@ML&{PCBul*c2QzCwgH)y8B;^aKk)>7g^ zE3pg`Mx{UB6*}sd$ve`E=LX*8PdD2|%tTqwM(s+sBn1YXgbv;vvIv!%eC=T6j(xE; z$Pcs~Dxq{f)eb%N>8ZBG$;J2FdrHk54Jwe1ZE2q>8{qqubBU$pQ) zU-NKZWPYz{0&Hm>=iuM8)NdYaB1Rac7#C6PUhm(t`Dt=;9>}^K%;*lu&eJB~^KzZE zacQ_BMG$)TnXBJiwMtWn#ljUEJoHa`)a`1ce|`~&(U zm+91)Yzmd|YA4m%dfpsjvF$u*VXCjczomOnMDctWGwrA1Wsp|)xGP+SzKJ+ZE0laCf>A{GUSP{O)qMO+K{ zP=E9cz6``{2EJ^5>U0sE54dQ;z71(qjH>tf>NsUNW6&33(UduAxM?$wpkVW;4rTZ> z6;Rf}lbIKOeDwJdWzr3CYIV5i?d4UDwj)R|l9RimOsql#to>zM(i@-^!I>Si&)FDl zM3GYNc?4~u?YrT$Hx!k;H#9gQ5Je1(nO22Hn+g>r6jHfDlU|~!WfWdacXf6S&8}6E zhV3GfNz7$z-A0yc-9pO2rQ+ zWGvC4G-h)XI{dW!;*=EWbSNqB#=%pN^3JdJaPu~XudAO~7ck<1*Pi84ajO9-s#O8( zyDQYE+vQy7;{b;UwBn0JMOE;nx(%m1V+Z_N3v2FCQBmopiZA;3)2ZU<5CPxV*QrmR zETsRe$^?2t@H1hnP>y(mHj?mq*HjD@#vRl=Cj-9x!d}SXHoUHfJQGGP$6ffio!@F6 zF$Q`o5mZcHJl+dWdTC7t=cP7k!^iywR&dm^t!l4B-6K)Zh54&ZK-;#lAA5q70|(y( zN(0Pqx4&Esl~vSe*XCqn0q_=k+X|J0no+_N`qrbj997AJ;am$XtyHTb6^aiv+y}O} z)c&M|TUk3TPNHuYRatS{(1M$9G7qcZGdUN17lpt6`+x=y1zcUK<>>yJKM(#se*FUv zqTnA={EtJ;Kcx6SAq9h^u6i3iDEQqqAc82*ifv^_^#WextG}cuW;GeV0zY15T}nEe z0~k7shL=8<4gMD+EJt5PqL<7jR36al&t!iv0~-9H#-DUTa_~nr{)i*u!JmTie^EEN Y&Yd}+V?~)HA_Voc4Ybk^*!=sy0NJoj5dZ)H literal 0 HcmV?d00001 diff --git a/src/database-migration.js b/src/database-migration.js index ce85c81..e17ad4d 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -17,15 +17,29 @@ * but with time it will be worth it, really. * ***/ -const TARGET_VERSION = 300; + const TARGET_VERSION = 400; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 [ 0, 100, (db) => basicDB(db) ], [ 100, 200, (db) => commercialsRemover(db) ], [ 200, 300, (db) => appNameChange(db) ], + [ 300, 400, (db) => createDeviceId(db) ], ] +const { v4: uuidv4 } = require('uuid'); + +function createDeviceId(db) { + let deviceId = db['client-id'].find(); + if (deviceId.length == 0) { + let clientId = uuidv4().replace(/-/g,"").slice(0,16) + "-org-dizquetv-" + process.platform + let dev = { + clientId: clientId, + } + db['client-id'].save( dev ); + } +} + function appNameChange(db) { let xmltv = db['xmltv-settings'].find() @@ -66,7 +80,7 @@ function basicDB(db) { maxPlayableResolution: "1920x1080", maxTranscodeResolution: "1920x1080", videoCodecs: 'h264,hevc,mpeg2video', - audioCodecs: 'ac3', + audioCodecs: 'ac3,aac', maxAudioChannels: '2', audioBoost: '100', enableSubtitles: false, @@ -105,7 +119,7 @@ function basicDB(db) { let hdhrSettings = db['hdhr-settings'].find() if (hdhrSettings.length === 0) { db['hdhr-settings'].save({ - tunerCount: 1, + tunerCount: 2, autoDiscovery: true }) } diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 40147e4..3dfa9f5 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -73,8 +73,9 @@ class FFMPEG extends events.EventEmitter { this.spawn( {errorTitle: 'offline'}, streamStats, undefined, `${duration}ms`, true, false, 'offline', false); } async spawn(streamUrl, streamStats, startTime, duration, limitRead, enableIcon, type, isConcatPlaylist) { + let ffmpegArgs = [ - `-threads`, this.opts.threads, + `-threads`, isConcatPlaylist? 1 : this.opts.threads, `-fflags`, `+genpts+discardcorrupt+igndts`]; if (limitRead === true) @@ -94,6 +95,17 @@ class FFMPEG extends events.EventEmitter { //TODO: Do something about missing audio stream if (!isConcatPlaylist) { + let inputFiles = 0; + let audioFile = -1; + let videoFile = -1; + let overlayFile = -1; + if ( typeof(streamUrl.errorTitle) === 'undefined') { + ffmpegArgs.push(`-i`, streamUrl); + videoFile = inputFiles++; + audioFile = videoFile; + } + + // When we have an individual stream, there is a pipeline of possible // filters to apply. // @@ -108,8 +120,8 @@ class FFMPEG extends events.EventEmitter { // Initially, videoComplex does nothing besides assigning the label // to the input stream var videoIndex = 'v'; - var audioComplex = `;[0:${audioIndex}]anull[audio]`; - var videoComplex = `;[0:${videoIndex}]null[video]`; + var audioComplex = `;[${audioFile}:${audioIndex}]anull[audio]`; + var videoComplex = `;[${videoFile}:${videoIndex}]null[video]`; // Depending on the options we will apply multiple filters // each filter modifies the current video stream. Adds a filter to // the videoComplex variable. The result of the filter becomes the @@ -197,11 +209,10 @@ class FFMPEG extends events.EventEmitter { audioComplex += ';[audioy]arealtime[audiox]'; currentVideo = "[videox]"; currentAudio = "[audiox]"; - } else { - ffmpegArgs.push(`-i`, streamUrl); } if (doOverlay) { ffmpegArgs.push(`-i`, `${this.channel.icon}` ); + overlayFile = inputFiles++; } // Resolution fix: Add scale filter, current stream becomes [siz] @@ -223,7 +234,7 @@ class FFMPEG extends events.EventEmitter { if (this.channel.iconDuration > 0) icnDur = `:enable='between(t,0,${this.channel.iconDuration})'` - videoComplex += `;[1:v]scale=${this.channel.iconWidth}:-1[icn];${currentVideo}[icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[comb]` + videoComplex += `;[${overlayFile}:v]scale=${this.channel.iconWidth}:-1[icn];${currentVideo}[icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[comb]` currentVideo = '[comb]'; } if (this.volumePercent != 100) { @@ -250,14 +261,14 @@ class FFMPEG extends events.EventEmitter { transcodeVideo = true; //this is useful so that it adds some lines below filterComplex += videoComplex; } else { - currentVideo = `0:${videoIndex}`; + currentVideo = `${videoFile}:${videoIndex}`; } // same with audio: if (currentAudio != '[audio]') { transcodeAudio = true; filterComplex += audioComplex; } else { - currentAudio = `0:${audioIndex}`; + currentAudio = `${audioFile}:${audioIndex}`; } //If there is a filter complex, add it. @@ -309,7 +320,7 @@ class FFMPEG extends events.EventEmitter { } else { //Concat stream is simpler and should always copy the codec ffmpegArgs.push( - `-probesize`, `100000000`, + `-probesize`, 32 /*`100000000`*/, `-i`, streamUrl, `-map`, `0:v`, `-map`, `0:${audioIndex}`, diff --git a/src/offline-player.js b/src/offline-player.js index 3ab458d..00d5b20 100644 --- a/src/offline-player.js +++ b/src/offline-player.js @@ -13,6 +13,11 @@ class OfflinePlayer { constructor(error, context) { this.context = context; this.error = error; + if (context.isLoading === true) { + context.channel = JSON.parse( JSON.stringify(context.channel) ); + context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/loading-screen.png`; + context.channel.offlineSoundtrack = undefined; + } this.ffmpeg = new FFMPEG(context.ffmpegSettings, context.channel); } diff --git a/src/plex-player.js b/src/plex-player.js index 985f7f6..d1c4e59 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -10,6 +10,8 @@ const EventEmitter = require('events'); const helperFuncs = require('./helperFuncs') const FFMPEG = require('./ffmpeg') +let USED_CLIENTS = {}; + class PlexPlayer { constructor(context) { @@ -17,9 +19,17 @@ class PlexPlayer { this.ffmpeg = null; this.plexTranscoder = null; this.killed = false; + let coreClientId = this.context.db['client-id'].find()[0].clientId; + let i = 0; + while ( USED_CLIENTS[coreClientId+"-"+i]===true) { + i++; + } + this.clientId = coreClientId+"-"+i; + USED_CLIENTS[this.clientId] = true; } cleanUp() { + USED_CLIENTS[this.clientId] = false; this.killed = true; if (this.plexTranscoder != null) { this.plexTranscoder.stopUpdatingPlex(); @@ -39,7 +49,7 @@ class PlexPlayer { try { let plexSettings = db['plex-settings'].find()[0]; - let plexTranscoder = new PlexTranscoder(plexSettings, channel, lineupItem); + let plexTranscoder = new PlexTranscoder(this.clientId, plexSettings, channel, lineupItem); this.plexTranscoder = plexTranscoder; let enableChannelIcon = this.context.enableChannelIcon; let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index a945404..1fb8dd8 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -2,12 +2,12 @@ const { v4: uuidv4 } = require('uuid'); const axios = require('axios'); class PlexTranscoder { - constructor(settings, channel, lineupItem) { + constructor(clientId, settings, channel, lineupItem) { this.session = uuidv4() this.device = "channel-" + channel.number; this.deviceName = this.device; - this.clientIdentifier = this.session.replace(/-/g,"").slice(0,16) + "-org-dizquetv-" + process.platform; + this.clientIdentifier = clientId; this.product = "dizqueTV"; this.settings = settings @@ -60,7 +60,10 @@ class PlexTranscoder { stream.directPlay = true; } } - if (stream.directPlay) { + if (stream.directPlay || this.isAV1() ) { + if (! stream.directPlay) { + this.log("Plex doesn't support av1, so we are forcing direct play, including for audio because otherwise plex breaks the stream.") + } this.log("Direct play forced or native paths enabled") stream.directPlay = true this.setTranscodingArgs(stream.directPlay, true, false) @@ -78,14 +81,15 @@ class PlexTranscoder { await this.getDecision(stream.directPlay); stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}` } else { + //This case sounds complex. Apparently plex is sending us just the audio, so we would need to get the video in a separate stream. this.log("Decision: Direct stream. Audio is being transcoded") + stream.separateVideoStream = (this.settings.streamPath === 'direct') ? this.file : this.plexFile; stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}` } stream.streamStats = this.getVideoStats(); // use correct audio stream if direct play - let audioIndex = await this.getAudioIndex(); - stream.streamStats.audioIndex = (stream.directPlay) ? audioIndex : 'a' + stream.streamStats.audioIndex = (stream.directPlay) ? ( await this.getAudioIndex() ) : 'a' this.log(stream) @@ -178,6 +182,14 @@ lang=en` } } + isAV1() { + try { + return this.getVideoStats().videoCodec === 'av1'; + } catch (e) { + return false; + } + } + isDirectPlay() { try { return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy"; diff --git a/src/program-player.js b/src/program-player.js index 2097522..f3354ab 100644 --- a/src/program-player.js +++ b/src/program-player.js @@ -32,6 +32,11 @@ class ProgramPlayer { if (program.err instanceof Error) { console.log("About to play error stream"); this.delegate = new OfflinePlayer(true, context); + } else if (program.type === 'loading') { + console.log("About to play loading stream"); + /* loading */ + context.isLoading = true; + this.delegate = new OfflinePlayer(false, context); } else if (program.type === 'offline') { console.log("About to play offline stream"); /* offline */ diff --git a/src/svg/loading-screen.svg b/src/svg/loading-screen.svg new file mode 100644 index 0000000..cc65e82 --- /dev/null +++ b/src/svg/loading-screen.svg @@ -0,0 +1,107 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Loading... + + diff --git a/src/video.js b/src/video.js index e8a331c..2c4a886 100644 --- a/src/video.js +++ b/src/video.js @@ -143,6 +143,11 @@ function video(db) { res.status(404).send("Channel doesn't exist") return } + let isLoading = false; + if ( (typeof req.query.first !== 'undefined') && (req.query.first=='0') ) { + isLoading = true; + } + let isFirst = false; if ( (typeof req.query.first !== 'undefined') && (req.query.first=='1') ) { isFirst = true; @@ -164,7 +169,14 @@ function video(db) { // Get video lineup (array of video urls with calculated start times and durations.) let t0 = (new Date()).getTime(); let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0); - if (lineupItem == null) { + if (isLoading) { + lineupItem = { + type: 'loading', + streamDuration: 1000, + duration: 1000, + start: 0, + }; + } else if (lineupItem == null) { let prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, channel) if (prog.program.isOffline && channel.programs.length == 1) { @@ -207,7 +219,9 @@ function video(db) { } console.log("========================================================="); - channelCache.recordPlayback(channel.number, t0, lineupItem); + if (! isLoading) { + channelCache.recordPlayback(channel.number, t0, lineupItem); + } let playerContext = { lineupItem : lineupItem, @@ -280,6 +294,41 @@ function video(db) { }); }); + + router.get('/m3u8', (req, res) => { + res.type('text') + + // Check if channel queried is valid + if (typeof req.query.channel === 'undefined') { + res.status(500).send("No Channel Specified") + return + } + + let channelNum = parseInt(req.query.channel, 10) + let channel = channelCache.getChannelConfig(db, channelNum ); + if (channel.length === 0) { + res.status(500).send("Channel doesn't exist") + return + } + + // Maximum number of streams to concatinate beyond channel starting + // If someone passes this number then they probably watch too much television + let maxStreamsToPlayInARow = 100; + + var data = "#EXTM3U\n" + + let ffmpegSettings = db['ffmpeg-settings'].find()[0] + + if ( ffmpegSettings.enableFFMPEGTranscoding === true) { + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0\n`; + } + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1\n` + for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}\n` + } + + res.send(data) + }) router.get('/playlist', (req, res) => { res.type('text') @@ -302,6 +351,11 @@ function video(db) { var data = "ffconcat version 1.0\n" + let ffmpegSettings = db['ffmpeg-settings'].find()[0] + + if ( ffmpegSettings.enableFFMPEGTranscoding === true) { + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0'\n`; + } data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1'\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}'\n`