New
Apps 232Makers 232This week 232
Promote your app−30%
Tapo uploader screenshot 1
0views in
the last 30 days

Tapo uploader

Tapo C200 to YouTube: record only on motion (ONVIF + RTSP + ffmpeg), upload private videos with a da

Built with Claude Code

by luigiellebalotta · from ITItaly · added Oct 29, 2026
no ratings0 comments
Open app Code

tapo-uploader: record your Tapo camera on motion, keep the videos in your own storage

🌐 English · Italiano

A self-hosted web app that records your TP-Link Tapo camera (tested on the Tapo C200) only when it detects motion and keeps the videos in the storage you choose: YouTube, your own OneDrive, Nextcloud or any WebDAV, any rclone cloud (Mega, S3, Dropbox...) or the disk of the machine that runs it. One page to watch everything, whichever storage it lives in. No cloud subscription, no firmware hacking.

The Tapo C200 can't send its recordings to your own storage by itself. tapo-uploader is a small Python service that pulls the camera's RTSP stream with ffmpeg, listens for ONVIF motion events, and saves the result where you decide. Upload destinations can be mixed and changed at any time: the videos you saved earlier stay on your page, each marked with the storage it lives in. It runs for one person from a single .env, or for a group of friends with accounts, each with their own camera and storage. A Raspberry Pi, your own PC, a cheap VPS or a Docker container is enough.

 Tapo C200 ──RTSP──▶ ffmpeg ──▶ 30 s before + 5 min after each motion ──▶ one video a day, with chapters
 (home)    ──ONVIF motion events──▶                                       ──▶ YouTube · OneDrive · WebDAV · rclone · disk
                                                                              (a page to watch them all)

Features

  • Motion-triggered recording. Keeps 30 seconds before the detection and 5 minutes after; motion near the end of the window extends it. All times are configurable.
  • One video per day. Clips are joined without re-encoding and uploaded once a day (21:00 by default), with the time of every event as chapters in the description.
  • Or continuous recording. 6-hour segments, each uploaded as its own video.
  • Your storage, your choice. YouTube (private videos, a playlist per day), your own OneDrive, Nextcloud/WebDAV, any rclone cloud, or the server's disk; change it any time and nothing is lost.
  • One page for everything. The web app lists the videos of every storage you ever used, each marked with where it lives, plays them in the page, and also shows clips still waiting for their upload.
  • Frees the disk. Files are deleted right after a successful upload; videos kept on the server can rotate by age, by a space limit per person, or when the disk runs low.
  • No re-encoding. ffmpeg -c copy: close to zero CPU, so a Raspberry Pi is plenty.
  • Resilient. Restarts ffmpeg when the stream drops, retries failed uploads, handles the YouTube quota, survives reboots (fragmented MP4 stays playable even if killed).
  • Credentials never logged. The camera password is masked in every log line.
  • Runs anywhere. systemd unit, Dockerfile, and Render blueprints included.
  • A complete, honest guide. Every step of a real setup is documented, including the dead ends.

Contents


How it works

There are two modes. Both use the same camera, the same storage settings and the same .env.

Continuous (tapo_uploader.py) Motion (tapo_motion.py)
Records Everything, in 6-hour segments Only around motion: 30 s before, 5 min after
Uploads Each segment as soon as it closes One video a day, at a fixed time
Disk use A few GB at most Almost nothing (about one minute of buffer)
Needs RTSP RTSP + ONVIF events (port 2020)
YouTube quota (if YouTube is the storage) 4 uploads/day 1 upload/day

tapo_uploader.py run starts two threads:

  1. Recording. ffmpeg reads the RTSP stream with -c copy (the video is not re-encoded) and writes YYYYMMDD_HHMMSS.mp4 files into recordings/. Cuts are aligned to the wall clock: with 6-hour segments they close at 00:00, 06:00, 12:00 and 18:00. If the stream drops, ffmpeg restarts.
  2. Upload. Every minute it looks for finished segments, sends them to the storage you chose (on YouTube as private videos) titled Tapo YYYY-MM-DD HH:MM-HH:MM, adds them to the playlist Tapo YYYY-MM-DD (created if missing) and deletes the file. If an upload fails the file stays and is retried.

