Skip to content

Commit 2d21fac

Browse files
authored
Fix live broadcast privacy promotion (#13)
## Summary - align privacy-only `liveBroadcasts.update` requests with `part=status` - preserve made-for-kids status and exact privacy readback verification - add regression coverage and update the latest release notes ## Root Cause The helper requested `part=status` but also sent `snippet` and `contentDetails` in the resource body. YouTube rejected those extra resource parts with HTTP 400 `unexpectedPart` after the broadcast had already reached healthy `live` state. The supervisor therefore entered cooldown without promoting the existing event from unlisted to public. ## Impact Healthy managed broadcasts can now be promoted to the configured final privacy without creating, completing, or replacing the event. The mutation remains serialized and is still verified through a fresh remote readback before stable state is recorded. ## Validation - focused regression observed failing before the implementation and passing afterward - `pytest -q`: 198 passed - `ruff check .`: passed - production scripts compile with `python3 -m py_compile` - `git diff --check`: passed - Gemini 3.1 Pro high-effort code review: no findings ## Rollback Restore the prior API helper artifact and restart the encoder service. A successful visibility change is intentionally not reverted because public visibility is the configured target; no broadcast identity changes are introduced by this patch. ## Public-Safety Checklist - [x] No OAuth client files, refresh tokens, stream keys, camera credentials, broadcast IDs, or generated runtime state were committed. - [x] Documentation was updated for the user-facing behavior change. - [x] Local tests and independent review passed.
1 parent 2fdc14b commit 2d21fac

4 files changed

Lines changed: 12 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable project changes are recorded here. The README shows only the most recent entry.
44

5+
## 2026-07-11 - Live Visibility Publication Fix
6+
7+
- Corrected privacy-only `liveBroadcasts.update` requests so `part=status` sends only the matching `id` and `status` resource fields, eliminating HTTP 400 `unexpectedPart` failures after the healthy-live gate.
8+
- Preserved made-for-kids status and post-update privacy verification while removing unrelated scheduled-start and monitor-stream fields from publication requests.
9+
- Added regression coverage for the exact part/body contract used to promote an existing live broadcast without creating or replacing it.
10+
511
## 2026-07-11 - Optional Video Telemetry
612

713
- Added an opt-in `videos.list` command and one-shot collector for aggregate live-stream timing, viewer, view, like, and comment metrics.

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -517,10 +517,10 @@ Each eligible collection performs one [`videos.list`](https://developers.google.
517517

518518
Only the most recent changelog entry is shown here. See `CHANGELOG.md` for full history.
519519

520-
### 2026-07-11 - Optional Video Telemetry
520+
### 2026-07-11 - Live Visibility Publication Fix
521521

522-
- Added disabled-by-default, quota-bounded aggregate video telemetry through one read-only `videos.list` call per eligible collection, with private local snapshots, write-ahead throttling, retention, and independent system or user timers.
523-
- Telemetry remains isolated from FFmpeg supervision, broadcast recovery, lifecycle transitions, privacy changes, and retry state.
522+
- Corrected the privacy-only YouTube update payload so a healthy live broadcast can be promoted from unlisted to the configured final visibility without an HTTP 400 `unexpectedPart` failure.
523+
- Publication still preserves the existing broadcast and verifies the remote privacy readback before declaring the stream stable.
524524

525525
## Repository Layout
526526

‎bin/youtube-autoencoder-api‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -921,22 +921,9 @@ def set_broadcast_privacy(broadcast_id: str, privacy: str) -> dict[str, Any]:
921921
current = broadcast_by_id(broadcast_id)
922922
if current is None:
923923
raise ReconciliationError(f"liveBroadcast not found: {broadcast_id}")
924-
snippet = current.get("snippet") or {}
925-
content_details = current.get("contentDetails") or {}
926-
monitor = content_details.get("monitorStream") or {}
927924
status = current.get("status") or {}
928-
scheduled_start = snippet.get("scheduledStartTime")
929-
if not scheduled_start:
930-
raise ReconciliationError("broadcast has no scheduledStartTime required for privacy update")
931925
body = {
932926
"id": broadcast_id,
933-
"snippet": {"scheduledStartTime": scheduled_start},
934-
"contentDetails": {
935-
"monitorStream": {
936-
"enableMonitorStream": bool(monitor.get("enableMonitorStream", True)),
937-
"broadcastStreamDelayMs": int(monitor.get("broadcastStreamDelayMs") or 0),
938-
}
939-
},
940927
"status": {
941928
"privacyStatus": privacy,
942929
"selfDeclaredMadeForKids": bool(status.get("selfDeclaredMadeForKids", False)),

‎tests/test_api.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -741,9 +741,11 @@ def test_list_managed_broadcasts_requires_exact_marker(load_script, monkeypatch)
741741
assert [item["id"] for item in api.list_managed_broadcasts("encoder-1")] == ["broadcast-1"]
742742

743743

744-
def test_set_broadcast_privacy_preserves_required_fields_and_verifies_readback(load_script, monkeypatch):
744+
def test_set_broadcast_privacy_sends_only_status_part_and_verifies_readback(load_script, monkeypatch):
745745
api = load_script("youtube-autoencoder-api", "yta_api_privacy")
746746
current = managed_broadcast(api, "broadcast-1", "live")
747+
current["snippet"].pop("scheduledStartTime")
748+
current["contentDetails"].pop("monitorStream")
747749
public = {
748750
**current,
749751
"status": {**current["status"], "privacyStatus": "public"},
@@ -768,8 +770,6 @@ def fake_api(method, path, params, body=None):
768770
"params": {"part": "status"},
769771
"body": {
770772
"id": "broadcast-1",
771-
"snippet": {"scheduledStartTime": "2026-07-10T21:00:00Z"},
772-
"contentDetails": {"monitorStream": {"enableMonitorStream": True, "broadcastStreamDelayMs": 0}},
773773
"status": {"privacyStatus": "public", "selfDeclaredMadeForKids": False},
774774
},
775775
}

0 commit comments

Comments
 (0)