Skip to content

Instantly share code, notes, and snippets.

@jannejava
Created November 6, 2025 10:15
Show Gist options
  • Save jannejava/d0af62aaa07cbec41e2bdb5d8c87cb69 to your computer and use it in GitHub Desktop.
Save jannejava/d0af62aaa07cbec41e2bdb5d8c87cb69 to your computer and use it in GitHub Desktop.
Convert H264 to SER
#!/usr/bin/env python3
"""Convert video to SER format for Planet Stacker X"""
import struct
import subprocess
import sys
from datetime import datetime
def create_ser_file(input_file, output_file):
"""
Create a proper SER file with correct headers.
SER format specification: http://www.grischa-hahn.homepage.t-online.de/astro/ser/
"""
# Get video info using ffprobe
cmd = [
'ffprobe', '-v', 'error',
'-select_streams', 'v:0',
'-count_frames',
'-show_entries', 'stream=width,height,nb_read_frames,r_frame_rate',
'-of', 'default=noprint_wrappers=1',
input_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
lines = result.stdout.strip().split('\n')
width = height = frames = 0
for line in lines:
if line.startswith('width='):
width = int(line.split('=')[1])
elif line.startswith('height='):
height = int(line.split('=')[1])
elif line.startswith('nb_read_frames='):
frames = int(line.split('=')[1])
if not all([width, height, frames]):
print(f"Error: Could not get video info. w={width}, h={height}, frames={frames}")
return False
print(f"Video info: {width}x{height}, {frames} frames")
# SER Header structure
file_id = b'LUCAM-RECORDER' # 14 bytes
lu_id = 0 # 4 bytes (LuID)
color_id = 0 # 4 bytes (0 = MONO, 8 = BAYER_RGGB, etc.)
little_endian = 0 # 4 bytes (0 = big-endian, 1 = little-endian)
image_width = width # 4 bytes
image_height = height # 4 bytes
pixel_depth = 8 # 4 bytes (bits per pixel, 8 or 16)
frame_count = frames # 4 bytes
observer = b''.ljust(40, b'\x00') # 40 bytes
instrument = b''.ljust(40, b'\x00') # 40 bytes
telescope = b''.ljust(40, b'\x00') # 40 bytes
date_time = 0 # 8 bytes (UTC time)
date_time_utc = 0 # 8 bytes
# Write SER header
with open(output_file, 'wb') as f:
f.write(file_id)
f.write(struct.pack('<I', lu_id))
f.write(struct.pack('<I', color_id))
f.write(struct.pack('<I', little_endian))
f.write(struct.pack('<I', image_width))
f.write(struct.pack('<I', image_height))
f.write(struct.pack('<I', pixel_depth))
f.write(struct.pack('<I', frame_count))
f.write(observer)
f.write(instrument)
f.write(telescope)
f.write(struct.pack('<Q', date_time))
f.write(struct.pack('<Q', date_time_utc))
print(f"SER header written (178 bytes)")
# Extract frames using ffmpeg and append to SER file
print("Extracting frames as raw grayscale data...")
cmd = [
'ffmpeg', '-i', input_file,
'-f', 'rawvideo',
'-pix_fmt', 'gray',
'-'
]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
with open(output_file, 'ab') as f:
while True:
chunk = process.stdout.read(1024 * 1024) # Read 1MB at a time
if not chunk:
break
f.write(chunk)
process.wait()
if process.returncode == 0:
print(f"✓ SER file created successfully: {output_file}")
return True
else:
print(f"Error during conversion")
print(process.stderr.read().decode())
return False
if __name__ == '__main__':
input_file = 'input.mp4'
output_file = 'output.ser'
if len(sys.argv) > 1:
input_file = sys.argv[1]
if len(sys.argv) > 2:
output_file = sys.argv[2]
print(f"Converting {input_file} to SER format...")
create_ser_file(input_file, output_file)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment