Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download SD videos #4

Closed
wsantos opened this issue Nov 16, 2020 · 29 comments
Closed

Download SD videos #4

wsantos opened this issue Nov 16, 2020 · 29 comments
Labels
enhancement New feature or request

Comments

@wsantos
Copy link

wsantos commented Nov 16, 2020

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.

@JurajNyiri
Copy link
Owner

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.

@JurajNyiri
Copy link
Owner

JurajNyiri commented Dec 5, 2020

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
Copy link
Contributor

depau commented Dec 6, 2020

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.

@wsantos
Copy link
Author

wsantos commented Dec 15, 2020

@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.

@JurajNyiri
Copy link
Owner

@depau if its mine sample please do not share publicly

@depau
Copy link
Contributor

depau commented Dec 16, 2020

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.

@wsantos
Copy link
Author

wsantos commented Dec 21, 2020

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

@CrashLaker
Copy link

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.

@DrmnSamoLiu
Copy link

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.

@konrad-ch
Copy link

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

@dvelayos
Copy link

dvelayos commented Dec 10, 2021

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.

@antonhagg
Copy link

antonhagg commented Jan 23, 2022

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.

@antonhagg
Copy link

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.

@JurajNyiri
Copy link
Owner

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",.

@dvelayos
Copy link

dvelayos commented Jan 25, 2022 via email

@antonhagg
Copy link

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. :/

@jepes1981
Copy link

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

@JurajNyiri
Copy link
Owner

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.

@JurajNyiri
Copy link
Owner

JurajNyiri commented Nov 24, 2022

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.

@JurajNyiri
Copy link
Owner

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?

@jepes1981

This comment was marked as off-topic.

@JurajNyiri
Copy link
Owner

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

@JurajNyiri
Copy link
Owner

JurajNyiri commented Feb 17, 2023

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:

@JurajNyiri
Copy link
Owner

JurajNyiri commented Feb 17, 2023

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!

@JurajNyiri
Copy link
Owner

JurajNyiri commented Feb 17, 2023

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.

@JurajNyiri
Copy link
Owner

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.

@JurajNyiri
Copy link
Owner

Screenshot 2023-02-18 at 14 44 07

Coming soon.

@JurajNyiri
Copy link
Owner

JurajNyiri commented Feb 18, 2023

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.

@JurajNyiri JurajNyiri added enhancement New feature or request coming soon and removed help wanted Extra attention is needed labels Feb 18, 2023
@JurajNyiri
Copy link
Owner

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

Instructions are in readme.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

9 participants