A native Cocoa YouTube client for Mac OS X 10.4 Tiger on PowerPC G3, paired with a Python/ffmpeg transcoding proxy that runs on a modern host. The G3 does libmpeg2 decode + GL_APPLE_ycbcr_422 render + CoreAudio playback; it cannot run a modern TLS stack or yt-dlp itself.
The Mac this runs on is imacg3 (600 MHz iMac G3, Rage 128 Pro). The dev host is this laptop (hostname uranium) — "uranium" in user messages means this machine, not a remote.
Edit locally, then:
~/bin/tiger-rsync.sh --exclude=build/ --exclude=.git/ --exclude=docs/ \
--exclude=proxy/ --exclude=libs/ \
/Users/cell/github/cellularmitosis/TigerTube/ imacg3:tmp/TigerTube/
ssh imacg3 "cd tmp/TigerTube && xcodebuild -configuration Debug"
ssh imacg3 "cd tmp/TigerTube && ./run_and_log.sh"
ssh imacg3 "tail -60 ~/tmp/tigertube.log"
- Do not use plain
rsync. Tiger's rsync is from ~2005 and a modern-rsync → old-rsync transfer needs specific wire-protocol and directory-handling flags, or you get empty/wrong transfers with no error. The~/bin/tiger-rsync.shwrapper on this laptop bakes them in (rsync --protocol=27 --no-dirs -rlptgoDv "$@"— that's-avexpanded plus the two Tiger-specific flags). Pass any extra flags (--delete,--exclude,--dry-run) as positional args. See theimacg3-devskill for the full environment crib sheet (bash 3.2 under /opt, modern curl with CA bundle, perl 5.36, etc.). - Reach for the
leopard-adc-docsskill for Cocoa/ObjC 1.0 questions. Local mirror of Apple's July 2009 ADC Reference Library — the last doc set to cover the pre-ObjC-2.0 world first-class. Useful for: "is this NSFoo method on 10.4?" (availability markers grep), the ObjC 1.0 Language book + Runtime Reference, 1,431 pre-unpacked Apple sample projects, and legacy docs (QuickTime, Carbon) that Apple has since deleted from developer.apple.com. - Always build
Debugfor iteration.run_and_log.shlaunches./build/Debug/TigerTube.app/..., so aReleasebuild looks successful but leaves the user running yesterday's Debug binary. Release is only for producing the GitHub zip. run_and_log.shalready quits any running instance, captures stderr to~/tmp/tigertube.log, and relaunches. Don't open.appdirectly — LaunchServices swallows stderr.tiger-rsync.shpreserves source mtimes (archive mode); if Xcode's dependency tracking doesn't notice a change to any source file (.h,.m, or.c),ssh imacg3 "touch tmp/TigerTube/src/Foo.m"and rebuild.- The proxy runs on uranium (this laptop), advertised over mDNS as
_tigertube-proxy._tcpon port 5002. The client auto-discovers it; there is no hardcoded IP to update. - UI iteration — use
screencaptureon imacg3 and scp the PNG back.ssh imacg3 "screencapture -x /tmp/f.png" && scp imacg3:/tmp/f.png /tmp/f.pngplusRead /tmp/f.pnglets the agent see the current window state directly. Faster than describing alignment issues in prose and catches problems like non-centered labels or truncated popup titles on the first look. - Driving the UI from the agent — what works, what needs a human.
The productive split for QA loops on imacg3/imacg52 is:
- Text input and Return: AppleScript via ssh works reliably.
run_and_log.shleaves the search field as first responder, so immediately after launch:(ssh imacg3 'osascript \ -e "tell application \"System Events\" to tell process \"TigerTube\"" \ -e "set frontmost to true" \ -e "keystroke \"file:/tmp/test.mp4\"" \ -e "key code 36" \ -e "end tell"'key code 36is Return.) This lets the agent type search queries, submit them, and observe the resulting table. - Visual verification:
screencapture -x+scp+Read(as above) is how the agent sees what the app did with the input. - Log verification:
tail -N ~/tmp/tigertube.logafter each step confirms things likesearchAction: fired,file: accepted "…", HTTP status, etc. - Mouse clicks on NSTableView rows: needs a human.
System Events click at {x,y}is unreliable for firing a table row's target/action on Tiger — clicks land on the widget but the single-click action doesn't fire consistently, and the System Events "click element" variant (click text field 1 of window 1) often returnsNSReceiverEvaluationScriptError. So the agent types the query, screenshots the row, then asks the user to click "play." This is a fair split: agent handles the deterministic stuff, user supplies the one click per test case. - Clipboard / Cmd+A: AppleScript
keystroke "a" using command downcan trigger apbslookup warning in the log; harmless but noisy. Relaunching TigerTube is cheaper and gives a clean search field.
- Text input and Return: AppleScript via ssh works reliably.
src/— all Cocoa app sources (.m / .h)main.m,AppController.{h,m}— app entry + search UIYTClient.{h,m}— YouTube Data API v3 client (libcurl + SBJson)TTPlayerWindowController.{h,m}— orchestrates playback, owns the two fetch pthreads and the 30 Hz display timerTTPlayerView.{h,m}— NSOpenGLView, YUV→RGB on the GPUTTVideoDecoder.{h,m}— libmpeg2 wrapper, emits UYVY viampeg2convert_uyvyTTAudioPlayer.{h,m}— Default Output AudioUnit + s16be ring bufferThumbnailCache.{h,m}— background-fetches search result thumbnails
icons/—play.png,pause.png(transport-bar glyphs)SBJson-2.2.3/— vendored JSON (Tiger-compatible; don't replace)libs/{curl,openssl,libmpeg2}/— vendored native deps, built for ppcproxy/tigertube-proxy.py— Flask transcoding proxy (modern host)run_and_log.sh— on imacg3, wrapper around the binary that captures stderrdocs/— design notes and postmortems (see "Feature workflow" below); not shipped
- Obj-C 1.0, manual retain/release (no ARC — Tiger's runtime
predates it). Every
-init…that returns nil must[self release]before returning. NSString* foo— asterisk hugs the type. No multi-decls on one line. K&R braces. These apply to our code; don't reformat vendored libs (SBJson, libmpeg2, libcurl).- Category files are named
Foo+.h/Foo+.m(notFoo+Topic.h). - Build UI programmatically. The nib under
English.lproj/MainMenu.nibis minimal on purpose — don't push UI back into it. - AltiVec is runtime-detected via
mpeg2_accel(MPEG2_ACCEL_DETECT): G3 picks no accel, G4/G5 picks AltiVec. Single ppc binary serves both. -std=c99+ Obj-C,-mmacosx-version-min=10.4,-arch ppc. Don't use 10.5+ APIs (no blocks, no@property, no ARC, noNSApplicationPresentationHideMenuBar— we use Carbon'sSetSystemUIModefor fullscreen).fprintf(stderr, ...)for diagnostics, viewed via~/tmp/tigertube.log. No NSLog for hot paths.
The player is three concurrent actors plus a main-thread display timer:
- Video curl thread →
TTVideoDecoder.feedData:→ libmpeg2 →didDecodeFrame:callback enqueues UYVY into a 3-slot frame queue under a mutex. Decoder blocks onqueueNotFullif the display is behind; this propagates backpressure through TCP to the proxy's ffmpeg. - Audio curl thread →
TTAudioPlayer.feedPCM:→ 256 KB ring buffer.feedPCMbusy-waits (1 ms) when the ring is full. - CoreAudio render callback (real-time thread) drains the ring,
converts s16be → Float32, and advances
samplesOut. - Display timer on main at 30 Hz non-blocking-dequeues one frame,
uploads via
glTexSubImage2DwithGL_YCBCR_422_APPLE, draws a letterboxed quad,flushBuffer.
The A/V clock is the audio sample counter: position = startTime + samplesOut/44100. The decoder paces itself to stay ~40 ms ahead of
that clock. Audio playback does not start until the video decoder has
produced its first frame (otherwise the audio clock advances from 0
while the decoder is still starting up, and when video catches up it
races through N seconds of frames).
stopRequested is the global shutdown signal. Curl write callbacks
return 0 on stopRequested to abort in-flight transfers. The decoder
waits on queueNotFull with a !stopRequested guard and gets
broadcast-woken. TTAudioPlayer.cancel unsticks feedPCM when the
ring is saturated.
- Do not stop the audio unit before the fetch threads exit. If you
stop rendering while the ring is full,
feedPCMbusy-waits forever because nothing drains. The seek path calls[audioPlayer cancel](sets a flag thatfeedPCMchecks) and leaves the unit running until the threads are confirmed dead, thenresets it. - libmpeg2
mpeg2_reset(dec, 1)does not reliably preserve thempeg2_converthook in 0.5.1 — the first frame after reset comes back with adisplay_fbuf->buf[0]pointing into an unmapped page.-[TTVideoDecoder reset]tears down and rebuilds the decoder instead. - Proxy's
-ss 0is a trap. ffmpeg's HLS demuxer (used for YouTube format 301) logs "could not seek to position 0.000" and compensates by skipping the first ~5 s of content.build_video_cmdomits-ssentirely whent==0. - NSSearchField chrome doesn't scale with font size on Tiger. If
you want a bigger search input, use
NSTextFieldwithNSTextFieldSquareBezel. We do. - YouTube Data API thumbnail dimensions are not a source aspect
signal.
default/medium/highalways come back as 120x90 / 320x180 / 480x360 regardless of what the source really is. Don't try to detect pillarbox from them. - Proxy cropdetect is opt-in via
?crop=autoor?crop=W:H:X:Y. The client currently omits the param (fast path). Only wire it in if the double-bars case is a real user complaint — the probe adds 1–2 s to first-frame latency. - Xcode dependency tracking can miss rsync'd sources because
archive-mode rsync preserves source mtimes. Applies to
.h,.m, and.cfiles alike — Xcode reports** BUILD SUCCEEDED **without recompiling and the relaunched binary is yesterday's. If a change doesn't trigger recompilation,ssh imacg3 "touch tmp/TigerTube/src/Foo.m"and rebuild.
Non-trivial features follow a plan → implement → postmortem flow, one Claude session per phase, so no single session has to hold the whole thing in context:
docs/features/<slug>/plan.md— written first, before any code. Goals, files touched, ordered steps, design rationale for non-obvious calls, validation checklist. Self-contained enough that a fresh session can pick it up cold.docs/features/<slug>/postmortem.md— written after the feature lands. What actually shipped vs. the plan, surprises, what to do differently.- When asked to "plan a feature," write
plan.mdin a newdocs/features/<slug>/directory and stop there. Don't start implementing in the same session. - When asked to "implement" a feature with an existing
plan.md, follow the plan; deviate only when the plan is wrong, and note the deviation for the postmortem. - Bug postmortems (not feature-paired) can live at
docs/root or underdocs/postmortems/— not underdocs/features/.
When implementing features unsupervised (i.e. the user isn't watching each step in real time), two conventions make it easy for them to review asynchronously:
- Stash every build. After producing
~/tmp/TigerTube/build/Debug/TigerTube.appon a build machine, also copy it to~/tmp/builds/<feature-slug>/TigerTube.appon whichever machines are appropriate for review. Use the feature's directory slug underdocs/features/. E.g. for the triggered-display feature:~/tmp/builds/triggered-display/TigerTube.app. The~/tmp/builds/directory serves as a per-feature stash that persists across sessions and lets the user flip between different feature branches' binaries without rebuilding. - End-of-session summary. When stopping work after queued tasks,
the last message should include:
- What was actually done (which commits, which behaviours).
- Which stashed builds are ready for review, with machine paths.
- Any assumptions made to proceed unsupervised (especially
[ASSUMPTION]markers from plans that were resolved in code without explicit confirmation). - What's needed from the user: QA on which machines, which questions need a design call, which open plan questions were punted.
- Known issues or rough edges that should block a release.
- "Commit this" and "push this up" both mean commit and push. Don't leave unpushed local commits unless the user explicitly asks you to.
Tagged releases live on GitHub as vX.Y with a TigerTube-X.Y.zip
asset. The zip layout matches existing releases:
TigerTube-X.Y/
TigerTube.app/... (Release build from imacg3)
proxy/tigertube-proxy.py
Steps:
- Commit + push everything to
main. ssh imacg3 "cd tmp/TigerTube && xcodebuild -configuration Release"~/bin/tiger-rsync.sh imacg3:tmp/TigerTube/build/Release/TigerTube.app /tmp/tigertube-release/TigerTube-X.Y/- Copy
proxy/in alongside it. zip -r TigerTube-X.Y.zip TigerTube-X.Ygh release create vX.Y --title "Version X.Y" --notes-file ... --target main TigerTube-X.Y.zip
Release notes should list user-visible changes, what's in the zip, run instructions, and the player controls.