The camera can't upload on its own, so you need an always-on computer that can reach it: a Raspberry Pi or mini PC at home, a VPS, or a container host.

Requirements

You need Notes
A Tapo camera with RTSP Tested on the Tapo C200. You need the "Camera Account" (step 1).
An always-on computer Linux recommended (VPS, Raspberry Pi). Python 3.10+ and ffmpeg.
Somewhere to keep the videos A YouTube channel (a Google account), a OneDrive, a Nextcloud/WebDAV, an rclone cloud, or just the disk.
Router access (only if the server is not at home) To open a port towards the camera.

Disk: at 640×360 a 6-hour segment is about 0.4 GB. A few free GB are enough because files are deleted after upload. Bandwidth: about 1 MB per minute of video.

Other ONVIF cameras that expose RTSP may work, but only the Tapo C200 has been tested.


1. The camera

  1. In the Tapo app open the camera, then Settings (gear) → Advanced → Camera Account. Create a username and password just for the camera. They are not your Tapo/cloud account: using those is the most common mistake and gives 401 Unauthorized.

  2. Find the camera's IP address in the app (Device Info) or in your router, and ideally give it a fixed IP (DHCP reservation) in the router.

  3. Set the video quality to High (Settings → Video Quality). It may default to low: in our case both streams were 640×360.

  4. The stream address is:

    rtsp://USER:PASSWORD@CAMERA_IP:554/stream1     (high quality)
    rtsp://USER:PASSWORD@CAMERA_IP:554/stream2     (low quality)
    

    If the password contains special characters (!, @, :) they must be URL-encoded. The script does it for you; by hand, a letters-and-digits password is easier.

Important limit: the camera accepts one RTSP session per stream. While the service records stream1, a second program opening stream1 gets Operation not permitted. To experiment while the service runs, use stream2 (CAMERA_STREAM=stream2) or stop the service first.


2. Google Cloud and YouTube

Only if you want YouTube as the storage. For OneDrive, Nextcloud/WebDAV, rclone or the disk skip to 3 and follow the storage setup guide.

The script uploads to your channel through the official YouTube Data API v3, so you need a small Google "project" that identifies the app. It is free.

2.1 Project and API

  1. Go to https://console.cloud.google.com/projectcreate, name it tapo-uploader, Create.
  2. Open https://console.cloud.google.com/apis/library/youtube.googleapis.com and press Enable (with the new project selected at the top).

2.2 OAuth consent screen

  1. Open https://console.cloud.google.com/auth/overview and press Get started.
  2. App name tapo-uploader, support email = yours.
  3. Audience: External. Contact email = yours. Accept and Create.
  4. Under Audience → Test users → Add users add your own Google account, the one that owns the YouTube channel.

2.3 Credentials

  1. Clients → Create client. Type: Desktop app, name tapo-uploader. Create.
  2. Press Download JSON and save it as client_secret.json in the project folder. It is a secret: don't commit it (.gitignore excludes it).

2.4 Publish the app (important)

While the app stays in Testing, the access token expires after 7 days and the script stops uploading until you log in again. To avoid it, publish the app ("In production"). For personal use Google needs no verification, but the Publish app button stays disabled until Branding has:

  • a link to the app home page,
  • a link to the privacy policy,
  • the matching authorized domain.

Use your own pages, not this project's. Google checks that the home page and the privacy policy belong to the domain of whoever owns the app, and that they describe that app. The policy in this repository names its author as the person responsible and describes the author's own server, so it would be wrong on your app and could fail Google's review. Copy index.html and privacy.html (MIT licensed) and rewrite the privacy policy for your own use: who runs it, what is stored, and who can use it (just you, or other people on your server: then say so, and name the services involved). A link back to this project on your home page is appreciated but not required. Each person who runs the software needs their own Google app and their own pages.

You don't need a real website, only two static pages. The quickest way is GitHub Pages:

  1. Create a public repository (this one works) with two files: index.html (one line about what the app does) and privacy.html (what it accesses, what is stored, how to revoke access). This repository has both, ready to copy and adapt.
  2. In Settings → Pages choose the main branch, folder /.
  3. After a minute the pages are at https://YOURUSER.github.io/REPO-NAME/.
  4. In Google Cloud, Branding → App domain: enter the home page and privacy policy; under Authorized domains → Add domain put YOURUSER.github.io. Save.
  5. Audience → Publish app → Confirm ("Push to production?").

