1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:38:53 +03:00

snap next steps egress review

This commit is contained in:
Anton Afanasyeu
2026-05-21 19:06:10 +02:00
parent 2e04711c8f
commit 26fbbe5252
10 changed files with 586 additions and 0 deletions

View File

@@ -0,0 +1,383 @@
#!/usr/bin/env python3
"""Build 20260521_next_gen_steps_egress_review.pdf from summary text and diagram PNGs."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import (
Image,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
ROOT = Path(__file__).resolve().parents[2]
BUILD = Path(__file__).resolve().parent
OUT_PDF = ROOT / "20260521_next_gen_steps_egress_review.pdf"
MM = [
("diagram_current_arch.mmd", "diagram_current_arch.png", "Current sender path (1 encode, N unicast sends)"),
("diagram_multicast_proposal.mmd", "diagram_multicast_proposal.png", "Proposed multicast setup (handshake then one media flow)"),
]
def render_mermaid() -> None:
for mmd, png, _ in MM:
src = BUILD / mmd
dst = BUILD / png
subprocess.run(
[
"npx",
"--yes",
"@mermaid-js/mermaid-cli@11.4.0",
"-i",
str(src),
"-o",
str(dst),
"-b",
"white",
"-w",
"1400",
"-H",
"900",
],
cwd=str(ROOT),
check=True,
timeout=180,
)
def p(text: str, style) -> Paragraph:
return Paragraph(text.replace("\n", "<br/>"), style)
def build_pdf() -> None:
styles = getSampleStyleSheet()
title = ParagraphStyle(
"DocTitle",
parent=styles["Title"],
fontSize=18,
spaceAfter=12,
textColor=colors.HexColor("#1565C0"),
)
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=14, spaceAfter=8)
h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=12, spaceBefore=10, spaceAfter=6)
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)
story = []
story.append(p("Android Cast — Next-gen egress review", title))
story.append(
p(
"Summary of sender 1:N behavior, bandwidth model, limitations, and feasibility "
"of single-stream multicast plus multi-tier (full / 720p / 480p) renditions. "
"Generated 2026-05-21.",
small,
)
)
story.append(Spacer(1, 0.4 * cm))
# --- Current state ---
story.append(p("1. Current sender behavior", h1))
story.append(
p(
"<b>How many receivers?</b> Up to 5 when MULTI_RECEIVER_ENABLED "
"(CastConfig.MAX_RECEIVERS). Sender opens one CastSession per selected device; "
"failed handshakes are skipped if at least one peer connects.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>CPU / GPU / memory:</b> Capture and encode run once per cast. "
"CastFanoutPump duplicates each encoded frame to every session. "
"Network send and per-peer UDP/FEC state scale with N; encode cost does not.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Bandwidth:</b> Per-receiver unicast duplicate of the same bitstream — not IP multicast. "
"Rough LAN egress ≈ (video + ~96 kbps audio) × N, plus per-peer protocol overhead.",
body,
)
)
story.append(Spacer(1, 0.5 * cm))
img1 = BUILD / MM[0][1]
if img1.exists():
story.append(p(MM[0][2], h2))
iw = 16 * cm
story.append(Image(str(img1), width=iw, height=iw * 0.45))
story.append(Spacer(1, 0.4 * cm))
# Table current limits
story.append(p("1.1 Current limitations (sender-focused)", h2))
limits = [
["Area", "Limitation"],
["Primary peer", "First connected peer sets negotiated MIME, adaptive tuning, and network feedback."],
["Codec", "Single encoder; all peers must tolerate the same bitstream."],
["Send path", "One CastFanoutPump thread; serial fan-out to N sessions."],
["WiFi", "Uplink often saturates before CPU on 35 receivers."],
["Topology", "Sender dials out; no shared LAN broadcast stream today."],
]
t = Table(limits, colWidths=[3.2 * cm, 13.3 * cm])
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t)
story.append(PageBreak())
# --- Proposal 1 ---
story.append(p("2. Proposal: single outgoing stream (multicast)", h1))
story.append(
p(
"<b>Goal:</b> One encoded stream on the air; only handshake-authorized receivers consume it; "
"save ~(N1) × bitrate vs todays unicast fan-out.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Feasible</b> with UDP IP multicast + unicast handshake (group address, port, sessionId, PIN). "
"Handshake-only targeting without multicast does <b>not</b> reduce bandwidth. "
"TCP/QUIC cannot deliver true 1× WiFi airtime.",
body,
)
)
story.append(Spacer(1, 0.3 * cm))
img2 = BUILD / MM[1][1]
if img2.exists():
story.append(p(MM[1][2], h2))
iw = 16 * cm
story.append(Image(str(img2), width=iw, height=iw * 0.55))
story.append(Spacer(1, 0.4 * cm))
story.append(p("2.1 Approach comparison", h2))
approaches = [
["Approach", "1× sender airtime?", "Notes"],
["Unicast fan-out (today)", "No", "Same encode; N × send()."],
["UDP multicast", "Yes on good LAN", "MulticastLock; AP client isolation risk."],
["Sender server, N TCP clients", "No", "Flip connect direction; still N wire copies."],
["LAN relay receiver", "Sender 1×", "Relay pays duplicate bandwidth."],
]
t2 = Table(approaches, colWidths=[4.5 * cm, 3.2 * cm, 8.8 * cm])
t2.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t2)
story.append(Spacer(1, 0.3 * cm))
story.append(
p(
"<b>Protocol notes:</b> CastFramingContext.sessionId and CastSessionGate already support "
"session binding. FEC/NACK must become group-scoped (one retransmit cache, resend to multicast). "
"Existing code paths: MultiCastCoordinator, CastFanoutPump, UdpCastTransport.",
body,
)
)
story.append(PageBreak())
# --- Proposal 2 ---
story.append(p("3. Proposal: multi-tier streams (e.g. full + 720p + 480p)", h1))
story.append(
p(
"<b>Goal:</b> Several encoded renditions from one capture; each receiver picks a tier at handshake "
"(e.g. full quality vs 480p for bandwidth-limited devices).",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Feasible</b> via multiple encoders fed from one capture (EGL / scaled Surfaces into separate "
"MediaCodec instances). Cost scales with <b>active</b> tiers (~23 HW encoders typical phone limit; "
"3× software VP9 is heavy). Audio usually stays one AAC stream.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Bandwidth:</b> Unicast per tier → egress ≈ sum of each peers tier bitrate. "
"Multicast per tier → one group per tier; airtime ≈ sum of tiers with ≥1 subscriber.",
body,
)
)
story.append(Spacer(1, 0.3 * cm))
story.append(p("3.1 Tier architecture options", h2))
tiers = [
["Option", "Verdict"],
["Multiple HW encoders from one capture", "Recommended v1."],
["One encode + CPU re-encode", "Too expensive for live cast."],
["SVC / simulcast in one bitstream", "Poor Android HW support; defer."],
["Receiver-only downscale", "Saves sender encode, not downlink bandwidth."],
]
t3 = Table(tiers, colWidths=[6 * cm, 10.5 * cm])
t3.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t3)
story.append(PageBreak())
# --- Rollout ---
story.append(p("4. Recommended rollout", h1))
rollout = [
["Phase", "Deliverable", "Benefit"],
[
"A — Tier selection (unicast)",
"Handshake tier; 720p + 480p encoders; send chosen tier per peer",
"Per-device quality without multicast; no AP surprises",
],
[
"B — Multicast one tier",
"UDP multicast publish after unicast handshake",
"~(N1) egress for same-quality 1:N",
],
[
"C — Multicast per tier",
"Separate multicast group per tier; lazy encoder start",
"Combines (1) and (2) on good LANs",
],
]
t4 = Table(rollout, colWidths=[3.5 * cm, 7.5 * cm, 5.5 * cm])
t4.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t4)
story.append(Spacer(1, 0.4 * cm))
story.append(p("4.1 Summary judgment", h2))
summary = [
["Idea", "Feasible?", "Main win", "Main cost"],
[
"(1) Multicast + handshake gate",
"Yes (UDP)",
"~(N1) uplink on good LAN",
"Transport refactor; FEC/NACK; AP testing",
],
["(1) Unicast only", "Trivial", "Auth only", "No bandwidth win"],
["(2) Multi-tier encode", "Yes", "Per-receiver quality", "23× encode resources"],
["(1) + (2) combined", "Yes", "Multicast per tier", "Highest complexity"],
]
t5 = Table(summary, colWidths=[4.2 * cm, 2.2 * cm, 4.5 * cm, 4.6 * cm])
t5.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t5)
story.append(Spacer(1, 0.5 * cm))
story.append(
p(
"<b>Key code paths today:</b> CastConfig.MAX_RECEIVERS, MultiCastCoordinator, "
"CastFanoutPump, ScreenCastService (primary peer), UdpCastTransport, CastFramingContext, "
"CastSessionGate.",
small,
)
)
doc = SimpleDocTemplate(
str(OUT_PDF),
pagesize=A4,
leftMargin=1.8 * cm,
rightMargin=1.8 * cm,
topMargin=1.5 * cm,
bottomMargin=1.5 * cm,
title="Android Cast — Next-gen egress review",
author="Android Cast project",
)
doc.build(story)
print(f"Wrote {OUT_PDF}")
def main() -> int:
render_mermaid()
build_pdf()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,15 @@
flowchart LR
subgraph once [Once per cast]
Cap[MediaProjection capture]
Enc[Video and audio encoders]
Q[CastFanoutPump queues]
end
subgraph perPeer [Per receiver today]
S1[CastSession 1 UDP or TCP]
S2[CastSession 2]
SN[CastSession N]
end
Cap --> Enc --> Q
Q -->|same encoded payload| S1
Q --> S2
Q --> SN

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,9 @@
sequenceDiagram
participant S as Sender
participant R1 as Receiver 1
participant R2 as Receiver N
S->>R1: Unicast handshake PIN codec tier
S->>R2: Unicast handshake
Note over S: One encoder one sessionId
S-->>R1: Multicast media packets
S-->>R2: Same packets

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB