FROM python:3.11-slim-trixie RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg mkvtoolnix ca-certificates tzdata && rm -rf /var/lib/apt/lists/* RUN ffmpeg -hide_banner -h protocol=rtmps 2>&1 | grep -q rtmp_enhanced_codecs WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py tiktok_auth.py index.html ./ # --- YouTube ingestion ------------------------------------------------------- # copy = forward the source untouched (original quality, no extra CPU). DEFAULT. # The YouTube stream is created with cdn.resolution='variable' and # cdn.frameRate='variable', which per the Live Streaming API means # "YouTube should automatically detect the resolution of your streamed # video". So a mid-stream resolution change does NOT require a new # session, and YOUTUBE_FORMAT_GUARD=off keeps the RTMPS session alive # across it. Restarting the session was itself a visible gap and, when # the phone flipped resolution repeatedly, a reconnect storm. # fixed1080 = constant 1080x1920 canvas, re-encodes to H.264. Zero gaps but costs # CPU: measured 2.26x real time on 2 vCPU (~44% for one stream). # fixed720 = constant 720x1280 canvas, 3.72x real time (~27%). Lighter. # Both canvas modes stay available from Settings if YouTube's own healthStatus ever # reports videoResolutionUnsupported; they are opt-in because of the CPU cost. # # YOUTUBE_FORMAT_GUARD=on -> tear the session down on a resolution change (old # behaviour: 1-3 s gap, but YouTube re-declares the new size explicitly). # YOUTUBE_FLAP_LIMIT: if resolution flips this many times in 10 minutes, copy mode # falls back to YOUTUBE_FLAP_FALLBACK_MODE for the rest of the event. Dormant # while the guard is off, kept as a safety net. # # YOUTUBE_FIFO_TIMESHIFT: the push goes through FFmpeg's fifo pseudo-muxer, which # reconnects on its own when YouTube drops the connection instead of killing the # whole process (that is why the recording kept running while the broadcast died). # timeshift additionally delays the YouTube feed behind the live TikTok source and # keeps that many seconds buffered, so a source hiccup is covered instead of # starving YouTube's ingest, and packets survive a reconnect instead of being # dropped. Cost is latency only, no CPU. https://ffmpeg.org/ffmpeg-formats.html#fifo # NOTE: fifo's queue_size defaults to 60 packets, under one second of stream, so # timeshift requires it to be raised or the queue overflows and the stream is # destroyed instead of delayed. relay.py sizes it automatically (ts*250+500). # YOUTUBE_HEALTH_INTERVAL: seconds between reads of YouTube's own # healthStatus/configurationIssues while live. This is what reports videoIngestion # Starved (viewers buffer), gopSizeLong, openGop, videoResolutionUnsupported and # videoCodec, i.e. the ground truth instead of a guess. ENV YOUTUBE_SEND_MODE=copy \ YOUTUBE_FORMAT_GUARD=off \ YOUTUBE_FLAP_LIMIT=4 \ YOUTUBE_FLAP_FALLBACK_MODE=fixed720 \ YOUTUBE_X264_PRESET=veryfast \ YOUTUBE_CRF=22 \ YOUTUBE_RECOVERY_WAIT=1 \ YOUTUBE_FIFO_TIMESHIFT=10 \ YOUTUBE_HEALTH_INTERVAL=180 # --- TikTok session token (age-restricted 18+ rooms) ------------------------- # Set TIKTOK_SESSIONID in Settings > Variables and secrets, or paste it into the # UI panel (no Factory reboot needed). Sessions are short-lived (~6 h per # sid_guard), so the UI panel is the practical way to refresh. # Per account: TIKTOK_SESSIONID_ (upper-case, non A-Z0-9 -> _) # Compact map: TIKTOK_TOKENS=user1=token1;user2=token2 ENV TIKTOK_TT_TARGET_IDC=useast1a ENV PORT=7860 PYTHONUNBUFFERED=1 YOUTUBE_VIDEO_MODE=original PASSWORD=admin EXPOSE 7860 CMD ["sh", "-c", "exec uvicorn app:app --host 0.0.0.0 --port ${PORT:-7860} --timeout-graceful-shutdown 65"]