Publish before creating the token (step 3). A token created while the app was in Testing can stay subject to the 7-day expiry even after publishing: to be safe, delete token.json and run auth again after publishing.


3. Install and test on your PC

These steps work on any system (Windows, macOS, Linux). You need Python 3.10+ and ffmpeg in the PATH (ffmpeg -version to check).

git clone https://github.com/LuigiElleBalotta/tapo-uploader.git
cd tapo-uploader
python -m venv .venv
. .venv/bin/activate            # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env            # then fill in CAMERA_USER, CAMERA_PASS, CAMERA_HOST

Choose where the videos go first (see Where the recordings go; to try it with no account at all use STORAGE=local and LOCAL_ROOT=/some/folder in .env). If you use YouTube, put client_secret.json (step 2.3) in the folder, then:

python tapo_uploader.py check   # 1. does the camera answer? prints codec and resolution
python tapo_uploader.py auth    # 2. Google login, once: creates token.json
python tapo_uploader.py test    # 3. records 60 s and uploads them: checks everything
  • check must print codec_name=h264, the resolution and the audio. 401 Unauthorized means wrong credentials; Operation not permitted means the stream is already in use (see above).
  • auth prints an address: open it in a browser on the same computer, choose the Google account of the channel and allow YouTube access. The browser shows "The authentication flow has completed" and token.json appears. If, while in Testing, you see "Google hasn't verified this app": Advanced → Go to tapo-uploader.
  • test records 60 seconds, uploads them as a private video and creates the day's playlist. Check that the video is on YouTube, then delete it from Studio.

If the server has no browser, run auth on your PC and copy token.json to the server.


Where to run it

The program must stay on 24/7 and be able to reach the camera. The options, simplest first:

Where Rough cost How it reaches the camera Notes
Your own PC (Windows, macOS, Linux) free LAN Fine if it stays on and awake. No port forwarding. See below.
Raspberry Pi / mini PC at home one-off, about $15–50 LAN Recommended: always on, no port forwarding, no DDNS, no exposed camera.
VPS (as in this guide) a few dollars a month port forwarding limited to the VPS's fixed IP Steps 4 and 5. This is the tested setup.
Render (Background Worker) about $7/month, plus $0.25/GB of disk port forwarding to Render's shared IPs Docker image included, not tested. See below.
Render free (web service) $0 port forwarding to Render's shared IPs Tested up to startup, not with the camera. Ephemeral disk: clips can be lost. See below.
Vercel – – Not suitable. See below.

Other container platforms (Fly.io, Railway) follow the same pattern as Render: the Dockerfile works anywhere, only prices and networking change.

Your own PC (Windows, macOS, Linux)

Yes, a personal computer works, with one condition: it must stay on, awake and on the same network as the camera, because it records only while the program runs. A laptop that sleeps in the evening will miss the motion. Nothing is opened on the router. A short version (the full, illustrated-by-commands guide is the PC install guide):

  1. Create the Camera Account in the Tapo app (step 1) and note the camera's IP address (ask the router to always give it the same one).
  2. Install the tools. Windows (PowerShell): winget install Python.Python.3.12, winget install Gyan.FFmpeg, winget install Git.Git. macOS: brew install python ffmpeg git. Linux: sudo apt install python3 python3-venv ffmpeg git.
  3. git clone https://github.com/LuigiElleBalotta/tapo-uploader.git, cd tapo-uploader, python -m venv .venv, activate it (.venv\Scripts\Activate.ps1 on Windows, . .venv/bin/activate elsewhere) and pip install -r requirements.txt.
  4. Copy .env.example to .env and set CAMERA_HOST, CAMERA_USER, CAMERA_PASS. To keep the videos on the PC: STORAGE=local and LOCAL_ROOT=C:\Tapo (or /home/you/Tapo). Other storages: see Where the recordings go.
  5. python tapo_uploader.py check, then python tapo_motion.py run.
  6. Keep it running: turn off sleep (and "close the lid" actions on a laptop), and start it at login (Windows Task Scheduler, a macOS login item, or a systemd service on Linux).

