Last active
March 9, 2026 18:40
-
-
Save akod1ng/7af310c7bfbd247d8ee216b6812a3641 to your computer and use it in GitHub Desktop.
Download frame.io preview on windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================ | |
| # 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