Project: Ring-Vision

Mu He | Oct 20, 2024 min read

As a long-time Ring owner, I have come to appreciate its stable and reliable service. While browsing through my garden camera timeline, I often wondered if there was a way to save video clips as timelapse recordings. To address this, I used to set an alert on my phone to manually take a screenshot every day around the same time. Although it seemed like just a few taps, maintaining this routine was challenging, and I eventually gave up after about six months.

Recently, I finally found time to explore the technical solutions available on the web to automate this process. I decided to document my research here, which I hope will save you some time. My first attempt was to search the web for existing solutions. And there is actually a project ring-timelapse that does exactly what I want. It is a Node.js project that uses the unofficial APIs to capture the snapshots and then uses ffmpeg to convert them into timelapse videos. The author wrapped the project into a Docker container, which makes it even easier to use. However, the latest update for this project is 10 months ago from today, and the way it handles the token is no longer working. You may refer to this wiki for more details. Since this project is based on the ring-client-api, I decided to take a look at the project and see if I can make some updates to make it work again. But, I am not a Node.js developer, and I do not have much experience with it. So, I decided to give up on this project.

In fact, I use Cursor to help me navigate through the code and understand the logic. The key part that I need to update is handling the refresh token, and actually there is an example in the codebase. I just refined it a little bit to make it work for my case.

ringApi.onRefreshTokenUpdated.subscribe(
      async ({ newRefreshToken, oldRefreshToken }: { newRefreshToken: string; oldRefreshToken?: string }) => {
        if (!oldRefreshToken || !newRefreshToken) {
          console.warn("Refresh token update event received with missing data");
          return;
        }
        console.log("Refresh Token Updated");
      }
    );

Here is the module for taking the snapshots.

const processCameraSchedule = (camera: RingCamera, scheduleTime: string, takeVideo = true) => {
      console.log(`- ${camera.id}: ${camera.name} (${camera.deviceType})`);

      cron.schedule(scheduleTime, async () => {
        try {
          const timestamp = new Date().toISOString().replace(/[:]/g, '-');
          
          // Take and save snapshot
          const snapshot = await camera.getSnapshot();
          const snapshotPath = path.join(snapshotDir, `${camera.name.toLowerCase()}_${timestamp}.png`);
          await writeFile(snapshotPath, snapshot);
          console.log(`Snapshot saved: ${snapshotPath}`);

          if (takeVideo) {
            // Record and save video
            const videoPath = path.join(videoDir, `${camera.name.toLowerCase()}_${timestamp}.mp4`);
            await camera.recordToFile(videoPath, 5);
            console.log(`Video recorded: ${videoPath}`);
          }
        } catch (error) {
          console.error(`Error processing ${camera.name}:`, error);
        }
      });
      
      console.log(`Scheduled daily task for ${camera.name} at ${scheduleTime}`);
    };

The logic is straightforward. It uses the cron module to schedule the task at the specified time. And then it takes the snapshot and saves it to the local directory. If the user wants to take a video, it will record the video and save it to the local directory. However, here are a few things that you need to pay attention to:

  1. The getSnapshot() function will generate a 640 x 480 png image. It may not be ideal for a good timelapse video. As a result, I called recordToFile(videoPath, 5) function to record a short video with the default resolution of 1080p for better quality.
  2. NOT all cameras support recording to file. I happened to have a Floodlight Cam in my garage, and it does not support this feature. I’m not sure if this a bug or not. I tried multiple times and it always failed. Fortunately, the camera on my garden does support this feature. Therefore, I did not keep digging into this issue.

The rest of the code is more or less self-explanatory. I used PM2 to run the script as a service, and wrote a simple script to monitor the status of the service. You can refer to this repo ring-vision for more details. PS. If you read my previous blog post, I also used my garage camera to monitor if I parked my car on the driveway. So don’t be confused if you see the module that using gpt-4o-mini for detection. You can remove it if you do not need it.

All right, that’s it for today. I hope you find this post helpful. If you have any questions or feedback, please let me know. Thanks for reading!