Skip to content

Instantly share code, notes, and snippets.

@akod1ng
Last active March 9, 2026 18:40
Show Gist options
  • Select an option

  • Save akod1ng/7af310c7bfbd247d8ee216b6812a3641 to your computer and use it in GitHub Desktop.

Select an option

Save akod1ng/7af310c7bfbd247d8ee216b6812a3641 to your computer and use it in GitHub Desktop.
Download frame.io preview on windows
# ============================================================
# Frame.io Video Downloader
# ============================================================
# Requirements (run once before first use):
# pip install playwright
# playwright install chromium
#
# Also requires:
# yt-dlp -> winget install yt-dlp
# ffmpeg -> winget install ffmpeg
#
# Usage:
# python frameio_download.py "https://next.frame.io/share/.../view/..."
# ============================================================
import subprocess
import sys
import re
from playwright.sync_api import sync_playwright
def extract_hls_urls(page_url):
video_url = None
audio_url = None
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Intercept all responses and capture /manifest/hls/media URLs
def handle_response(response):
nonlocal video_url, audio_url
url = response.url
if "/manifest/hls/media" in url:
# Decode JWT payload to identify stream type (middle part of JWT)
try:
import base64, json
jwt_part = url.split("/token/")[1].split("/manifest")[0]
payload = jwt_part.split(".")[1]
# Pad base64 if needed
payload += "=" * (4 - len(payload) % 4)
decoded = json.loads(base64.b64decode(payload))
stream_type = decoded.get("sub", {}).get("stream_type", "")
if stream_type == "video":
video_url = url
print(f"[+] Found VIDEO URL")
elif stream_type == "audio":
audio_url = url
print(f"[+] Found AUDIO URL")
except Exception:
# Fallback: assign in order
if not video_url:
video_url = url
elif not audio_url:
audio_url = url
page.on("response", handle_response)
print(f"[*] Opening: {page_url}")
page.goto(page_url, wait_until="networkidle", timeout=30000)
# Click play if video isn't auto-playing
try:
page.click("video", timeout=3000)
except Exception:
pass
# Wait for both URLs to be captured
page.wait_for_timeout(5000)
browser.close()
return video_url, audio_url
def download_stream(url, outfile):
print(f"[*] Downloading: {outfile}")
result = subprocess.run([
"yt-dlp", "--output", outfile, url
])
if result.returncode != 0:
print(f"[!] yt-dlp failed, exiting.")
sys.exit(1)
def merge_streams(video_file, audio_file, output_file):
print(f"[*] Merging into: {output_file}")
subprocess.run([
"ffmpeg", "-y",
"-i", video_file,
"-i", audio_file,
"-c:v", "copy",
"-c:a", "aac",
"-map", "0:v:0",
"-map", "1:a:0",
output_file
], check=True)
if __name__ == "__main__":
if len(sys.argv) < 2:
page_url = input("Paste your Frame.io share URL: ").strip()
else:
page_url = sys.argv[1]
# Extract asset ID for output filename
match = re.search(r'/view/([a-f0-9\-]{8})', page_url)
base_name = f"frameio_{match.group(1)}" if match else "frameio_video"
print("\n[*] Extracting HLS URLs from browser...")
video_url, audio_url = extract_hls_urls(page_url)
if not video_url or not audio_url:
print("[!] Could not capture both streams. Try again or paste URLs manually.")
video_url = input("Paste VIDEO URL: ").strip()
audio_url = input("Paste AUDIO URL: ").strip()
video_temp = f"{base_name}_video.mp4"
audio_temp = f"{base_name}_audio.mp4"
final_out = f"{base_name}_final.mp4"
download_stream(video_url, video_temp)
download_stream(audio_url, audio_temp)
merge_streams(video_temp, audio_temp, final_out)
import os
os.remove(video_temp)
os.remove(audio_temp)
print(f"\n[✓] Done! Output: {final_out}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment