{"id":99,"date":"2025-09-02T14:58:30","date_gmt":"2025-09-02T14:58:30","guid":{"rendered":"https:\/\/scootercam.net\/notes\/?p=99"},"modified":"2025-09-02T14:58:30","modified_gmt":"2025-09-02T14:58:30","slug":"live-capture-code","status":"publish","type":"post","link":"https:\/\/scootercam.net\/blog\/live-capture-code\/","title":{"rendered":"Live capture code"},"content":{"rendered":"\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env python3\n\nimport subprocess\nimport os\nimport sys\nfrom datetime import datetime\nimport time\n\n# Configuration\nREOLINK_USER = \"admin\"\nREOLINK_PASS = \"Scootercam02\"\nREOLINK_IP = \"192.168.1.211\"\nREOLINK_PORT = \"554\"  # Standard RTSP port\n\nFTP_USER = \"ftp_user@scootercam.net\"\nFTP_PASS = \"password\"\nFTP_HOST = \"11.222.33.444\"\n\n# Directories\nTEMP_DIR = \"\/tmp\"\nOUTPUT_DIR = \"\/var\/www\/html\/mp\"\n\ndef log(message):\n    \"\"\"Simple logging function\"\"\"\n    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\n    print(f\"&#91;{timestamp}] {message}\")\n\ndef check_connectivity():\n    \"\"\"Check if camera is reachable\"\"\"\n    log(\"Checking camera connectivity...\")\n    try:\n        result = subprocess.run(&#91;'ping', '-c', '1', '-W', '2', REOLINK_IP], \n                              capture_output=True, timeout=5)\n        if result.returncode == 0:\n            log(\"Camera is reachable\")\n            return True\n        else:\n            log(\"ERROR: Camera is not reachable\")\n            return False\n    except subprocess.TimeoutExpired:\n        log(\"ERROR: Ping timeout\")\n        return False\n    except Exception as e:\n        log(f\"ERROR: Connectivity check failed: {e}\")\n        return False\n\ndef capture_video():\n    \"\"\"Capture 10 seconds of video from Reolink camera\"\"\"\n    timestamp = datetime.now().strftime('%Y%m%d%H%M%S')\n    temp_filename = f\"{TEMP_DIR}\/reolink_capture_{timestamp}.mp4\"\n    final_filename = f\"{OUTPUT_DIR}\/live_{timestamp}.mp4\"\n    \n    # RTSP URL for Reolink camera\n    rtsp_url = f\"rtsp:\/\/{REOLINK_USER}:{REOLINK_PASS}@{REOLINK_IP}:{REOLINK_PORT}\/h264Preview_01_main\"\n    \n    log(\"Starting 10-second video capture...\")\n    \n    # ffmpeg command to capture 10 seconds of video with audio\n    ffmpeg_cmd = &#91;\n        'ffmpeg',\n        '-i', rtsp_url,\n        '-t', '10',  # 10 seconds duration\n        '-c:v', 'libx264',\n        '-preset', 'fast',\n        '-crf', '23',\n        '-c:a', 'aac',\n        '-b:a', '128k',\n        '-vf', 'scale=1280x720:flags=lanczos',\n        '-movflags', '+faststart',\n        '-y',  # Overwrite output file\n        temp_filename\n    ]\n    \n    try:\n        # Run ffmpeg with timeout\n        result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, timeout=30)\n        \n        if result.returncode == 0:\n            log(\"Video capture completed successfully\")\n            \n            # Check if file was created and has reasonable size\n            if os.path.exists(temp_filename):\n                file_size = os.path.getsize(temp_filename)\n                if file_size > 100000:  # At least 100KB\n                    log(f\"Video file created: {os.path.basename(temp_filename)} ({file_size} bytes)\")\n                    \n                    # Move to final location\n                    os.makedirs(OUTPUT_DIR, exist_ok=True)\n                    os.rename(temp_filename, final_filename)\n                    return final_filename\n                else:\n                    log(f\"ERROR: Video file too small ({file_size} bytes)\")\n                    if os.path.exists(temp_filename):\n                        os.remove(temp_filename)\n                    return None\n            else:\n                log(\"ERROR: Video file was not created\")\n                return None\n        else:\n            log(f\"ERROR: ffmpeg failed with return code {result.returncode}\")\n            log(f\"ffmpeg stderr: {result.stderr}\")\n            return None\n            \n    except subprocess.TimeoutExpired:\n        log(\"ERROR: Video capture timed out\")\n        return None\n    except Exception as e:\n        log(f\"ERROR: Video capture failed: {e}\")\n        return None\n\ndef upload_video(video_file):\n    \"\"\"Upload video to FTP server\"\"\"\n    if not video_file or not os.path.exists(video_file):\n        log(\"ERROR: No video file to upload\")\n        return False\n    \n    video_name = os.path.basename(video_file)\n    log(f\"Uploading {video_name} to FTP server...\")\n    \n    curl_cmd = &#91;\n        'curl',\n        '-T', video_file,\n        f'ftp:\/\/{FTP_HOST}\/live\/',  # Back to \/live\/ directory\n        '--user', f'{FTP_USER}:{FTP_PASS}',\n        '--connect-timeout', '30',\n        '--max-time', '300'  # 5 minute timeout for upload\n    ]\n    \n    try:\n        result = subprocess.run(curl_cmd, capture_output=True, text=True, timeout=360)\n        \n        if result.returncode == 0:\n            log(f\"Upload successful: {video_name}\")\n            return True\n        else:\n            log(f\"ERROR: Upload failed with return code {result.returncode}\")\n            if result.stderr:\n                log(f\"curl stderr: {result.stderr}\")\n            return False\n            \n    except subprocess.TimeoutExpired:\n        log(\"ERROR: Upload timed out\")\n        return False\n    except Exception as e:\n        log(f\"ERROR: Upload failed: {e}\")\n        return False\n\ndef cleanup_old_files():\n    \"\"\"Clean up old temporary and video files\"\"\"\n    try:\n        # Remove temp files older than 1 hour (more targeted search)\n        temp_pattern = f\"{TEMP_DIR}\/reolink_capture_*.mp4\"\n        subprocess.run(&#91;'find', TEMP_DIR, '-maxdepth', '1', '-name', 'reolink_capture_*.mp4', \n                       '-mtime', '+0.04', '-delete'], timeout=10)\n        \n        # Remove old live videos older than 7 days\n        subprocess.run(&#91;'find', OUTPUT_DIR, '-name', 'live_*.mp4', \n                       '-mtime', '+7', '-delete'], timeout=10)\n        \n        log(\"Cleanup completed\")\n    except Exception as e:\n        log(f\"Cleanup failed: {e}\")\n\ndef main():\n    \"\"\"Main execution function\"\"\"\n    log(\"=== Starting on-demand Reolink video capture ===\")\n    \n    # Check dependencies\n    dependencies = &#91;'ffmpeg', 'curl', 'ping']\n    for dep in dependencies:\n        try:\n            subprocess.run(&#91;'which', dep], check=True, capture_output=True)\n        except subprocess.CalledProcessError:\n            log(f\"ERROR: Required dependency '{dep}' not found\")\n            sys.exit(1)\n    \n    # Check camera connectivity\n    if not check_connectivity():\n        sys.exit(1)\n    \n    # Capture video\n    video_file = capture_video()\n    \n    if video_file:\n        # Upload video\n        if upload_video(video_file):\n            log(\"Process completed successfully\")\n        else:\n            log(\"Process completed with upload errors\")\n    else:\n        log(\"Process failed during video capture\")\n        sys.exit(1)\n    \n    # Clean up old files\n    cleanup_old_files()\n    \n    log(\"=== On-demand capture completed ===\")\n\nif __name__ == \"__main__\":\n    main()<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[8],"tags":[],"class_list":["post-99","post","type-post","status-publish","format-standard","hentry","category-raspberry-pi"],"_links":{"self":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts\/99","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/comments?post=99"}],"version-history":[{"count":1,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts\/99\/revisions"}],"predecessor-version":[{"id":100,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/posts\/99\/revisions\/100"}],"wp:attachment":[{"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/media?parent=99"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/categories?post=99"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scootercam.net\/blog\/wp-json\/wp\/v2\/tags?post=99"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}