JurajNyiri/pytapo

Download SD videos

Closed this issue · 29 comments

Would be possible to download the contents of the SD card ? I'd like to have a script that automatically backups all cameras videos, thanks in advance.

Hello, I too would love this feature.

Someone will need to discover a way how to get those videos from the camera and then I will add this to this lib.

Ok here we go, huge amount of work went into this and we need more help.

Thanks to a huge help from @depau who RE the camera and figured how the camera loads recording and also wrote a lot of code, we were able to get to an almost-there state.

Almost is an important word here.

So here is a summary of whats going on:

  • Camera uses its own http/websockets like protocol on a custom port. Because of this we had to develop a completely new low-level code and not use something like requests (reimplement the digest, the whole communication with camera through sockets etc.). Depau documented how exactly it works here.
  • Camera 2-way encrypts all the streams with your cloud password so the communication is secure
  • Camera sends data in N chunks, and needs acknowledgment after every N chunks to send N more.
  • When one recording ends, another one starts automatically. Camera doesn't inform about this.
  • When all the recordings for the day are sent in the stream, camera responds with {'type': 'notification', 'params': {'event_type': 'stream_status', 'status': 'finished'}}

With the current code, we are able to get all the raw & decoded data for the whole day (currently its temporarily limited to 2000 sequences, gives around a minute or 2, for testing).

However, when we convert the TS data into mp4, we get a video having around 1.3FPS which is terrible.

Here is where we need help. There are 2 possibilities what is wrong here:

1.) We are missing something in the code. Currently we are getting all the binary data and decoding & connecting it together. It decodes fine and is also able to be converted, so I think this is not the case.
2.) The conversion which we are doing through ffmpeg (not part of the code) is wrong. Maybe we need to specify another input codec or something else. Converted video also doesn't have a sound. I think this is the culprit.

After we resolve the low fps issue we can continue with the code and:

  • Implement converting via the library directly, along with some nice functions
  • We could also split the video file according to data we have (start and end times of all the recordings) so that we get individual recordings for the whole day

If you have the python / ffmpeg experience and wish to help:

Download latest dev branch.

Run file below. The file should use the downloaded library, not your installed one.
It will download a part of the day recordings and save it to ./output/ directory.

Try to get a smooth video file from it... and please let us know if you have any success!

from pytapo import Tapo
from datetime import datetime
import json
import os
import asyncio
import logging

# change variables here
date = "20201202"  # change to a date when you had some recordings YYYYMMDD
host = "192.168.100.72"  # change to camera IP
user = os.environ.get("PYTAPO_USER")  # set to your custom user account set via app
password = os.environ.get(
    "PYTAPO_PASSWORD"
)  # set to your custom acc password set via app
password_cloud = os.environ.get("PYTAPO_CLOUD_PASSWORD")  # set to your cloud password
logging.basicConfig(level=logging.DEBUG)

# testing code below

print("Connecting to camera...")
tapo = Tapo(host, user, password, password_cloud)

print("Getting recordings")
recordings = tapo.getRecordings(date)
print("Found {len} recordings... Getting first one.".format(len=len(recordings)))
userID = tapo.getUserID()


async def download_async():
    print("Starting...")
    mediaSession = tapo.getMediaSession()
    async with mediaSession:
        for recording in recordings:
            for key in recording:
                startTime = recording[key]["startTime"]
                endTime = recording[key]["endTime"]
                payload = {
                    "type": "request",
                    "seq": 1,
                    "params": {
                        "playback": {
                            "client_id": userID,
                            "channels": [0],
                            "scale": "1/1",
                            "start_time": str(startTime),
                            "end_time": str(endTime),
                            "event_type": [1, 2],
                        },
                        "method": "get",
                    },
                }

                payload = json.dumps(payload)
                output = b""
                async for resp in mediaSession.transceive(payload):
                    if resp.mimetype == "video/mp2t":
                        output += resp.plaintext

                date = datetime.utcfromtimestamp(int(startTime)).strftime(
                    "%Y-%m-%d %H_%M_%S"
                )

                fileName = "./output/" + str(date) + ".ts"
                print("Saving to " + fileName + "...")
                file = open(fileName, "wb")
                file.write(output)
                file.close()
            break


loop = asyncio.get_event_loop()
loop.run_until_complete(download_async())
depau commented

Camera 2-way encrypts all the streams with your cloud password so the communication is secure

FYI people: An attacker who has a dump of the communication and your cloud password can still decrypt the video. Beware ;)


Anyway I sent a TS video sample to a friend of mine who's into video codecs, he's gonna check it out.

@depau would you be able to share it, I'd like to try it to if possible. I'm away from my cameras until next year, so it would be awesome if you can share that sample.

@depau if its mine sample please do not share publicly

depau commented

Yeah I only have your sample, I'm not sending it around. I only showed it to my friend (i know him personally) and he said that to him it looks like the video is actually recorded at 1/2 fps, I have no idea why that would be.

Maybe someone else can try to retrieve some samples and publish them so people can try to figure it out.

I'm going to try when I'm back to my house Jan 20th.

Same for me.
Playing the .ts file on kmplayer gives me a video with frames every 2 seconds + no audio.
Same result after converting it to MP4.

My friend and I achieved similar results before when we are doing research on tapo C200, the low framerate issue is a real mystery :/

Here are what we did: https://drmnsamoliu.github.io/video.html

And just FYI there is an auth bypass vulnerability in their implementation of RTSP protocol, but we failed to receive anything useful too.

Any update on this? I also failed to fix the issue of low fps in my own research... ;/

i'm checking the ts files in order to see if we can fix this... it seems that there are two streams, video and i suposse an audio stream, but my ffmpeg doesn't recognice the second stream codec...

[mpegts @ 0x5623237e0600] probed stream 1 failed
[mpegts @ 0x5623237e0600] Could not find codec parameters for stream 1 (Unknown: none ([144][0][0][0] / 0x0090)): unknown codec
Consider increasing the value for the 'analyzeduration' and 'probesize' options
Input #0, mpegts, from '2021-12-10 13_21_05.ts':
  Duration: 00:00:41.66, start: 1.934333, bitrate: 809 kb/s
  Program 1 
    Stream #0:0[0x44]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuvj420p(pc, bt709, progressive), 1920x1080, 15 fps, 15 tbr, 90k tbn, 30 tbc
    Stream #0:1[0x45]: Unknown: none ([144][0][0][0] / 0x0090)

i think its a subtitle stream... i tried removing it (just in case the subtitles stream is forcing some kind of weird fps), with no luck.

also, i've transformed each frame to jpg, and it seems that, even if the FPS are 15, the frames are repeated, so my guess is that the transceive method is making something wrong, or the json payload must have more params or some of the params must be changed in order to get the correct TS.

Has there been any more progress? I was wondering if someone has compared the original file on the SD-card and the one captured from the stream? I.e. Size, if it is spesific framnes that are streamed etc.

I have no idea if this could affect the dropping of frames, but when viewing in the app. It is possible to select different playback speeds. I guess having a faster playback speed means dropping frames. So maybe it is possible to experiment with different playback speeds to see if this affect the fps.

I have no idea if this could affect the dropping of frames, but when viewing in the app. It is possible to select different playback speeds. I guess having a faster playback speed means dropping frames. So maybe it is possible to experiment with different playback speeds to see if this affect the fps.

Good idea, unfortunately already tried it when originally implementing. It is passed as a parameter "scale": "1/1",.

I have no idea if this could affect the dropping of frames, but when viewing in the app. It is possible to select different playback speeds. I guess having a faster playback speed means dropping frames. So maybe it is possible to experiment with different playback speeds to see if this affect the fps.

Good idea, unfortunately already tried it when originally implementing. It is passed as a parameter "scale": "1/1",.

Ahh to bad. :(

Whe Tapo care is activate, it says the videos is uploaded to the cloud, maybe by analyzing this traffic. Its possible to get more knowledge i to how videos are streamed/downloaded from the camera. Maybe this has already been drone. :/

A recent vulnerability was discovered where a reverse shell can be made by using post request (similar to what pytapo does). Maybe someone with version below 1.1.16 can utilize this to explore the inner workings of tapo C200. Unfortunately I have already updated my firmware to 1.1.18 before discovering this.

https://www.hacefresko.com/posts/tp-link-tapo-c200-unauthenticated-rce

One step forwards, one step back.

Discovered a way how to "turn off" encryption of stream (and get state of the encryption). As I later found out this simply changes authorization to stream from using cloud password to using a hardcoded super secret password.

After getting a recorded file with this way, I still get the same result as before after converting it with ffmpeg. I guess this at least confirms we are decoding the streamed data correctly.

The changes are in dev branch as always.

I analyzed chunks of data received when downloading the stream. There is always a bigger chunk, and 2 or 3 smaller chunks between the large ones. They always have length 376 decrypted, 384 encrypted but different content.

I tried ignoring these chunks when building the file, and interestingly, it resulted into exactly the same result as before, with smaller file size (4mb vs 4.4mb). So it looks like these chunks can be ignored and we achieve the same result. Maybe it is sound or I was also thinking of the "difference" between the big chunks (that seems to be missing in our resulting file) but there is not more of them on motion in file, always just 2-3.

After removing the small chunks, ffmpeg is still reporting 2 streams.

Updated DEV branch with code changes adding ability to record live stream.

It uses the same port and communication as recordings, with following differences:

  • There is no need to send acknowledgments
  • Server does not return seq number

How to run:

from pytapo import Tapo
import json
import asyncio
import logging

logging.basicConfig(level=logging.DEBUG)
# change variables here
host = ""  # change to camera IP
user = ""  # your username
password = ""  # your password
password_cloud = ""  # set to your cloud password

tapo = Tapo(host, user, password, password_cloud)

devID = tapo.getBasicInfo()["device_info"]["basic_info"]["dev_id"]


async def download_async():
    print("Starting...")
    mediaSession = tapo.getMediaSession()
    async with mediaSession:
        payload2 = {
            "params": {
                "preview": {
                    "audio": ["default"],
                    "channels": [0],
                    "deviceId": devID,
                    "resolutions": ["HD"],
                },
                "method": "get",
            },
            "type": "request",
        }

        payload = json.dumps(payload2)
        output = b""
        dataChunks = 0
        async for resp in mediaSession.transceive(payload):
            if resp.mimetype == "video/mp2t":
                # if len(resp.plaintext) != 376:
                output += resp.plaintext
                print(len(resp.plaintext))
                dataChunks += 1
            if dataChunks > 2000:
                break

        fileName = "./output/stream.ts"
        print("Saving to " + fileName + "...")
        file = open(fileName, "wb")
        file.write(output)
        file.close()


loop = asyncio.get_event_loop()
loop.run_until_complete(download_async())

The resulting file is broken the same way as recordings are.

Thanks to this we know:

  • its not an issue with seq or acknowledgements.
  • its not an issue with parameters sent

With that, we can deduce that it can be an issue with:

  • Incorrect codec conversion via ffmpeg of resulting ts file
  • Incorrect data gathering in script
  • Something else?

I'm not getting anything. zero size all the time.

python 3.10.8
tapo C200 firmware 1.1.18

(venv) [jepes@ARCHJEPES pytapodev]$ rm output/stream.ts;  python vidtest.py && ls -lha output/stream.ts 
rm: cannot remove 'output/stream.ts': No such file or directory
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST / HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=2b9780464aa92c3541d432cec0fa4dc1/ds HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=2b9780464aa92c3541d432cec0fa4dc1/ds HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=2b9780464aa92c3541d432cec0fa4dc1/ds HTTP/1.1" 200 None
/home/jepes/Scripts/pytapodev/vidtest.py:54: DeprecationWarning: There is no current event loop
  loop = asyncio.get_event_loop()
DEBUG:asyncio:Using selector: EpollSelector
Starting...
INFO:pytapo.media_stream.session:Connected to the media streaming server
DEBUG:pytapo.media_stream.session:Authentication data retrieved
AES key: e38abba4bdc01ea2d70432e4c06ab103, iv: 97f17abbc57362b5a3b13f668a548bf4
DEBUG:pytapo.media_stream.crypto:AES cipher set up correctly
DEBUG:pytapo.media_stream.session:AES key exchange performed
DEBUG:pytapo.media_stream.session:Plaintext request of type application/json sent (sequence 23309, session None), expecting 51 responses from queue 140129600595200
DEBUG:pytapo.media_stream.session:Response handler is running
DEBUG:pytapo.media_stream.session:Handling new server response
DEBUG:pytapo.media_stream.session:Plaintext response of type application/json processed (sequence 23309, session 4), dispatching to queue 140129600595200
DEBUG:pytapo.media_stream.session:Handling new server response
DEBUG:pytapo.media_stream.session:Got one response from queue 140129600595200
DEBUG:pytapo.media_stream.session:Server did not send a new chunk in 1.0 sec (sequence 23309, session 4), assuming the stream is over
Saving to ./output/stream.ts...
-rw-r--r-- 1 jepes jepes 0 Nov 25 16:34 output/stream.ts
(venv) [jepes@ARCHJEPES pytapodev]$ rm output/stream.ts;  python vidtest.py && ls -lha output/stream.ts 
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST / HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=ecb7644838c447a2306b38017a63c01b/ds HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=ecb7644838c447a2306b38017a63c01b/ds HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=ecb7644838c447a2306b38017a63c01b/ds HTTP/1.1" 200 None
/home/jepes/Scripts/pytapodev/vidtest.py:54: DeprecationWarning: There is no current event loop
  loop = asyncio.get_event_loop()
DEBUG:asyncio:Using selector: EpollSelector
Starting...
INFO:pytapo.media_stream.session:Connected to the media streaming server
DEBUG:pytapo.media_stream.session:Authentication data retrieved
AES key: 0cd3d919492e7887a2811119630e63bb, iv: 5a7a33a3a5884d1f9978b7b12fcf2b3a
DEBUG:pytapo.media_stream.crypto:AES cipher set up correctly
DEBUG:pytapo.media_stream.session:AES key exchange performed
DEBUG:pytapo.media_stream.session:Plaintext request of type application/json sent (sequence 7707, session None), expecting 51 responses from queue 140015703760976
DEBUG:pytapo.media_stream.session:Response handler is running
DEBUG:pytapo.media_stream.session:Handling new server response
DEBUG:pytapo.media_stream.session:Plaintext response of type application/json processed (sequence 7707, session 5), dispatching to queue 140015703760976
DEBUG:pytapo.media_stream.session:Handling new server response
DEBUG:pytapo.media_stream.session:Got one response from queue 140015703760976
DEBUG:pytapo.media_stream.session:Server did not send a new chunk in 1.0 sec (sequence 7707, session 5), assuming the stream is over
Saving to ./output/stream.ts...
-rw-r--r-- 1 jepes jepes 0 Nov 25 16:34 output/stream.ts
(venv) [jepes@ARCHJEPES pytapodev]$ 
(venv) [jepes@ARCHJEPES pytapodev]$ rm output/stream.ts;  python vidtest.py && ls -lha output/stream.ts 
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST / HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=5fea787fb38b42d5a1cf01e62cc1c0ba/ds HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=5fea787fb38b42d5a1cf01e62cc1c0ba/ds HTTP/1.1" 200 None
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 192.168.1.211:443
DEBUG:urllib3.connectionpool:https://192.168.1.211:443 "POST /stok=5fea787fb38b42d5a1cf01e62cc1c0ba/ds HTTP/1.1" 200 None
/home/jepes/Scripts/pytapodev/vidtest.py:54: DeprecationWarning: There is no current event loop
  loop = asyncio.get_event_loop()
DEBUG:asyncio:Using selector: EpollSelector
Starting...
INFO:pytapo.media_stream.session:Connected to the media streaming server
DEBUG:pytapo.media_stream.session:Authentication data retrieved
AES key: d73cc07cfdf4c9bf3256a036ee192241, iv: 792a20ca67719a583ab44c84bfb3772b
DEBUG:pytapo.media_stream.crypto:AES cipher set up correctly
DEBUG:pytapo.media_stream.session:AES key exchange performed
DEBUG:pytapo.media_stream.session:Plaintext request of type application/json sent (sequence 18349, session None), expecting 51 responses from queue 140268362850720
DEBUG:pytapo.media_stream.session:Response handler is running
DEBUG:pytapo.media_stream.session:Handling new server response
DEBUG:pytapo.media_stream.session:Plaintext response of type application/json processed (sequence 18349, session 6), dispatching to queue 140268362850720
DEBUG:pytapo.media_stream.session:Handling new server response
DEBUG:pytapo.media_stream.session:Got one response from queue 140268362850720
DEBUG:pytapo.media_stream.session:Server did not send a new chunk in 1.0 sec (sequence 18349, session 6), assuming the stream is over
Saving to ./output/stream.ts...
-rw-r--r-- 1 jepes jepes 0 Nov 25 16:34 output/stream.ts
(venv) [jepes@ARCHJEPES pytapodev]$ 

@jepes1981 please come to discord to debug the issue with running this script.

There has been progress on this made on go2rtc repository.

I have spent a few hours on this comparing the code and have found that we need to be decoding the received payload.

So here:

async for resp in mediaSession.transceive(payload):
            if resp.mimetype == "video/mp2t":
                # if len(resp.plaintext) != 376:
                output += resp.plaintext
                print(len(resp.plaintext))
                dataChunks += 1
            if dataChunks > 2000:
                break

We need to be decoding resp.plaintext as a TS packet, similarly to how it is done here.

I have verified that:

  • pytapo & go2rtc is receiving the same communication
  • pytapo & go2rtc is is receiving exactly the same sizes of payloads before AES decoding
  • pytapo & go2rtc is is receiving exactly the same sizes of payloads after AES decoding

The difference between is that pytapo is not decoding the received packets, where as go2rtc is doing that and sending the decoded payload, which is significantly smaller. There is also something happening with tracks there, I am not sure what yet.

I have tried following libraries in python to achieve the same with no success:

Discovery

Modified go2rtc with following logs:

Screenshot 2023-02-17 at 22 47 18

This gives me following output:

Screenshot 2023-02-17 at 22 46 44

Notice the 0 0 1, this is important part of the data and needs to be in every chunk received.

Implementation

Next, I have rewritten all the methods from go2rtc into pytapo (ouch) as described in above comment. File attached. I think they should be working as expected but have not tested them fully yet.

tmp6.py.zip

Result

I have found that when using the functions that I wrote (if uncommented in code above/screenshot below), I land in a code branch where go2rtc never lands.

image

The reason for that is that the chunks that I am receiving in pytapo do not have 0 0 1 like the chunks in go2rtc. Interestingly, size of the chunk is the same!

Simplifying debugging for above

Screenshot 2023-02-17 at 23 44 01

Download:
tmp6.py.zip

Just fill in ENV variables at the start of file or hardcode them, you can use the latest github main branch version.

Go2RTC output

Screenshot 2023-02-17 at 23 46 42

PyTapo output

Screenshot 2023-02-17 at 23 50 53

Next action item

We now have an easily debuggable scenario with comparison code and we need to make sure that the output of pytapo is the same as Go2RTC.

SOLVED AFTER MORE THAN 2 YEARS

All we needed to do was refresh IV in crypto after every message.

image

Also, all that code I rewrote from go to python for stream RTP was not needed in the end.

Screenshot 2023-02-18 at 14 44 07

Coming soon.

Script to download all recordings have been added here.

You can launch it like so:

output="/Users/jurajnyiri/tapo/recordings/LivingRoom" date=20230218 host="192.168.1.65" user="myUser" password="s3cr3t" password_cloud="v3rys3cr3t" python3 DownloadRecordings.py

Next task

We need to figure out how to convert audio into the files.

All needed functionality has been added. Recordings now also include audio converted into AAC.

Instructions are in readme.