With Docker Desktop you can skip Python and ffmpeg and use the published image.

Render (paid Background Worker)

Render can run an always-on process (a Background Worker) from a Docker image. The repository contains Dockerfile and render.yaml. Things to know:

  • Cost. The smallest worker (0.5c-512mb: 0.5 CPU and 512 MB) is listed at $7/month. Render's free plan is for web services only, not workers. With ffmpeg in -c copy mode CPU is not the limit; RAM was not measured on Render.
  • Disk. The container's disk is lost on every restart and deploy. Without a persistent disk ($0.25/GB per month, 1 GB is enough; it attaches to a worker) the token, the state and the day's clips are lost. render.yaml asks for one mounted at /data, and the script uses it through DATA_DIR.
  • Secrets. A container has no files to copy: token.json and client_secret.json are passed as the environment variables YOUTUBE_TOKEN_JSON and YOUTUBE_CLIENT_SECRET_JSON (the files' contents). On first start the script writes them to /data with mode 600; afterwards the refreshed token stays on disk. The login (auth) is done once on your PC.
  • Networking is the weak point. Render's outbound IPs are shared by every service in the same region, and a service may use any address in its range (see Render's docs). To restrict access to the camera you would open port forwarding to all of the region's ranges, so to other Render customers too. Dedicated IPs exist but, from the information found, are paid and only on Pro/Scale plans (about $100/month per set of IPs): not worth it for this. If you pick Render, the exposed port must be protected by a long, unique password and a non-standard external port; or connect home and Render with a VPN, which was not verified.

Steps: connect the repository to Render (New → Blueprint), fill in the variables it asks for (CAMERA_USER, CAMERA_PASS, CAMERA_HOST/CAMERA_PORT, ONVIF_HOST/ONVIF_PORT, YOUTUBE_TOKEN_JSON, YOUTUBE_CLIENT_SECRET_JSON), open the router ports to Render's IPs and watch the worker's logs. Render only asks for the variables when the Blueprint is created: later you change them in the dashboard. For continuous recording instead of motion mode, uncomment dockerCommand in render.yaml.

Render free (no VPS)

For people without a server. The VPS remains the main choice: this is a zero-cost alternative with precise limits. Deploying as a web service on the free plan was tried on Render up to startup (build, health endpoint on port 10000, ffmpeg start); recording from the camera through Render was not verified, because it needs the router rules described below.

How it works: the program opens a small HTTP port (/health, active when Render sets PORT) and a free monitoring service (UptimeRobot, cron-job.org) calls it every 5 minutes. Per Render's documentation a free service sleeps after 15 minutes without inbound traffic; outbound traffic doesn't count, so a call from outside is needed. The 750 free monthly hours are enough for one always-on service.

Steps (the ones actually done, from the dashboard):

  1. On dashboard.render.com choose New → Web Service, the Public Git Repository tab, paste the repository URL and press Connect. Render detects the Dockerfile by itself.
  2. Under Compute pick the $0 / month (0.1 CPU, 512 MB) row. The preselected plan is the $7/month one: if you leave it, you pay.
  3. Under Environment Variables press Add from .env and paste the lines below with your values. The token and client secret must be on a single line, as compact JSON (python -c "import json;print(json.dumps(json.load(open('token.json')),separators=(',',':')))").
    CAMERA_USER=...
    CAMERA_PASS=...
    CAMERA_HOST=your-ddns-name.example.com
    CAMERA_PORT=external RTSP port
    ONVIF_HOST=your-ddns-name.example.com
    ONVIF_PORT=external ONVIF port
    CAMERA_STREAM=stream1
    DATA_DIR=/data
    TZ=Europe/Rome
    YOUTUBE_TOKEN_JSON=...
    YOUTUBE_CLIENT_SECRET_JSON=...
    
  4. Deploy web service. The build takes about a minute and a half. In the logs you should see health endpoint on port 10000 and ffmpeg start. The warning file_cache is only supported with oauth2client<4.0.0 is harmless.
  5. From the service page take the URL (https://NAME.onrender.com) and, with the Connect → Outbound button, the IP ranges to give to the router (in our case `74

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#cctv · #cloud-storage · #self-hosted