Some video bitrate improvements

This commit is contained in:
2026-04-05 15:31:32 +00:00
parent fc4c002a2e
commit a76715064a

View File

@@ -20,9 +20,23 @@ STATE_PATH = "twitter2bsky_state.json"
SCRAPE_TWEET_LIMIT = 30
DEDUPE_BSKY_LIMIT = 30
TWEET_MAX_AGE_DAYS = 3
VIDEO_MAX_DURATION_SECONDS = 179
BSKY_TEXT_MAX_LENGTH = 275
# Video handling notes:
# - Bluesky video support is constrained not just by duration, but also by
# practical upload limits like final file size, bitrate, resolution, and
# server-side proxy/PDS body-size caps.
# - Custom PDSes such as eurosky.social may accept images fine but fail on
# larger video blob uploads.
# - The public video limits discussed in Bluesky tooling references are useful,
# but in practice the safest approach is to:
# 1. cap duration
# 2. compress aggressively
# 3. log final file size
# 4. skip obviously too-large uploads
VIDEO_MAX_DURATION_SECONDS = 179
MAX_VIDEO_UPLOAD_SIZE_MB = 45
BSKY_BLOB_UPLOAD_MAX_RETRIES = 5
BSKY_BLOB_UPLOAD_BASE_DELAY = 10
BSKY_BLOB_UPLOAD_MAX_DELAY = 300
@@ -170,6 +184,11 @@ def get_rate_limit_wait_seconds(error_obj, default_delay):
def upload_blob_with_retry(client, binary_data, media_label="media"):
"""
Retry Bluesky blob upload when rate-limited.
Diagnostic note:
On alternate PDSes, large video uploads may fail for reasons other than
429 rate limits. In those cases we log the exception more explicitly and
return None so the caller can degrade gracefully.
"""
last_exception = None
@@ -184,7 +203,15 @@ def upload_blob_with_retry(client, binary_data, media_label="media"):
is_rate_limited = "429" in error_text or "RateLimitExceeded" in error_text
if not is_rate_limited:
logging.warning(f"Could not upload {media_label}: {e}")
logging.warning(f"Could not upload {media_label}: {repr(e)}")
if hasattr(e, "response") and e.response is not None:
try:
logging.warning(f"Upload response status: {e.response.status_code}")
logging.warning(f"Upload response body: {e.response.text}")
except Exception:
pass
return None
backoff_delay = min(
@@ -201,10 +228,10 @@ def upload_blob_with_retry(client, binary_data, media_label="media"):
time.sleep(wait_seconds)
else:
logging.warning(
f"❌ Exhausted blob upload retries for {media_label} after rate limiting: {e}"
f"❌ Exhausted blob upload retries for {media_label} after rate limiting: {repr(e)}"
)
logging.warning(f"Could not upload {media_label}: {last_exception}")
logging.warning(f"Could not upload {media_label}: {repr(last_exception)}")
return None
@@ -226,19 +253,52 @@ def get_blob_from_url(media_url, client, http_client):
return upload_blob_with_retry(client, content, media_label=media_url)
except Exception as e:
logging.warning(f"Could not fetch media {media_url}: {e}")
logging.warning(f"Could not fetch media {media_url}: {repr(e)}")
return None
def get_blob_from_file(file_path, client):
"""
Upload a local file as a Bluesky blob.
Diagnostic notes:
- We log the final file size because this is often the real reason a custom
PDS rejects video uploads.
- Self-hosted or alternate services may have stricter proxy/body-size limits
than bsky.social.
"""
try:
if not os.path.exists(file_path):
logging.warning(f"Could not upload local file {file_path}: file does not exist")
return None
file_size = os.path.getsize(file_path)
file_size_mb = file_size / (1024 * 1024)
logging.info(f"📦 Uploading local file {file_path} ({file_size_mb:.2f} MB)")
if file_path.lower().endswith(".mp4") and file_size_mb > MAX_VIDEO_UPLOAD_SIZE_MB:
logging.warning(
f"Could not upload local file {file_path}: "
f"file too large ({file_size_mb:.2f} MB > {MAX_VIDEO_UPLOAD_SIZE_MB} MB)"
)
return None
with open(file_path, "rb") as f:
binary_data = f.read()
return upload_blob_with_retry(client, binary_data, media_label=file_path)
except Exception as e:
logging.warning(f"Could not upload local file {file_path}: {e}")
logging.warning(f"Could not upload local file {file_path}: {repr(e)}")
if hasattr(e, "response") and e.response is not None:
try:
logging.warning(f"Upload response status: {e.response.status_code}")
logging.warning(f"Upload response body: {e.response.text}")
except Exception:
pass
return None
@@ -924,8 +984,24 @@ def extract_video_url_from_tweet_page(context, tweet_url):
# --- Video Processing ---
def download_and_crop_video(video_url, output_path):
"""
Download, trim, and compress video before upload.
Practical comments based on Bluesky video limits and real-world custom PDS behavior:
- Duration alone is not enough; final file size matters a lot.
- A 90-second 1080x1920 video can still be too large for alternate services.
- We therefore:
1. download the source
2. trim to VIDEO_MAX_DURATION_SECONDS
3. re-encode with tighter settings
4. scale down to max width 720
5. log final file size
- This improves compatibility with services like eurosky.social that may have
stricter body-size or timeout limits than bsky.social.
"""
temp_input = output_path.replace(".mp4", "_source.mp4")
temp_output = output_path.replace(".mp4", "_cropped.mp4")
temp_trimmed = output_path.replace(".mp4", "_trimmed.mp4")
temp_output = output_path.replace(".mp4", "_compressed.mp4")
try:
logging.info(f"⬇️ Downloading video source with ffmpeg: {video_url}")
@@ -953,11 +1029,7 @@ def download_and_crop_video(video_url, output_path):
temp_input,
]
download_result = subprocess.run(
download_cmd,
capture_output=True,
text=True
)
download_result = subprocess.run(download_cmd, capture_output=True, text=True)
if download_result.returncode != 0:
logging.error(f"❌ ffmpeg download failed:\n{download_result.stderr}")
@@ -985,29 +1057,64 @@ def download_and_crop_video(video_url, output_path):
cropped_clip = video_clip.subclip(0, end_time)
cropped_clip.write_videofile(
temp_output,
temp_trimmed,
codec="libx264",
audio_codec="aac",
preset="veryfast",
bitrate="1800k",
audio_bitrate="128k",
logger=None
)
video_clip.close()
cropped_clip.close()
if not os.path.exists(temp_output) or os.path.getsize(temp_output) == 0:
logging.error("Cropped video output is missing or empty.")
if not os.path.exists(temp_trimmed) or os.path.getsize(temp_trimmed) == 0:
logging.error("Trimmed video output is missing or empty.")
return None
trimmed_size_mb = os.path.getsize(temp_trimmed) / (1024 * 1024)
logging.info(f"📦 Trimmed video size before compression: {trimmed_size_mb:.2f} MB")
compress_cmd = [
"ffmpeg",
"-y",
"-i", temp_trimmed,
"-vf", "scale='min(720,iw)':-2",
"-c:v", "libx264",
"-preset", "veryfast",
"-crf", "30",
"-maxrate", "1800k",
"-bufsize", "3600k",
"-c:a", "aac",
"-b:a", "128k",
"-movflags", "+faststart",
temp_output,
]
compress_result = subprocess.run(compress_cmd, capture_output=True, text=True)
if compress_result.returncode != 0:
logging.error(f"❌ ffmpeg compression failed:\n{compress_result.stderr}")
return None
if not os.path.exists(temp_output) or os.path.getsize(temp_output) == 0:
logging.error("❌ Compressed video output is missing or empty.")
return None
final_size_mb = os.path.getsize(temp_output) / (1024 * 1024)
logging.info(f"✅ Video compressed successfully: {temp_output} ({final_size_mb:.2f} MB)")
os.replace(temp_output, output_path)
logging.info(f"Video cropped to {int(end_time)} seconds: {output_path}")
logging.info(f"Final video ready: {output_path}")
return output_path
except Exception as e:
logging.error(f"❌ Error processing video: {e}")
logging.error(f"❌ Error processing video: {repr(e)}")
return None
finally:
for path in [temp_input, temp_output]:
for path in [temp_input, temp_trimmed, temp_output]:
if os.path.exists(path):
try:
os.remove(path)