diff --git a/README.md b/README.md index 10afb5a..bc63e2a 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Green **`master`** plus integration branch **`next`**. See [docs/GIT_FLOW.md](do ## PDF helpers -Generate the GraphVis decision memo PDF (from `tmp/GRAFANA_vs_others_graphvis_pivot.md` source): +Generate the GraphVis decision memo PDF (from `docs/GRAFANA_vs_others_graphvis_pivot.md` source): ```bash bash scripts/build-graphvis-pdf.sh @@ -109,4 +109,4 @@ bash scripts/build-graphvis-pdf.sh Output: -`tmp/GRAFANA_vs_others_graphvis_pivot.pdf` +`docs/GRAFANA_vs_others_graphvis_pivot.pdf` diff --git a/app/src/main/java/com/foxx/androidcast/CastSettings.java b/app/src/main/java/com/foxx/androidcast/CastSettings.java index 112fff2..a9cb1eb 100644 --- a/app/src/main/java/com/foxx/androidcast/CastSettings.java +++ b/app/src/main/java/com/foxx/androidcast/CastSettings.java @@ -45,7 +45,9 @@ public class CastSettings implements Serializable { /** Future Speex (UI disabled until integrated). */ SPEEX, /** Future Opus with in-band FEC (UI disabled until integrated). */ - OPUS + OPUS, + /** Explicit AAC passthrough wrapper for debug comparisons. */ + PASSTHROUGH_DEBUG } public enum AdjustmentPreset { diff --git a/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java b/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java index dcb8473..00e784b 100644 --- a/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java +++ b/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java @@ -604,6 +604,8 @@ public final class CastSettingsBinder { return activity.getString(R.string.codec_speex); case OPUS: return activity.getString(R.string.codec_opus); + case PASSTHROUGH_DEBUG: + return activity.getString(R.string.codec_audio_passthrough_debug); case AUTO: default: return activity.getString(R.string.codec_audio_auto); diff --git a/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsFormatter.java b/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsFormatter.java index 4e4c0ca..af40018 100644 --- a/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsFormatter.java +++ b/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsFormatter.java @@ -16,6 +16,7 @@ import com.foxx.androidcast.CastTuningEngine; import com.foxx.androidcast.media.CodecNegotiator; import com.foxx.androidcast.media.codec.CodecSessionRegistry; import com.foxx.androidcast.media.codec.PassthroughCodecPolicy; +import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.network.control.NetworkStatsSnapshot; import com.foxx.androidcast.network.diag.DiagPingMetrics; import com.foxx.androidcast.receiver.StreamMetrics; @@ -393,9 +394,17 @@ public final class CastDiagnosticsFormatter { if (prefs != null && prefs.isAudioRequested()) { CastSettings.AudioCodec ac = prefs.getAudioCodec(); if (ac == CastSettings.AudioCodec.OPUS) { - html.append(" · Opus in-band FEC"); + if (NativeCodecBridge.isOpusAvailable()) { + html.append(" · Opus in-band FEC"); + } else { + html.append(" · Opus fallback AAC"); + } } else if (ac == CastSettings.AudioCodec.SPEEX) { - html.append(" · Speex FEC"); + if (NativeCodecBridge.isSpeexAvailable()) { + html.append(" · Speex FEC"); + } else { + html.append(" · Speex fallback AAC"); + } } else { html.append(" · AAC-LC (MediaCodec)"); } diff --git a/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java b/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java index f3f740f..7e30de5 100644 --- a/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java +++ b/app/src/main/java/com/foxx/androidcast/diagnostics/CastDiagnosticsLabels.java @@ -12,6 +12,7 @@ package com.foxx.androidcast.diagnostics; * Digest: SHA256 1c6f25ad871cb71788ed6fd1817d4d59d22d5ef6638704c9483937a2a9c15977 **********************************************************************/ import com.foxx.androidcast.CastSettings; +import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; /** Human-readable names for diagnostics (no bare AUTO enum tokens). */ public final class CastDiagnosticsLabels { @@ -137,9 +138,13 @@ public final class CastDiagnosticsLabels { case AAC: return "AAC-LC (forced)"; case SPEEX: - return "Speex (stub → AAC)"; + return NativeCodecBridge.isSpeexAvailable() + ? "Speex (native)" : "Speex (fallback AAC)"; case OPUS: - return "Opus + in-band FEC (stub → AAC)"; + return NativeCodecBridge.isOpusAvailable() + ? "Opus + in-band FEC (native)" : "Opus (fallback AAC)"; + case PASSTHROUGH_DEBUG: + return "Passthrough AAC (debug)"; case AUTO: default: return "AUTO (AAC via MediaCodec)"; diff --git a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java index fad0c72..ce36985 100644 --- a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityCatalog.java @@ -248,10 +248,13 @@ public final class CodecPriorityCatalog { public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) { if (preference == CastSettings.AudioCodec.SPEEX) { - return CodecTier.PASSTHROUGH; + return NativeCodecBridge.isSpeexAvailable() ? CodecTier.SOFTWARE : CodecTier.PASSTHROUGH; } if (preference == CastSettings.AudioCodec.OPUS) { - return CodecTier.SOFTWARE; + return NativeCodecBridge.isOpusAvailable() ? CodecTier.SOFTWARE : CodecTier.PASSTHROUGH; + } + if (preference == CastSettings.AudioCodec.PASSTHROUGH_DEBUG) { + return CodecTier.PASSTHROUGH; } return CodecTier.HARDWARE; } diff --git a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityId.java b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityId.java index ae72975..c5cb092 100644 --- a/app/src/main/java/com/foxx/androidcast/media/CodecPriorityId.java +++ b/app/src/main/java/com/foxx/androidcast/media/CodecPriorityId.java @@ -84,6 +84,8 @@ public enum CodecPriorityId { return OPUS; case SPEEX: return SPEEX; + case PASSTHROUGH_DEBUG: + return PCM; case AUTO: default: return null; diff --git a/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecBackend.java b/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecBackend.java index 1c1c5c6..6e5459d 100644 --- a/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecBackend.java +++ b/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecBackend.java @@ -14,8 +14,11 @@ package com.foxx.androidcast.media.codec; /** Implementation backing negotiated / configured audio. */ public enum AudioCodecBackend { MEDIA_CODEC_AAC("MediaCodec AAC-LC"), + SPEEX_NATIVE("Speex native"), + OPUS_NATIVE("Opus native"), SPEEX_STUB("Speex (stub → AAC passthrough)"), OPUS_STUB("Opus (stub → AAC passthrough)"), + PASSTHROUGH_DEBUG("Passthrough AAC (debug)"), DISABLED("off"), UNKNOWN("?"); diff --git a/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java b/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java index 21ad624..6cb9e8b 100644 --- a/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java +++ b/app/src/main/java/com/foxx/androidcast/media/codec/AudioCodecFactory.java @@ -21,7 +21,9 @@ public final class AudioCodecFactory { public static AudioEncoderSink createEncoder(CastSettings.AudioCodec preference) { CodecSessionRegistry.setAudioPreference(preference); AudioCodecBackend backend = PassthroughCodecPolicy.audioBackendFor(preference, true); - if (backend == AudioCodecBackend.SPEEX_STUB || backend == AudioCodecBackend.OPUS_STUB) { + if (backend == AudioCodecBackend.SPEEX_STUB + || backend == AudioCodecBackend.OPUS_STUB + || backend == AudioCodecBackend.PASSTHROUGH_DEBUG) { return new PassthroughAudioEncoderSink(new AudioEncoder(), preference); } return new AudioEncoder(); diff --git a/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughAudioEncoderSink.java b/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughAudioEncoderSink.java index 31c879a..0b874a8 100644 --- a/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughAudioEncoderSink.java +++ b/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughAudioEncoderSink.java @@ -31,6 +31,8 @@ final class PassthroughAudioEncoderSink implements AudioEncoderSink { CodecSessionRegistry.setAudioBackend(AudioCodecBackend.OPUS_STUB); } else if (preference == CastSettings.AudioCodec.SPEEX) { CodecSessionRegistry.setAudioBackend(AudioCodecBackend.SPEEX_STUB); + } else if (preference == CastSettings.AudioCodec.PASSTHROUGH_DEBUG) { + CodecSessionRegistry.setAudioBackend(AudioCodecBackend.PASSTHROUGH_DEBUG); } delegate.prepare(callback); } diff --git a/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java b/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java index 27de45d..aec8396 100644 --- a/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java +++ b/app/src/main/java/com/foxx/androidcast/media/codec/PassthroughCodecPolicy.java @@ -57,7 +57,12 @@ public final class PassthroughCodecPolicy { } switch (preference) { case SPEEX: + return NativeCodecBridge.isSpeexAvailable() + ? CastSettings.AudioCodec.SPEEX : CastSettings.AudioCodec.AUTO; case OPUS: + return NativeCodecBridge.isOpusAvailable() + ? CastSettings.AudioCodec.OPUS : CastSettings.AudioCodec.AUTO; + case PASSTHROUGH_DEBUG: return CastSettings.AudioCodec.AUTO; default: return preference; @@ -70,10 +75,15 @@ public final class PassthroughCodecPolicy { return AudioCodecBackend.DISABLED; } if (preference == CastSettings.AudioCodec.SPEEX) { - return AudioCodecBackend.SPEEX_STUB; + return NativeCodecBridge.isSpeexAvailable() + ? AudioCodecBackend.SPEEX_NATIVE : AudioCodecBackend.SPEEX_STUB; } if (preference == CastSettings.AudioCodec.OPUS) { - return AudioCodecBackend.OPUS_STUB; + return NativeCodecBridge.isOpusAvailable() + ? AudioCodecBackend.OPUS_NATIVE : AudioCodecBackend.OPUS_STUB; + } + if (preference == CastSettings.AudioCodec.PASSTHROUGH_DEBUG) { + return AudioCodecBackend.PASSTHROUGH_DEBUG; } return AudioCodecBackend.MEDIA_CODEC_AAC; } @@ -150,6 +160,8 @@ public final class PassthroughCodecPolicy { return "Opus"; case AAC: return "AAC"; + case PASSTHROUGH_DEBUG: + return "Passthrough (debug)"; case AUTO: default: return "AUTO (AAC)"; diff --git a/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java b/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java index 620bc1c..5a81f0c 100644 --- a/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java +++ b/app/src/main/java/com/foxx/androidcast/network/CastCodecFlags.java @@ -13,6 +13,7 @@ package com.foxx.androidcast.network; **********************************************************************/ import android.media.MediaFormat; +import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; import com.foxx.androidcast.sender.CodecCatalog; import java.util.List; @@ -28,6 +29,8 @@ public final class CastCodecFlags { public static final int AUDIO_UNKNOWN = 0; public static final int AUDIO_PCM = 1; public static final int AUDIO_AAC = 2; + public static final int AUDIO_OPUS = 4; + public static final int AUDIO_SPEEX = 8; private CastCodecFlags() {} @@ -59,6 +62,13 @@ public final class CastCodecFlags { } public static int localAudioMask() { - return AUDIO_AAC | AUDIO_PCM; + int mask = AUDIO_AAC | AUDIO_PCM; + if (NativeCodecBridge.isOpusAvailable()) { + mask |= AUDIO_OPUS; + } + if (NativeCodecBridge.isSpeexAvailable()) { + mask |= AUDIO_SPEEX; + } + return mask; } } diff --git a/app/src/main/java/com/foxx/androidcast/network/CastProtocol.java b/app/src/main/java/com/foxx/androidcast/network/CastProtocol.java index e4a088f..4410058 100644 --- a/app/src/main/java/com/foxx/androidcast/network/CastProtocol.java +++ b/app/src/main/java/com/foxx/androidcast/network/CastProtocol.java @@ -103,6 +103,7 @@ public final class CastProtocol { dos.writeInt(settings.getCaptureMode().ordinal()); dos.writeInt(settings.getStreamProtection().ordinal()); dos.writeInt(settings.getReceiverDisplayResolution().ordinal()); + dos.writeInt(settings.getAudioCodec().ordinal()); dos.flush(); return bos.toByteArray(); } @@ -147,6 +148,10 @@ public final class CastProtocol { readEnum(CastSettings.ReceiverDisplayResolution.values(), dis.readInt(), CastSettings.ReceiverDisplayResolution.AUTO_FIT)); } + if (dis.available() > 0) { + settings.setAudioCodec(readEnum(CastSettings.AudioCodec.values(), dis.readInt(), + CastSettings.AudioCodec.AUTO)); + } } else { dis.reset(); settings.setTransport(dis.readUTF()); diff --git a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java index f4e4ba2..e1634ae 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java @@ -88,10 +88,13 @@ public final class CodecCatalog { public static List audioCodecChoicesForUi(CastSettings settings) { List out = new ArrayList<>(); + boolean opus = NativeCodecBridge.isOpusAvailable(); + boolean speex = NativeCodecBridge.isSpeexAvailable(); out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true, 0)); out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings)); - out.add(scoredAudio(CastSettings.AudioCodec.OPUS, true, settings)); - out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, true, settings)); + out.add(scoredAudio(CastSettings.AudioCodec.OPUS, opus, settings)); + out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, speex, settings)); + out.add(scoredAudio(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, true, settings)); sortAudioChoices(out); return out; } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8e6c421..bb6fa0b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -226,12 +226,13 @@ Перезагрузить codecs.json Подгружает overrides из assets/codecs.json без перезапуска. Откройте настройки снова, чтобы обновить списки. · %1$d - Кодек для звука + Аудио кодек libvpx (VP8) libvpx (VP9) AAC Speex Opus + Passthrough (отладка) AUTO (AAC) Картинка в картинке (PiP) Режим «картинка в картинке» (PiP) недоступен на этом устройстве diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 131ad8b..2fcfc70 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -318,12 +318,13 @@ Download failed: %1$s App updates · %1$d - Voice codec + Audio codec libvpx (VP8) libvpx (VP9) AAC Speex Opus + Passthrough (debug) AUTO (AAC) Picture-in-picture Picture-in-picture is not available on this device diff --git a/docs/GRAFANA_vs_others_graphvis_pivot.md b/docs/GRAFANA_vs_others_graphvis_pivot.md index 81e1d6c..c0e6c84 100644 --- a/docs/GRAFANA_vs_others_graphvis_pivot.md +++ b/docs/GRAFANA_vs_others_graphvis_pivot.md @@ -79,4 +79,4 @@ _Assumptions: current MariaDB crash/ticket/session structures are available; no ## Source linkage - This markdown file is the editable source for: - - `tmp/GRAFANA_vs_others_graphvis_pivot.pdf` + - `docs/GRAFANA_vs_others_graphvis_pivot.pdf` diff --git a/docs/GRAFANA_vs_others_graphvis_pivot.pdf b/docs/GRAFANA_vs_others_graphvis_pivot.pdf index 186a9db..027af76 100644 --- a/docs/GRAFANA_vs_others_graphvis_pivot.pdf +++ b/docs/GRAFANA_vs_others_graphvis_pivot.pdf @@ -42,7 +42,7 @@ endobj endobj 7 0 obj << -/Author (Android Cast project) /CreationDate (D:20260602143110+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260602143110+02'00') /Producer (ReportLab PDF Library - \(opensource\)) +/Author (Android Cast project) /CreationDate (D:20260602144833+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260602144833+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Subject (\(unspecified\)) /Title (Grafana vs Others GraphVis Pivot) /Trapped /False >> endobj @@ -53,17 +53,17 @@ endobj endobj 9 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 2837 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2776 >> stream -Gb!Sm=``=W&q9SYkXT2.;7oK>:J0snZUsN9b1&P`.Ku#tKU>gH;RY`>DrVrcGt_/)h0mq?'?_X>5bHrcJ/kf`a-"-#*8pJkVJdJo>Z`PnnLYgu+5qTeo&I[9P!NX3Xr"Y\Ks;A;JWKDAfWo#=$TtZ+=_L;[=Ae#%HjAre$*NN[D/d<2[rB0?`>5U,2@(EZ8+8AiRO0%m+/XkeQ6)a'&^?CBnIo01+pNF])94'muO$\qHDHXB=um[j8MZ7660tKTn:rd^I;#U,9#T75Iuk1"9c(-b"cX'M(NqD8R!(FgaE-]GF_b+3-_DI=X)V;!g5-8+$MONCb;U\JU@R\)Q&._#tip$Hq=G;=W,WfMBXlE^:cgJ*L2,nA=-K=k=a%GJGPnhl[HaX-7l`e0/!8M-uD$17G%d+k[NLgq^4J^bN-EOK>:*ZF1J6[WA%p/ljKmObO?^VBoO&%4=(5F.\a/+]O&3b+bR?c+p>_QrFg0QM/4K>t]]`;12_+L-sD[VTe#ir?[ab?f.0F:3kaBC,'hS?!?)$^rs/]L*EY[1jLs,G;Xk3s#g=iiSVdi9l'`F,nP"4ppWl2S<`Q@U+#rsnc/=s.s<-Z=*jdioa[Z)mS0,4]:ojt)-k@YK7Uqlp7C368C'U(-ME5]h,&JQ'(6%AK7Ru#i#`C&]7E=:bk@&Q;uRKKk+P`B9-9NFZ<2YsKoa("#R$9&"F_??oSsbt6YHCYc:3ftk79C`)n)HUgH6kVFr9'6G^mL,K^k25`(#W/,XL0o]1s^jh)J8BEgLdu0QNX[.#VmLC^I1oKG_ZNdLAQ8ERRHN'eJAX"EWY[W;3QGW=f`KX:RC[&9>.AFgSELIT,)LbfR>lj\L$)GO_&KN93]4AueMD\.S=6\bCCCgcGHKs8eg54?X9jtFAR[%d=L0N_F,nb0P['>_r-]l*jq$7]J\`D-mi9\[PE)p@_=7<'f$4+[O6G!%5]X9cY\Bu=llBUH"#itWt49O[\'"i`_horVorP;SX,B(6_<11P]kjgU@h5%[%$W#f.Mg)#9?Ob.14?IA(G.kmD!`&^.$\m1PDK-cnm=?Y;HLapQP#SDQ:(-%\S)GK*9U\rWW#*q0FonFb,RM)Zbf"HL$n[R.^Sg&,rhWbl=pl$4o.9[cdWZNfR1%"G1FO0MiLPkrd1#R%7n$BlT"DnIY#-J,m+=-1W@dfYK/!^0,&er.U']CBO"lji&JVFr9BFo,=I#FR8#_q44;^k>9(-QAo:/e`Tqm*qgV.:VZD]SjVPnENY:D470=Dt3g=++4ID[Ea&QF^_recDH\&Bu.#,u64OAM&4q\qM?lou-p8f553?IlF\D#akaC>@I-A)3@LpnmnBCdOLH^NK/]q#5>4u!G;4^BG?.kcjo*SKEn6O,qo$8';A%7?aP&:n9th"ZB;\6jMg72nt/jFoRL,0++6M16]2,3A;(oS4.4CRiYaFu,8B2_/]b&`_;?;5nkqBXr\3=Zj34a*?g1g]=ti6CcG[30(E,394MWo`DHVT@OtVBIUq.ES%OJ$L`qIWR3)4]A4"co_Ro]@mDNTj.\cT5hs`k5+M-lj1H*Ee'%cZT..mL^/SkQ[BP%BrN_Om/c=OJd',PF+)$Cn47O1)2]dK(Ni%^HOPA3S-h+mtOS?fq*m]Q!g&DN.g(3cJp$9V['Q)7ZY=A4,Hl36I[XCo4:!hr,%%k`/:cZRJZM\7bf`:(H]lnaF2!AjC*IhE&G62KC1nPendstream +Gat=-gNV@2'7\>80opEDN%JCNSnO:1p\WYi:$A0sa>lE#(8-Yl?-=R's8!bXp]>6#b970(,L;>2qAWjnN\AhcSu"r&e($"mo;(s%%?@Qag7A``0rkbs5)J?AGRhp%1s[ttE.u%UKDBH,<5,ef`cg)P:%>Pts*ad+lQ"u$]4^Uj]n:_&e%RC`]ZJ`_j?Kp:^j'+O:04(SLB/iV[pWUB0_ijp(j,W"+L\0%[W)f?5A?Lk4jMd+AqT9='%l/LNQ21!ti!V;i*NAFEn@31950*+eJ"408#k]P3N7D6CXgCN6r$"TNQKP;'iFKpP)fm$EebPH/u7_Z.?tPgeA_0->KMBT[?G.'f*.k_M$d_>ZV&5K1ce2uPd8jU+Z7AlR>1>cj0N^%6'hF"1Xj/G:75P5L+LS\b/gq-;!M]4.i9UMLokXK\oEi9);B=7H"^,?AE]SAd;o7%^q`QAmWg)T6P+LcA3/,;SM*&OGcAd?gcQkK3\`k?@eBZR&8`c\:_HCG(&_Z70Oh[X.R1r;HLZo8%1Ukl(7R0A$]lX_J9F\rOpE#g\C(2[T?+a*M6*@nR%OCb-#&Y"AaRNs5Vd,lq4>K7L"cJ8LuG^felfW=i__G_@>upn-j[__69B*S$KCe^f0B.\D_!1Vt(.MW7(J:qcld!mQi\ej>,]Xq9-B@>SHf/6'O\,R92\+H52[$7Qdp9)9Ng\3YNneO5/=XjHtfQGt=bSl24I^ni<]YM#,(]L&QP&2'[0nft(Bb-fCCKB5RoCAV`0JO+j"B/pJ)C#V*=E#:&4XuUhBGfUV`L6H8-Yk$i%j2blbBMP".Kmpua/'[0a'=qR\#]V&RV'.l+26Ru>@UI0^ZWp&]$bB5"3s'A)(t+H4R%5sM=a%43[$W3Z4=G'3o.\*N*-G?C'&XDHk$$UbJEsYFg"S,dr&+R\L@>"fc,,"3Le@T1qToKEgU@M6;T/tedBSANrQZ89N3+Q=(Yi_64j=+m>I5gg8IqiTReLVouPOQKD/,P#3n?fG`W+oG'($p+g6L8_6CK_2l@ROK9[(_@T*=)=,!KW+,*5pYrt(Yr>5F.-%@3DSQkk73WgpTPmfqF`=t2heO((^^kau69`FhYhg7qUHC)Q!cqM_'\nhU*5uj6\i-BYAf%Lg/`(2g!sNnO&N?s%9Hi2ja'tudE=J`_UO[7IAgMI$EBrO/Tu'B:QE!RgJc2:S#rr7K=K?kA-!mGL!ia*"5D/n[%$gYS0m67Pj5neGFTrp7O'`\@e2HTKY(6^5,Bemi?K\A=kE>Uc9s_K^fki"CZ"jE/DZ^2*\iCVD6\.H3p^FW,Vs$aLn29BKkNaZXb[XUef!iR9S=G5m.JspFa#H2>jP3m5o\JY"QpC>0E$0D^VZXi2ZgSg4pC/G?``$daH+AKBpl(*pTZS^8ghfAu~>endstream endobj 10 0 obj << -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1169 +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1790 >> stream -Gat=)gN)=4%"7kOi%_jCBo/!$3IT%]]=k#H\bIm4N'ob_PphonS@W'7OKrCW'1^N3`qIJ5!"M9P0SBFud!t3n^src-^P!.r=KNa*8IVY(pp-@e2%00",UEJN(kn2c,`.[$rQlO/Zein[69#+m?r^W#MP49?8mSs5KYW5]l`(Sti)\D9>#bh;]=QOSf?.4?V6]4sXM2U;ZcdiZc[-W52W?D_NVR&GP)$aQ?W5l2oK;+!.83[MWBo&M0Uo5'2A_bHG#>V@.,25S)l/T@YAeM9')>k/mNXm/u43qJAiMHbWd9DOVs^kdjulbH`H]gJpEh,!6b4_gc[fNO32:N,i1[B_>+Dp_h+h62$!E:`X*F'V&K2o<@6C1C[XCY6"!@hO@/37bGj_AIJI*EB2^;_p_;Xm$B4EHF&q6P1NR@f$HF^ohc+P0F\(ERrW+0I6"U#M&-/CD-F>`n8J?qWsk(fN@#mfOCI!<6nB)1akT2mpI[.If?W97N\loGX58EKVJ<8@W25!_L=""A"X%.#EIB9,=<@O>NZS3-:CLo:l=]E(mPg>f%If[".g^%-jE*'>05H5%"e;J+m:&FC=flil9Ge-Yi536Xf5eL[LF?F?U*`:>H5I4^-6l)Eu$V!=?Ug9a9g@E8.AfF",u?LshMU;0E6EAT^""NX*PFa9V(;c9Ih,QM&2I&nRnHP="DLR(2#3&e%t'\$S"Z0AJlHGYq&of2@umC3hXVMYk.MJS)\Y(nKTtLKtZ!]Ek\-Zf$"&r;s(%#'iU_8/Foq[cqqPmg.h169@&<;/c(@qgH2/,kKCQ5=Ig)E7"ge4mZ>[Vc-*SBJ+[^JZHYGhRr/oVj+C5^-GdO;CBL2Q;UL6mhsB6&,T*`=?Jm>*r=a`dQ#NY7dU[H9g=D<@keA(.XhY]GEM_V`GlubcD(n1G,Sr'@Yag2pusAc9Ao6m2:T-\Gk0DKEh$N\P*j_Ihsp;d7P^FP8I06a+26#k!j@6&tUToMN9i^,$9Q#C95cFq+-endstream +Gatm<>Bed\&:Vs/R)cS5NFhpS53:40GIMpfVEF!0Ym""j$S0Drm'#`oA7c<#H%O*!Hd-lZs4-<0a$9!kG-]GhNI5gkU%]n%IK]!$**CV4X$f@,bln)]3f6U%#m@.Q(3"L2If+3/s+>.e_;fJrs`=VK)Cj:/R@6K.f4[ug5'&#SeY^\QB8CP24U6M%X]cFG0jf6KS.o$rih.XI@Lap=')EGiSi5fJlaVO-'m7n(L7qKGk/!3N&6K45crt'1lQAbSX>/,aFru6rj7!;,)*uDX11Sl3n+Nj=sr]4),:T0Gj_j=M`>?[AqYq[TgW&A+`h5gi^rq`iJ'<-3P*c@_"[R24*!^7-a:[\\"IF#R^f.b]JatdcBIS6:"H\^)g#[a_hN#FmKRIuf:CE5H7/4!^d#cc(;WiC,=*n<.;T)rr)@LdlB.G4SN<65No)8$=L!f=keBqe#dK<%S_WsYI"`L\kl]`VDL*4)dbYU'sNrq/Fk-!?f0$?GjRP\[L@9BBV8R_4f\#YmWiVe%q4Jt6c(pS-XV>I)FnWm.^1:BKja4%FfF4+o/JSqVq*_>`@H@j0[d$#['G!a@+:tl=(dc9([l8M5]aNk&i8Y\fVpqbCYnUY+5$5!p*-N"@X4P.A#?`\%$>D!b:FqhjUJ:D^==q-/E_j#._gVRfbmH0?I(ZM9r=RjX(_#F#HQ/d^>e7Ceg;5/V)l:A'-pF*3]gDagierGWnJp:,Icugn^2KG^IHhlffJ2?L7\^%b0YJ@[Q:0$o3b"JP&ZGGOL>@6$&u6^+U.DG+T6J>P3JcsXbDAE(-7mD1@87km6o)0oN`iCD%]NUDb0NM!LC"rOHBY5q_O4BLS>eB,f77.BG9%i.a;shSW'u&.RkomQnGq+;DiNql4FtWeiAn?-arSSdeet"+SaUYX_d&3e]ou].;VQ'G[Ac:[=-TjOX/WhqRPYY9Um"X4`>S5MSjMJ)oih>k7Cj`C!uPWi"KJWGL*VU'TXiG"$6"&,[3\pP'brr.+DaFWY!r@h6b:$85G@Af#5uuYJZUhVpi6Tc?6$I@o:BPVTs"AjQ;6lU[1;FY@=!/r?auN6^G5'WToSA?E7>e&X7Qf[C(:/CYNg%p9Y/bbdS5os[[Kn<2OW'!Vq,sBm9gSI%7rq%Z+#A:hR&2[nLpgIF2e~>endstream endobj xref 0 11 @@ -77,11 +77,11 @@ xref 0000000796 00000 n 0000001102 00000 n 0000001167 00000 n -0000004095 00000 n +0000004034 00000 n trailer << /ID -[<8697b2018a3d689f70795c513a06ba7d><8697b2018a3d689f70795c513a06ba7d>] +[] % ReportLab generated PDF document -- digest (opensource) /Info 7 0 R @@ -89,5 +89,5 @@ trailer /Size 11 >> startxref -5356 +5916 %%EOF diff --git a/docs/_pdf_build/build_graphvis_pivot_pdf.py b/docs/_pdf_build/build_graphvis_pivot_pdf.py index 7e21a32..a909957 100644 --- a/docs/_pdf_build/build_graphvis_pivot_pdf.py +++ b/docs/_pdf_build/build_graphvis_pivot_pdf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build tmp/GRAFANA_vs_others_graphvis_pivot.pdf decision memo.""" +"""Build docs/GRAFANA_vs_others_graphvis_pivot.pdf decision memo.""" from __future__ import annotations @@ -13,16 +13,26 @@ from reportlab.lib.units import cm from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle ROOT = Path(__file__).resolve().parents[2] -OUT_PDF = ROOT / "tmp" / "GRAFANA_vs_others_graphvis_pivot.pdf" -SOURCE_MD = ROOT / "tmp" / "GRAFANA_vs_others_graphvis_pivot.md" +OUT_PDF = ROOT / "docs" / "GRAFANA_vs_others_graphvis_pivot.pdf" +SOURCE_MD = ROOT / "docs" / "GRAFANA_vs_others_graphvis_pivot.md" def p(text: str, style) -> Paragraph: return Paragraph(text.replace("\n", "
"), style) +def _cell(text: str, style: ParagraphStyle) -> Paragraph: + safe = (text or "").replace("&", "&").replace("<", "<").replace(">", ">") + return Paragraph(safe.replace("\n", "
"), style) + + def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> None: - data = [headers] + rows + styles = getSampleStyleSheet() + head = ParagraphStyle("TblHead", parent=styles["Normal"], fontName="Helvetica-Bold", fontSize=9, leading=11) + body = ParagraphStyle("TblBody", parent=styles["Normal"], fontSize=9, leading=11) + data = [[_cell(h, head) for h in headers]] + for row in rows: + data.append([_cell(c, body) for c in row]) t = Table(data, colWidths=col_widths) t.setStyle( TableStyle( @@ -33,6 +43,7 @@ def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> N ("FONTSIZE", (0, 0), (-1, -1), 9), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey), + ("WORDWRAP", (0, 0), (-1, -1), "CJK"), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]), ("LEFTPADDING", (0, 0), (-1, -1), 6), ("RIGHTPADDING", (0, 0), (-1, -1), 6), @@ -71,7 +82,7 @@ def build_pdf() -> None: ["Context", "apps.f0xx.org + BE Alpine (nginx + php-fpm + MariaDB)"], ["Constraint", "No cron ETL/data copy/pipeline complexity"], ["Output", "Decision memo: Grafana vs custom PHP dashboards"], - ["Markdown source", str(SOURCE_MD.relative_to(ROOT))], + ["Markdown source", "docs/GRAFANA_vs_others_graphvis_pivot.md"], ], [4.2 * cm, 12.2 * cm], ) diff --git a/examples/app_hub/index.html b/examples/app_hub/index.html index d1f397d..55f0ae7 100644 --- a/examples/app_hub/index.html +++ b/examples/app_hub/index.html @@ -11,8 +11,15 @@ })(); - - + + + @@ -94,6 +101,12 @@ AndroidCast crashes + + + AndroidCast graphs + +
  • + + + Graphs + +
  • + +
    +
    +

    AndroidCast graphs

    +
    + + +
    +
    +

    Loading…

    +
    +
    +

    User scope

    + +
    +
    +

    Slug-admin scope

    + +
    +
    +

    Platform scope

    + +
    +
    +

    Bitrate (kbps)

    + +
    +
    +

    NTP source mix

    +
    +
    +
    +

    Install source mix

    +
    +
    +
    +

    + APIs: /api/graphs.php and + /api/graph_upload.php +

    +
    diff --git a/scripts/build-graphvis-pdf.sh b/scripts/build-graphvis-pdf.sh index 8daceca..f8b3ef6 100644 --- a/scripts/build-graphvis-pdf.sh +++ b/scripts/build-graphvis-pdf.sh @@ -4,4 +4,4 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" python3 "$ROOT_DIR/docs/_pdf_build/build_graphvis_pivot_pdf.py" -echo "Generated: $ROOT_DIR/tmp/GRAFANA_vs_others_graphvis_pivot.pdf" +echo "Generated: $ROOT_DIR/docs/GRAFANA_vs_others_graphvis_pivot.pdf"