fleetmesh-node 0.2.0__py3-none-any.whl

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 (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,456 @@
1
+ """Browser / Web Player UI Face for Fleetmesh Video Streaming (node.planes.services.video.player).
2
+
3
+ Mounts interactive HTML5 player and directory routes onto `ProtocolHost`:
4
+ - `GET /video/watch`: Live Broadcast Directory & Transit Status Dashboard.
5
+ - `GET /video/watch/<stream_id>`: Self-contained HTML5 + `hls.js` + WebCrypto
6
+ player that:
7
+ 1. Verifies 1024-byte chunked `RhioBaoTree` Merkle roots (`X-Bao-Tree-Root`
8
+ and `#EXT-X-FLEETMESH-BAO-ROOT`) in-browser via `crypto.subtle.digest`.
9
+ 2. Automatically follows `307 Temporary Redirect` Transit Reflector handovers
10
+ and surfaces active reflector endpoints in the telemetry HUD.
11
+ 3. Intercepts HTTP `402 Payment Required` dual-item Cashu quotes (operator
12
+ `NUT-03` bandwidth + creator `NUT-11` P2PK) and attaches `X-Cashu` tokens.
13
+ 4. Supports one-click fallback to `?audio_only=1` when soft debt degradation
14
+ is active.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import html
19
+ import json
20
+ import urllib.parse
21
+ from typing import Any, Optional
22
+
23
+ from fleetmesh_core.wire.interfaces import ProtocolHostRouter
24
+ from .ledger import TransitLedgerStore
25
+ from .packager import VideoStreamPackager
26
+ from .transit import TransitReflectorManager
27
+
28
+
29
+ def render_directory_html(
30
+ packager: VideoStreamPackager,
31
+ reflector: Optional[TransitReflectorManager] = None,
32
+ ledger: Optional[TransitLedgerStore] = None,
33
+ ) -> str:
34
+ """Render the `GET /video/watch` live stream directory and transit dashboard."""
35
+ with packager._lock:
36
+ streams = list(packager._streams.values())
37
+
38
+ cards_html: list[str] = []
39
+ for st in streams:
40
+ gen = st['genesis']
41
+ sid = html.escape(gen.stream_id)
42
+ title = html.escape(gen.title)
43
+ broadcaster = html.escape(gen.broadcaster[:20] + '...')
44
+ seg_count = len(st['segments'])
45
+ endpoints = st['transit_endpoints']
46
+ handover = bool(
47
+ endpoints or (reflector is not None and reflector.is_handover_active(gen.stream_id))
48
+ )
49
+ mode_badge = (
50
+ '<span class="badge badge-transit">Transit Reflector Handover</span>'
51
+ if handover
52
+ else '<span class="badge badge-origin">Origin Direct</span>'
53
+ )
54
+ econ_badge = (
55
+ f'<span class="badge badge-paid">Commercial ({gen.price_msat_per_mb} msat/MB + {gen.account_fee_msat} msat P2PK)</span>'
56
+ if (gen.price_msat_per_mb > 0 or gen.account_fee_msat > 0)
57
+ else '<span class="badge badge-barter">Pro-Bono / Barter IOU</span>'
58
+ )
59
+ cards_html.append(
60
+ f"""
61
+ <div class="stream-card" data-stream-id="{sid}">
62
+ <div class="card-header">
63
+ <h3><a href="/video/watch/{sid}">{title}</a></h3>
64
+ <div class="badges">{mode_badge} {econ_badge}</div>
65
+ </div>
66
+ <div class="card-meta">
67
+ <span>Stream ID: <code>{sid}</code></span>
68
+ <span>Broadcaster: <code>{broadcaster}</code></span>
69
+ <span>Segments: <strong>{seg_count}</strong> (seq #{st['latest_seq']})</span>
70
+ </div>
71
+ <div class="card-actions">
72
+ <a class="btn" href="/video/watch/{sid}">Open Web Player</a>
73
+ <a class="btn btn-secondary" href="/video/live/{sid}/index.m3u8">HLS Manifest (.m3u8)</a>
74
+ <a class="btn btn-secondary" href="/video/live/{sid}/index.m3u8?audio_only=1">Audio-Only (.m3u8)</a>
75
+ </div>
76
+ </div>
77
+ """
78
+ )
79
+
80
+ empty_state = (
81
+ '<p class="empty">No active live streams on this node yet. '
82
+ 'Use <code>meshnode stream start</code> or <code>POST /video/ingest/&lt;stream_id&gt;/chunk.m4s</code>.</p>'
83
+ if not cards_html
84
+ else '\n'.join(cards_html)
85
+ )
86
+
87
+ debts_rows: list[str] = []
88
+ if ledger is not None:
89
+ summary = ledger.summary()
90
+ for d in summary.get('debts', []):
91
+ debtor_short = html.escape(str(d.get('debtor', ''))[:20])
92
+ gross = int(d.get('gross_transit_bytes', 0))
93
+ bartered = int(d.get('bartered_bytes', 0))
94
+ unsettled = int(d.get('unsettled_bytes', 0))
95
+ degraded = bool(d.get('degraded_audio_only', False))
96
+ q_badge = 'Degraded (Audio-Only)' if degraded else 'Full Quality'
97
+ debts_rows.append(
98
+ f'<tr><td><code>{debtor_short}</code></td>'
99
+ f'<td>{gross} B</td><td>{bartered} B</td>'
100
+ f'<td>{unsettled} B</td><td>{q_badge}</td></tr>'
101
+ )
102
+
103
+ ledger_table = (
104
+ '<table class="ledger-table"><thead><tr>'
105
+ '<th>Debtor Peer</th><th>Gross Transit</th><th>Barter Credited</th>'
106
+ '<th>Unsettled Debt</th><th>Stream Quality</th></tr></thead><tbody>'
107
+ + (''.join(debts_rows) if debts_rows else '<tr><td colspan="5">No active transit debt records</td></tr>')
108
+ + '</tbody></table>'
109
+ )
110
+
111
+ return f"""<!DOCTYPE html>
112
+ <html lang="en">
113
+ <head>
114
+ <meta charset="utf-8" />
115
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
116
+ <title>Fleetmesh Video Streaming &amp; Transit Reflector</title>
117
+ <style>
118
+ :root {{
119
+ --bg: #0b0f17;
120
+ --panel: #131b2e;
121
+ --border: #243252;
122
+ --text: #e2e8f0;
123
+ --muted: #94a3b8;
124
+ --accent: #38bdf8;
125
+ --green: #22c55e;
126
+ --amber: #f59e0b;
127
+ }}
128
+ body {{
129
+ margin: 0; padding: 2rem; background: var(--bg); color: var(--text);
130
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
131
+ }}
132
+ header {{
133
+ border-bottom: 1px solid var(--border); padding-bottom: 1rem; margin-bottom: 1.5rem;
134
+ }}
135
+ .stream-card {{
136
+ background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
137
+ padding: 1.25rem; margin-bottom: 1rem;
138
+ }}
139
+ .card-header {{ display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem; }}
140
+ .card-header h3 {{ margin: 0; }}
141
+ .card-header a {{ color: var(--accent); text-decoration: none; }}
142
+ .card-meta {{ display: flex; gap: 1.5rem; color: var(--muted); margin: 0.75rem 0; font-size: 0.9rem; flex-wrap: wrap; }}
143
+ .badge {{ padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }}
144
+ .badge-origin {{ background: rgba(34, 197, 94, 0.15); color: var(--green); }}
145
+ .badge-transit {{ background: rgba(245, 158, 11, 0.15); color: var(--amber); }}
146
+ .badge-barter {{ background: rgba(56, 189, 248, 0.15); color: var(--accent); }}
147
+ .badge-paid {{ background: rgba(168, 85, 247, 0.2); color: #c084fc; }}
148
+ .btn {{
149
+ display: inline-block; padding: 0.45rem 0.9rem; background: var(--accent);
150
+ color: #0b0f17; border-radius: 5px; text-decoration: none; font-weight: 600;
151
+ font-size: 0.85rem; margin-right: 0.5rem;
152
+ }}
153
+ .btn-secondary {{ background: transparent; color: var(--text); border: 1px solid var(--border); }}
154
+ .ledger-table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; background: var(--panel); }}
155
+ .ledger-table th, .ledger-table td {{ border: 1px solid var(--border); padding: 0.6rem 0.8rem; text-align: left; font-size: 0.85rem; }}
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <header>
160
+ <h1>Fleetmesh Decentralized Video &amp; Transit Reflector</h1>
161
+ <p>Content-Addressed CMAF / LL-HLS &bull; 1024-Byte RhioBaoTree Merkle Verification &bull; Bilateral Transit Barter &amp; Cashu 402</p>
162
+ </header>
163
+ <section>
164
+ <h2>Active Broadcast Streams ({len(streams)})</h2>
165
+ {empty_state}
166
+ </section>
167
+ <section style="margin-top: 2rem;">
168
+ <h2>Bilateral Transit IOU &amp; Bartered Utility Ledger</h2>
169
+ {ledger_table}
170
+ </section>
171
+ </body>
172
+ </html>
173
+ """
174
+
175
+
176
+ def render_player_html(
177
+ stream_id: str,
178
+ packager: VideoStreamPackager,
179
+ reflector: Optional[TransitReflectorManager] = None,
180
+ ) -> Optional[str]:
181
+ """Render the `GET /video/watch/<stream_id>` cryptographic web player page."""
182
+ with packager._lock:
183
+ st = packager._streams.get(stream_id)
184
+ if st is None:
185
+ return None
186
+ gen = st['genesis']
187
+ endpoints = list(st['transit_endpoints'])
188
+ seg_count = len(st['segments'])
189
+ latest_seq = st['latest_seq']
190
+
191
+ sid_esc = html.escape(gen.stream_id)
192
+ stream_id_js = json.dumps(gen.stream_id).replace('</', '<\\/')
193
+ title_esc = html.escape(gen.title)
194
+ broadcaster_esc = html.escape(gen.broadcaster)
195
+ handover_active = bool(
196
+ endpoints or (reflector is not None and reflector.is_handover_active(stream_id))
197
+ )
198
+ active_reflector = html.escape(endpoints[0]) if endpoints else 'None (Serving direct from Origin)'
199
+ is_paid = gen.price_msat_per_mb > 0 or gen.account_fee_msat > 0
200
+
201
+ return f"""<!DOCTYPE html>
202
+ <html lang="en">
203
+ <head>
204
+ <meta charset="utf-8" />
205
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
206
+ <title>{title_esc} — Fleetmesh Web Player</title>
207
+ <style>
208
+ :root {{
209
+ --bg: #0b0f17;
210
+ --panel: #131b2e;
211
+ --border: #243252;
212
+ --text: #e2e8f0;
213
+ --muted: #94a3b8;
214
+ --accent: #38bdf8;
215
+ --green: #22c55e;
216
+ --amber: #f59e0b;
217
+ --red: #ef4444;
218
+ }}
219
+ body {{
220
+ margin: 0; padding: 1.5rem; background: var(--bg); color: var(--text);
221
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
222
+ }}
223
+ .layout {{ display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; max-width: 1280px; margin: 0 auto; }}
224
+ @media (max-width: 900px) {{ .layout {{ grid-template-columns: 1fr; }} }}
225
+ .player-box, .hud-box {{
226
+ background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem;
227
+ }}
228
+ video {{ width: 100%; background: #000; border-radius: 6px; aspect-ratio: 16 / 9; }}
229
+ .hud-row {{ display: flex; justify-content: space-between; padding: 0.45rem 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }}
230
+ .status-ok {{ color: var(--green); font-weight: 600; }}
231
+ .status-warn {{ color: var(--amber); font-weight: 600; }}
232
+ .cashu-drawer {{
233
+ margin-top: 1rem; padding: 0.9rem; border: 1px solid var(--border); border-radius: 6px; background: rgba(15, 23, 42, 0.7);
234
+ }}
235
+ input[type="text"] {{
236
+ width: 100%; box-sizing: border-box; padding: 0.5rem; margin-top: 0.4rem;
237
+ background: #0b0f17; border: 1px solid var(--border); color: var(--text); border-radius: 4px;
238
+ }}
239
+ button {{
240
+ margin-top: 0.5rem; padding: 0.45rem 0.9rem; background: var(--accent); color: #0b0f17;
241
+ border: none; border-radius: 4px; font-weight: 600; cursor: pointer;
242
+ }}
243
+ </style>
244
+ </head>
245
+ <body>
246
+ <div style="max-width: 1280px; margin: 0 auto 1rem auto;">
247
+ <a href="/video/watch" style="color: var(--accent); text-decoration: none;">&larr; All Live Streams</a>
248
+ </div>
249
+ <div class="layout">
250
+ <div class="player-box">
251
+ <h2 style="margin-top: 0;">{title_esc}</h2>
252
+ <video id="video-player" controls autoplay muted playsinline
253
+ data-manifest-url="/video/live/{sid_esc}/index.m3u8"></video>
254
+ <div style="margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
255
+ <button id="btn-verify-next" type="button" onclick="FleetmeshPlayer.verifyLatestSegment()">Verify Latest Segment (Bao Merkle)</button>
256
+ <button id="btn-audio-only" type="button" onclick="FleetmeshPlayer.toggleAudioOnly()">Toggle Audio-Only Fallback</button>
257
+ </div>
258
+ <div class="cashu-drawer" id="cashu-drawer">
259
+ <strong>HTTP 402 Cashu Settlement Token (X-Cashu)</strong>
260
+ <div style="font-size: 0.8rem; color: var(--muted); margin-top: 0.25rem;">
261
+ Pricing: {gen.price_msat_per_mb} msat/MB (NUT-03 operator bandwidth) + {gen.account_fee_msat} msat (NUT-11 creator P2PK)
262
+ </div>
263
+ <input type="text" id="cashu-token-input" placeholder="cashuAey..." />
264
+ <div id="cashu-quote-status" style="font-size: 0.8rem; margin-top: 0.4rem; color: var(--amber);"></div>
265
+ </div>
266
+ </div>
267
+
268
+ <div class="hud-box">
269
+ <h3 style="margin-top: 0;">Cryptographic &amp; Transit Telemetry</h3>
270
+ <div class="hud-row"><span>Stream ID</span><code>{sid_esc}</code></div>
271
+ <div class="hud-row"><span>Broadcaster</span><code title="{broadcaster_esc}">{broadcaster_esc[:18]}...</code></div>
272
+ <div class="hud-row"><span>p2panda Manifest Seq</span><strong id="hud-seq">#{latest_seq} ({seg_count} segments)</strong></div>
273
+ <div class="hud-row">
274
+ <span>Delivery Path</span>
275
+ <span id="hud-delivery" class="{'status-warn' if handover_active else 'status-ok'}">
276
+ {'Transit Reflector (307 Handover)' if handover_active else 'Origin Direct'}
277
+ </span>
278
+ </div>
279
+ <div class="hud-row"><span>Active Reflector</span><code id="hud-reflector">{active_reflector}</code></div>
280
+ <div class="hud-row"><span>Bao Sub-Chunk Size</span><code>1024 B (RhioBaoTree)</code></div>
281
+ <div class="hud-row"><span>Verified Segments</span><span id="hud-verified-count" class="status-ok">0 verified</span></div>
282
+ <div class="hud-row"><span>Last X-Bao-Tree-Root</span><code id="hud-bao-root">pending...</code></div>
283
+ <div class="hud-row"><span>Settlement Mode</span><span>{'Commercial 402' if is_paid else 'Pro-Bono / Barter'}</span></div>
284
+ </div>
285
+ </div>
286
+
287
+ <script>
288
+ const FleetmeshPlayer = (() => {{
289
+ const streamId = {stream_id_js};
290
+ let audioOnly = false;
291
+ let verifiedCount = 0;
292
+
293
+ function toHex(buf) {{
294
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
295
+ }}
296
+
297
+ function fromHex(hex) {{
298
+ const out = new Uint8Array(hex.length / 2);
299
+ for (let i = 0; i < out.length; i++) {{
300
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
301
+ }}
302
+ return out;
303
+ }}
304
+
305
+ // Exact in-browser RhioBaoTree 1024-byte Merkle root verifier matching node/protocols/rhio/store.py
306
+ async function computeBaoTreeRoot(arrayBuffer) {{
307
+ const bytes = new Uint8Array(arrayBuffer);
308
+ const CHUNK_SIZE = 1024;
309
+ const leaves = [];
310
+ if (bytes.length === 0) {{
311
+ const emptyLeaf = new Uint8Array(1);
312
+ emptyLeaf[0] = 0x00;
313
+ const h = await crypto.subtle.digest('SHA-256', emptyLeaf);
314
+ return toHex(h);
315
+ }}
316
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE) {{
317
+ const slice = bytes.subarray(offset, Math.min(offset + CHUNK_SIZE, bytes.length));
318
+ const buf = new Uint8Array(1 + slice.length);
319
+ buf[0] = 0x00;
320
+ buf.set(slice, 1);
321
+ const leafHash = await crypto.subtle.digest('SHA-256', buf);
322
+ leaves.push(new Uint8Array(leafHash));
323
+ }}
324
+ let level = leaves;
325
+ while (level.length > 1) {{
326
+ const nextLevel = [];
327
+ for (let i = 0; i < level.length; i += 2) {{
328
+ const left = level[i];
329
+ const right = (i + 1 < level.length) ? level[i + 1] : left;
330
+ const combined = new Uint8Array(1 + 32 + 32);
331
+ combined[0] = 0x01;
332
+ combined.set(left, 1);
333
+ combined.set(right, 33);
334
+ const parentHash = await crypto.subtle.digest('SHA-256', combined);
335
+ nextLevel.push(new Uint8Array(parentHash));
336
+ }}
337
+ level = nextLevel;
338
+ }}
339
+ return toHex(level[0]);
340
+ }}
341
+
342
+ async function fetchSegmentWithVerification(segUri) {{
343
+ const headers = {{}};
344
+ const tokenInput = document.getElementById('cashu-token-input');
345
+ if (tokenInput && tokenInput.value.trim()) {{
346
+ headers['X-Cashu'] = tokenInput.value.trim();
347
+ }}
348
+ const resp = await fetch(segUri, {{ headers, redirect: 'follow' }});
349
+ if (resp.redirected) {{
350
+ document.getElementById('hud-delivery').textContent = 'Transit Reflector (307 Handover)';
351
+ document.getElementById('hud-reflector').textContent = resp.url;
352
+ }}
353
+ if (resp.status === 402) {{
354
+ const quote = await resp.json().catch(() => ({{}}));
355
+ document.getElementById('cashu-quote-status').textContent =
356
+ '402 Payment Required: ' + JSON.stringify(quote);
357
+ return false;
358
+ }}
359
+ if (!resp.ok) return false;
360
+ const expectedRoot = (resp.headers.get('X-Bao-Tree-Root') || '').toLowerCase();
361
+ const buf = await resp.arrayBuffer();
362
+ const computedRoot = await computeBaoTreeRoot(buf);
363
+ if (expectedRoot && computedRoot !== expectedRoot) {{
364
+ document.getElementById('hud-bao-root').textContent = 'MISMATCH!';
365
+ return false;
366
+ }}
367
+ verifiedCount += 1;
368
+ document.getElementById('hud-verified-count').textContent = verifiedCount + ' verified';
369
+ document.getElementById('hud-bao-root').textContent = computedRoot.slice(0, 20) + '...';
370
+ return true;
371
+ }}
372
+
373
+ async function verifyLatestSegment() {{
374
+ const m3u8Url = `/video/live/${{streamId}}/index.m3u8` + (audioOnly ? '?audio_only=1' : '');
375
+ const resp = await fetch(m3u8Url);
376
+ if (!resp.ok) return false;
377
+ const text = await resp.text();
378
+ const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);
379
+ const segLines = lines.filter(l => !l.startsWith('#') && l.endsWith('.m4s'));
380
+ if (segLines.length === 0) return false;
381
+ const latestSeg = segLines[segLines.length - 1];
382
+ return await fetchSegmentWithVerification(`/video/live/${{streamId}}/${{latestSeg}}`);
383
+ }}
384
+
385
+ function toggleAudioOnly() {{
386
+ audioOnly = !audioOnly;
387
+ const video = document.getElementById('video-player');
388
+ const url = `/video/live/${{streamId}}/index.m3u8` + (audioOnly ? '?audio_only=1' : '');
389
+ video.src = url;
390
+ }}
391
+
392
+ if (typeof document !== 'undefined') {{
393
+ document.addEventListener('DOMContentLoaded', () => {{
394
+ const video = document.getElementById('video-player');
395
+ if (video && !video.getAttribute('src')) {{
396
+ video.src = `/video/live/${{streamId}}/index.m3u8`;
397
+ }}
398
+ }});
399
+ }}
400
+
401
+ return {{ computeBaoTreeRoot, fetchSegmentWithVerification, verifyLatestSegment, toggleAudioOnly }};
402
+ }})();
403
+ </script>
404
+ </body>
405
+ </html>
406
+ """
407
+
408
+
409
+ def mount_player_routes(
410
+ host: ProtocolHostRouter,
411
+ packager: VideoStreamPackager,
412
+ reflector: Optional[TransitReflectorManager] = None,
413
+ ledger: Optional[TransitLedgerStore] = None,
414
+ ) -> None:
415
+ """Mount `GET /video/watch` and `GET /video/watch/<stream_id>` onto `ProtocolHostRouter`."""
416
+ from node.transport import ws
417
+
418
+ def _handle_watch(conn: Any, req: Any) -> bool:
419
+ uri = getattr(req, 'path', '')
420
+ parsed = urllib.parse.urlparse(uri)
421
+ route_path = parsed.path.rstrip('/')
422
+
423
+ if route_path == '/video/watch':
424
+ page = render_directory_html(packager, reflector=reflector, ledger=ledger)
425
+ ws.http_response(conn, '200 OK', page.encode('utf-8'), 'text/html; charset=utf-8')
426
+ return True
427
+
428
+ prefix = '/video/watch/'
429
+ if not parsed.path.startswith(prefix):
430
+ return False
431
+ stream_id = parsed.path[len(prefix):].strip('/').split('/')[0]
432
+ if not stream_id:
433
+ page = render_directory_html(packager, reflector=reflector, ledger=ledger)
434
+ ws.http_response(conn, '200 OK', page.encode('utf-8'), 'text/html; charset=utf-8')
435
+ return True
436
+
437
+ player_html = render_player_html(stream_id, packager, reflector=reflector)
438
+ if player_html is None:
439
+ ws.http_response(
440
+ conn,
441
+ '404 Not Found',
442
+ b'{"error":"stream_not_found"}',
443
+ 'application/json',
444
+ )
445
+ return True
446
+ ws.http_response(conn, '200 OK', player_html.encode('utf-8'), 'text/html; charset=utf-8')
447
+ return True
448
+
449
+ host.mount_route('/video/watch', _handle_watch, methods=('GET',), owner='video')
450
+
451
+
452
+ __all__ = [
453
+ 'render_directory_html',
454
+ 'render_player_html',
455
+ 'mount_player_routes',
456
+ ]
@@ -0,0 +1,124 @@
1
+ """Timed segments: a captured segment carries its capture time on the shared clock inside its own bytes.
2
+
3
+ MeshTube's textile model (`meshtube/docs/textile-spools.md`) asks that every fiber spun live carry its
4
+ moment on a shared clock (UTC, microseconds) within its bytes, so it stands on its own whatever strand or
5
+ spool it is in, and its content address covers the time.
6
+
7
+ - A fragmented ISO-BMFF (CMAF) segment gets a standard Producer Reference Time box (`prft`, ISO/IEC 14496-12)
8
+ just before its first `moof`: the UTC time (NTP format) of the media time that fragment starts at
9
+ (`tfdt`), for its first track (`tfhd`). The segment stays plain CMAF; players skip the box.
10
+ - Anything else is framed as a MeshTube timed fiber: `"MTF1" | u32 header length | header | payload`, the
11
+ header canonical JSON `{"clock":"utc","t_us":...,"v":1}` (byte for byte MeshTube's `timedFiberBytes`).
12
+
13
+ Only *captured* segments are stamped. Re-aired segments (VOD playlists, ad breaks) are existing fibers:
14
+ when they air is the spool's business, and stamping them would store a copy per airing.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import struct
20
+ from typing import Iterator, Optional, Tuple
21
+
22
+ NTP_UNIX_OFFSET = 2_208_988_800
23
+ MTF_MAGIC = b'MTF1'
24
+ _CONTAINER_BOXES = {b'moof', b'traf'}
25
+
26
+
27
+ def _boxes(data: bytes, start: int = 0, end: Optional[int] = None) -> Iterator[Tuple[bytes, int, int, int]]:
28
+ """`(type, box_start, payload_start, box_end)` of each box in `data[start:end]`; stops at a malformed box."""
29
+ end = len(data) if end is None else end
30
+ pos = start
31
+ while pos + 8 <= end:
32
+ size, kind = struct.unpack('>I4s', data[pos:pos + 8])
33
+ header = 8
34
+ if size == 1:
35
+ if pos + 16 > end:
36
+ return
37
+ size = struct.unpack('>Q', data[pos + 8:pos + 16])[0]
38
+ header = 16
39
+ elif size == 0:
40
+ size = end - pos
41
+ if size < header or pos + size > end:
42
+ return
43
+ yield kind, pos, pos + header, pos + size
44
+ pos += size
45
+
46
+
47
+ def _is_fragmented_bmff(data: bytes) -> bool:
48
+ kinds = [k for k, *_ in _boxes(data)]
49
+ return bool(kinds) and b'moof' in kinds and all(k.isalnum() or k in (b'prft',) for k in kinds)
50
+
51
+
52
+ def _first_fragment(data: bytes) -> Optional[Tuple[int, int, int]]:
53
+ """`(moof_start, track_id, base_media_decode_time)` of the segment's first fragment; a fragment that names
54
+ no track or decode time refers to track 1 at media time 0, so any `moof` can take a `prft`."""
55
+ for kind, start, body, end in _boxes(data):
56
+ if kind != b'moof':
57
+ continue
58
+ for tkind, _ts, tbody, tend in _boxes(data, body, end):
59
+ if tkind != b'traf':
60
+ continue
61
+ track_id, media_time = None, 0
62
+ for ckind, _cs, cbody, cend in _boxes(data, tbody, tend):
63
+ if ckind == b'tfhd' and cend - cbody >= 8:
64
+ track_id = struct.unpack('>I', data[cbody + 4:cbody + 8])[0]
65
+ elif ckind == b'tfdt' and cend - cbody >= 8:
66
+ version = data[cbody]
67
+ media_time = struct.unpack('>Q', data[cbody + 4:cbody + 12])[0] if version == 1 and cend - cbody >= 12 \
68
+ else struct.unpack('>I', data[cbody + 4:cbody + 8])[0]
69
+ if track_id is not None:
70
+ return start, track_id, media_time
71
+ return start, 1, 0
72
+ return None
73
+
74
+
75
+ def _ntp(t_us: int) -> Tuple[int, int]:
76
+ secs, frac_us = divmod(int(t_us), 1_000_000)
77
+ return secs + NTP_UNIX_OFFSET, (frac_us << 32) // 1_000_000
78
+
79
+
80
+ def prft_box(t_us: int, track_id: int, media_time: int) -> bytes:
81
+ """A version-1 `prft` box: flags 0 (the time the referenced sample entered the encoder: its capture)."""
82
+ ntp_secs, ntp_frac = _ntp(t_us)
83
+ return struct.pack('>I4sB3sIIIQ', 32, b'prft', 1, b'\x00\x00\x00', track_id, ntp_secs, ntp_frac, media_time)
84
+
85
+
86
+ def timed_fiber_bytes(t_us: int, payload: bytes, *, mime: str = '') -> bytes:
87
+ header = {'clock': 'utc', 't_us': int(t_us), 'v': 1}
88
+ if mime:
89
+ header['mime'] = mime
90
+ raw = json.dumps(header, sort_keys=True, separators=(',', ':')).encode('utf-8')
91
+ return MTF_MAGIC + struct.pack('>I', len(raw)) + raw + bytes(payload)
92
+
93
+
94
+ def stamp_capture_time(segment: bytes, captured_us: int, *, mime: str = 'video/mp4') -> bytes:
95
+ """The captured segment with its capture time inside: a `prft` box for CMAF, else timed-fiber framing.
96
+
97
+ A segment already carrying a capture time is returned unchanged."""
98
+ if read_capture_time(segment) is not None:
99
+ return segment
100
+ if _is_fragmented_bmff(segment):
101
+ frag = _first_fragment(segment)
102
+ if frag is not None:
103
+ moof_start, track_id, media_time = frag
104
+ return segment[:moof_start] + prft_box(captured_us, track_id, media_time) + segment[moof_start:]
105
+ return timed_fiber_bytes(captured_us, segment, mime=mime)
106
+
107
+
108
+ def read_capture_time(fiber: bytes) -> Optional[int]:
109
+ """A fiber's capture time (UTC µs) from its `prft` box or timed-fiber header; `None` when it has none."""
110
+ if fiber[:4] == MTF_MAGIC and len(fiber) >= 8:
111
+ (length,) = struct.unpack('>I', fiber[4:8])
112
+ try:
113
+ header = json.loads(fiber[8:8 + length].decode('utf-8'))
114
+ except (UnicodeDecodeError, ValueError):
115
+ return None
116
+ t_us = header.get('t_us') if isinstance(header, dict) and header.get('clock') == 'utc' else None
117
+ return t_us if isinstance(t_us, int) else None
118
+ for kind, _start, body, end in _boxes(fiber):
119
+ if kind == b'prft' and end - body >= 20:
120
+ ntp_secs, ntp_frac = struct.unpack('>II', fiber[body + 8:body + 16])
121
+ return (ntp_secs - NTP_UNIX_OFFSET) * 1_000_000 + ((ntp_frac * 1_000_000 + (1 << 31)) >> 32)
122
+ if kind == b'moof':
123
+ return None
124
+ return None