{
  "id": "dPs9XxIqUmMcJ4RI",
  "name": "Video Processing Pipeline with Thumbnail Generation and CDN Distribution",
  "tags": [],
  "nodes": [
    {
      "id": "0c0cdae8-5aa2-4edb-bc88-1bfc52dccc05",
      "name": "Sticky Note - Overview1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        1200
      ],
      "parameters": {
        "width": 400,
        "height": 380,
        "content": "## Video Processing Pipeline\n\nThis workflow automates video processing from upload to delivery. When a video is uploaded to S3, it automatically generates thumbnails at multiple sizes, creates an animated preview, transcodes the video into web-optimized formats (1080p, 720p, 480p), and distributes everything via CDN.\n\nYou'll need AWS S3 credentials, an FFmpeg API endpoint, Cloudflare API access, and a Slack bot token. The workflow can be triggered by S3 events or manually via webhook.\n\nProcessed assets are automatically uploaded back to S3, CDN cache is invalidated, and your team gets notified in Slack when processing completes."
      },
      "typeVersion": 1
    },
    {
      "id": "b99bbc11-8163-45ef-8138-962239fb6c80",
      "name": "Sticky Note - Step ",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        1056
      ],
      "parameters": {
        "color": 7,
        "width": 584,
        "height": 652,
        "content": "### Step 1: Video Detection\nReceives the upload event and validates the file is a video format. Extracts the S3 bucket and file path, then generates a unique job ID for tracking."
      },
      "typeVersion": 1
    },
    {
      "id": "39259dec-4c82-4ad6-8620-7149412105b2",
      "name": "Sticky Note - Step 5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        1024
      ],
      "parameters": {
        "color": 7,
        "width": 488,
        "height": 684,
        "content": "### Step 2: Media Analysis\nUses FFprobe to extract video metadata including duration, resolution, bitrate, and codec info. Determines optimal thumbnail timestamps and whether transcoding is needed."
      },
      "typeVersion": 1
    },
    {
      "id": "d8ab0e88-4c79-4b2c-b124-9ccaead04fd2",
      "name": "Sticky Note - Step 6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1696,
        992
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 716,
        "content": "### Step 3: Processing\nGenerates thumbnails in 3 sizes, creates an animated GIF preview, and transcodes the video into multiple resolutions. All assets are uploaded to S3 for CDN distribution."
      },
      "typeVersion": 1
    },
    {
      "id": "76673f42-9830-4752-8e9b-bff98cffaf1a",
      "name": "Sticky Note - Step 7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2080,
        992
      ],
      "parameters": {
        "color": 7,
        "width": 568,
        "height": 716,
        "content": "### Step 4: Distribution\nInvalidates CDN cache to serve fresh content, generates signed URLs for secure access, logs metrics to Google Sheets, and notifies your team via Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "53ac5a8e-8bb4-42ce-98a1-8659072659b5",
      "name": "S3 Event Webhook1",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        640,
        1232
      ],
      "parameters": {
        "path": "s3-video-upload",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "651350a0-385f-489b-b6a5-329ad302820e",
      "name": "Manual Process Trigger1",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        640,
        1424
      ],
      "parameters": {
        "path": "process-video",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "245e3b5c-924a-48f2-bedc-323460577049",
      "name": "Merge Triggers1",
      "type": "n8n-nodes-base.merge",
      "position": [
        864,
        1328
      ],
      "parameters": {
        "mode": "chooseBranch",
        "output": "empty"
      },
      "typeVersion": 3
    },
    {
      "id": "1591ba26-5593-4bfc-bb66-682ed043d886",
      "name": "Extract S3 Info1",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        1328
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const data = item.json;\n  let bucket, key, eventType;\n  \n  if (data.Records && data.Records[0]) {\n    const record = data.Records[0];\n    bucket = record.s3?.bucket?.name;\n    key = decodeURIComponent(record.s3?.object?.key?.replace(/\\+/g, ' '));\n    eventType = record.eventName;\n  } else if (data.body) {\n    bucket = data.body.bucket || 'video-uploads';\n    key = data.body.key || data.body.video_key;\n    eventType = 'manual';\n  } else {\n    bucket = data.bucket || 'video-uploads';\n    key = data.key || 'sample-video.mp4';\n    eventType = 'unknown';\n  }\n  \n  const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v'];\n  const ext = key ? '.' + key.split('.').pop().toLowerCase() : '';\n  const isVideo = videoExtensions.includes(ext);\n  const jobId = `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n  \n  results.push({\n    json: {\n      job_id: jobId,\n      bucket: bucket,\n      key: key,\n      s3_url: `s3://${bucket}/${key}`,\n      https_url: `https://${bucket}.s3.amazonaws.com/${encodeURIComponent(key)}`,\n      filename: key ? key.split('/').pop() : 'unknown',\n      extension: ext,\n      is_video: isVideo,\n      event_type: eventType,\n      started_at: new Date().toISOString()\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "ece174e5-6e4d-420d-9a41-6853c7e70b01",
      "name": "Check Is Video1",
      "type": "n8n-nodes-base.if",
      "position": [
        1216,
        1328
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond1",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.is_video }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "83515379-c92e-4fdb-994d-9049b14d242b",
      "name": "Invalid File Response1",
      "type": "n8n-nodes-base.set",
      "position": [
        1408,
        1536
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "status",
              "name": "status",
              "type": "string",
              "value": "error"
            },
            {
              "id": "message",
              "name": "message",
              "type": "string",
              "value": "File is not a supported video format"
            },
            {
              "id": "job_id",
              "name": "job_id",
              "type": "string",
              "value": "={{ $json.job_id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a404b1c5-56c1-4d35-bbac-ddd81a60159c",
      "name": "Get Video Metadata (FFprobe)1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1408,
        1232
      ],
      "parameters": {
        "url": "https://api.ffmpeg-service.com/v1/probe",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"input\": \"{{ $json.https_url }}\",\n  \"options\": {\n    \"show_format\": true,\n    \"show_streams\": true\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "231560fe-7da8-4cad-af09-64bd18a6202a",
      "name": "Parse Video Metadata1",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        1232
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const data = item.json;\n  const probeResult = data.result || data;\n  const videoStream = probeResult.streams?.find(s => s.codec_type === 'video') || {};\n  const audioStream = probeResult.streams?.find(s => s.codec_type === 'audio') || {};\n  const format = probeResult.format || {};\n  \n  const duration = parseFloat(format.duration || videoStream.duration || 0);\n  const durationFormatted = new Date(duration * 1000).toISOString().substr(11, 8);\n  const thumbnailTimes = [0.1, 0.3, 0.5, 0.7, 0.9].map(p => Math.floor(duration * p));\n  \n  const width = parseInt(videoStream.width || 1920);\n  const height = parseInt(videoStream.height || 1080);\n  const needsTranscode = width > 1920 || !['h264', 'hevc'].includes(videoStream.codec_name);\n  \n  results.push({\n    json: {\n      ...data,\n      width: width,\n      height: height,\n      resolution: `${width}x${height}`,\n      aspect_ratio: (width / height).toFixed(2),\n      duration_seconds: duration,\n      duration_formatted: durationFormatted,\n      video_codec: videoStream.codec_name || 'unknown',\n      video_bitrate: parseInt(videoStream.bit_rate || 0),\n      frame_rate: videoStream.r_frame_rate || '30/1',\n      audio_codec: audioStream.codec_name || 'none',\n      audio_channels: audioStream.channels || 0,\n      file_size_bytes: parseInt(format.size || 0),\n      file_size_mb: (parseInt(format.size || 0) / (1024 * 1024)).toFixed(2),\n      format_name: format.format_name || 'unknown',\n      thumbnail_times: thumbnailTimes,\n      needs_transcode: needsTranscode,\n      target_resolutions: ['1080p', '720p', '480p']\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "4e0a1804-73cf-49d4-8734-dc6aab541eb6",
      "name": "Generate Thumbnails (FFmpeg)1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        1136
      ],
      "parameters": {
        "url": "https://api.ffmpeg-service.com/v1/thumbnails",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"input\": \"{{ $json.https_url }}\",\n  \"output_bucket\": \"{{ $json.bucket }}\",\n  \"output_prefix\": \"thumbnails/{{ $json.job_id }}/\",\n  \"times\": {{ JSON.stringify($json.thumbnail_times) }},\n  \"sizes\": [\n    { \"width\": 1280, \"height\": 720, \"suffix\": \"_large\" },\n    { \"width\": 640, \"height\": 360, \"suffix\": \"_medium\" },\n    { \"width\": 320, \"height\": 180, \"suffix\": \"_small\" }\n  ],\n  \"format\": \"jpg\",\n  \"quality\": 85\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "c44f8ab4-c4ad-4348-b0e6-17b5b20819d6",
      "name": "Generate Preview GIF1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        1328
      ],
      "parameters": {
        "url": "https://api.ffmpeg-service.com/v1/gif",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"input\": \"{{ $json.https_url }}\",\n  \"output_bucket\": \"{{ $json.bucket }}\",\n  \"output_key\": \"previews/{{ $json.job_id }}/preview.gif\",\n  \"start_time\": {{ Math.floor($json.duration_seconds * 0.2) }},\n  \"duration\": 5,\n  \"fps\": 10,\n  \"width\": 480\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "a52101b0-eee8-41fb-9275-50e061e2e5b7",
      "name": "Transcode Video1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        1536
      ],
      "parameters": {
        "url": "https://api.ffmpeg-service.com/v1/transcode",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"input\": \"{{ $json.https_url }}\",\n  \"output_bucket\": \"{{ $json.bucket }}\",\n  \"output_prefix\": \"transcoded/{{ $json.job_id }}/\",\n  \"presets\": [\n    { \"name\": \"1080p\", \"width\": 1920, \"height\": 1080, \"bitrate\": \"5000k\" },\n    { \"name\": \"720p\", \"width\": 1280, \"height\": 720, \"bitrate\": \"2500k\" },\n    { \"name\": \"480p\", \"width\": 854, \"height\": 480, \"bitrate\": \"1000k\" }\n  ],\n  \"codec\": \"h264\",\n  \"audio_codec\": \"aac\",\n  \"format\": \"mp4\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "2cb16b80-f76b-4573-b2fb-3716e97990ae",
      "name": "Aggregate Processing Results1",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1936,
        1328
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "273af213-8fad-4d56-93ec-6b17ad6076ae",
      "name": "Invalidate CDN Cache1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2128,
        1328
      ],
      "parameters": {
        "url": "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/purge_cache",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"prefixes\": [\n    \"thumbnails/{{ $json.data[0]?.job_id || 'unknown' }}/\",\n    \"previews/{{ $json.data[0]?.job_id || 'unknown' }}/\",\n    \"transcoded/{{ $json.data[0]?.job_id || 'unknown' }}/\"\n  ]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "9d5e9314-f5f5-43c9-8db4-67fb441ba6a8",
      "name": "Generate Signed URLs1",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        1328
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst data = items[0].json;\nconst jobData = data.data?.[0] || data;\nconst jobId = jobData.job_id || 'unknown';\n\nconst cdnBase = 'https://cdn.example.com';\nconst expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();\n\nconst result = {\n  job_id: jobId,\n  status: 'completed',\n  completed_at: new Date().toISOString(),\n  original: {\n    filename: jobData.filename,\n    resolution: jobData.resolution,\n    duration: jobData.duration_formatted,\n    size_mb: jobData.file_size_mb\n  },\n  thumbnails: {\n    large: `${cdnBase}/thumbnails/${jobId}/thumb_0_large.jpg`,\n    medium: `${cdnBase}/thumbnails/${jobId}/thumb_0_medium.jpg`,\n    small: `${cdnBase}/thumbnails/${jobId}/thumb_0_small.jpg`\n  },\n  preview_gif: `${cdnBase}/previews/${jobId}/preview.gif`,\n  transcoded: {\n    '1080p': `${cdnBase}/transcoded/${jobId}/video_1080p.mp4`,\n    '720p': `${cdnBase}/transcoded/${jobId}/video_720p.mp4`,\n    '480p': `${cdnBase}/transcoded/${jobId}/video_480p.mp4`\n  },\n  urls_expire_at: expiresAt\n};\n\nreturn [{ json: result }];"
      },
      "typeVersion": 2
    },
    {
      "id": "2ff7a3a5-a318-4930-a3ac-c0fa3653d334",
      "name": "Log Processing Metrics1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2480,
        1232
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "31dc01fb-e496-4520-b480-5c0007c02e94",
      "name": "Send Slack Notification1",
      "type": "n8n-nodes-base.slack",
      "position": [
        2480,
        1424
      ],
      "parameters": {
        "text": "=*Video Processing Complete*\n*Job ID:* {{ $json.job_id }}\n*File:* {{ $json.original.filename }}\n*Resolution:* {{ $json.original.resolution }}\n*Duration:* {{ $json.original.duration }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#video-processing"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "ded2767b-743b-4180-be73-a96609925393",
      "name": "Merge Output Paths1",
      "type": "n8n-nodes-base.merge",
      "position": [
        2656,
        1328
      ],
      "parameters": {
        "mode": "chooseBranch",
        "output": "empty"
      },
      "typeVersion": 3
    },
    {
      "id": "592b01b7-3c0b-4311-a6cf-9c3252f0b5cb",
      "name": "Merge All Paths1",
      "type": "n8n-nodes-base.merge",
      "position": [
        2848,
        1424
      ],
      "parameters": {
        "mode": "chooseBranch",
        "output": "empty"
      },
      "typeVersion": 3
    },
    {
      "id": "33dbd120-352a-4a0d-b56c-6fa950beef7f",
      "name": "Respond to Webhook1",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3024,
        1424
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f7001495-d296-4204-adb1-7635abb041e5",
  "connections": {
    "Check Is Video1": {
      "main": [
        [
          {
            "node": "Get Video Metadata (FFprobe)1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Invalid File Response1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Triggers1": {
      "main": [
        [
          {
            "node": "Extract S3 Info1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract S3 Info1": {
      "main": [
        [
          {
            "node": "Check Is Video1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Paths1": {
      "main": [
        [
          {
            "node": "Respond to Webhook1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcode Video1": {
      "main": [
        [
          {
            "node": "Aggregate Processing Results1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "S3 Event Webhook1": {
      "main": [
        [
          {
            "node": "Merge Triggers1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Output Paths1": {
      "main": [
        [
          {
            "node": "Merge All Paths1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Preview GIF1": {
      "main": [
        [
          {
            "node": "Aggregate Processing Results1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Signed URLs1": {
      "main": [
        [
          {
            "node": "Log Processing Metrics1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Slack Notification1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Invalidate CDN Cache1": {
      "main": [
        [
          {
            "node": "Generate Signed URLs1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Video Metadata1": {
      "main": [
        [
          {
            "node": "Generate Thumbnails (FFmpeg)1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Preview GIF1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Transcode Video1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Invalid File Response1": {
      "main": [
        [
          {
            "node": "Merge All Paths1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Log Processing Metrics1": {
      "main": [
        [
          {
            "node": "Merge Output Paths1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Process Trigger1": {
      "main": [
        [
          {
            "node": "Merge Triggers1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Send Slack Notification1": {
      "main": [
        [
          {
            "node": "Merge Output Paths1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Aggregate Processing Results1": {
      "main": [
        [
          {
            "node": "Invalidate CDN Cache1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Thumbnails (FFmpeg)1": {
      "main": [
        [
          {
            "node": "Aggregate Processing Results1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Video Metadata (FFprobe)1": {
      "main": [
        [
          {
            "node": "Parse Video Metadata1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}