Skip to content

Instantly share code, notes, and snippets.

@greg-randall
Forked from Steve-Tech/codec-reader.py
Last active October 29, 2024 05:49
Show Gist options
  • Save greg-randall/798fde2376f8dd632ffffb8a7aa4bdb8 to your computer and use it in GitHub Desktop.
Save greg-randall/798fde2376f8dd632ffffb8a7aa4bdb8 to your computer and use it in GitHub Desktop.
Codec Reader for HTML Video Tags, gets the correct codecs parameter for AV1, HEVC (H.265), and H.264 videos.
import json
import mimetypes
import subprocess
import sys
# Codec Reader for HTML Video Tags by Steve-Tech
# Usage: python3 codec-reader.py [-d] file_name
# Requires ffmpeg and ffprobe to be installed.
#
# Supported Codecs:
# Video:
# AV1
# H.264
# HEVC (H.265)
# Audio:
# AAC
#
debug = False
def main():
global debug
if len(sys.argv) > 1:
debug = "-d" in sys.argv
file_name = sys.argv[-1]
else:
file_name = input("File name: ")
print(get_type(file_name))
def get_codecs(file_name: str) -> tuple[str]:
"""
Returns a tuple of codecs found in the file.
Requires ffprobe to be installed.
"""
output = subprocess.check_output(
["ffprobe", "-of", "json", "-show_streams", file_name],
stderr=subprocess.DEVNULL,
)
codecs = tuple(stream["codec_name"] for stream in json.loads(output)["streams"])
if debug:
print("Found codecs:", codecs)
return codecs
def get_ffmpeg_headers(file_name: str) -> bytes:
"""
Returns the headers from ffmpeg.
Requires ffmpeg to be installed.
"""
return subprocess.run(
[
"ffmpeg",
"-i",
file_name,
"-c:v",
"copy",
"-bsf:v",
"trace_headers",
"-f",
"null",
"/dev/null",
],
stdin=subprocess.DEVNULL,
stderr=subprocess.PIPE,
).stderr
def get_type(file_name: str) -> str:
"""
Returns the mime type of the file including codecs.
"""
mime = mimetypes.guess_type(file_name)[0]
codecs = get_codecs(file_name)
headers = get_ffmpeg_headers(file_name)
type_codecs = []
for codec in codecs:
match codec:
case "av1":
type_codecs.append(get_type_av1(headers))
case "h264":
type_codecs.append(get_type_h264(headers))
case "hevc":
type_codecs.append(get_type_hevc(headers)) # Added HEVC/H.265
case "aac":
type_codecs.append(get_type_aac(headers))
case _:
print(f"Unknown codec: {codec}")
return f"{mime}; codecs={','.join(type_codecs)}"
def str_chk(s, r: str | None = None) -> str:
"""
Returns a string if it is not None, otherwise returns the replacement or raises an error.
"""
if s is not None:
return str(s)
elif r is not None:
return r
else:
raise ValueError("Missing value")
def read_ffmpeg(headers: bytes, item: bytes) -> int | None:
"""
Returns the value of an item from the headers in ffmpeg.
None if the item is not found.
"""
index = headers.find(item)
if index == -1:
return None
stop = headers.index(b"\n", index)
start = headers.rindex(b" ", index, stop)
if debug:
print(headers[index:stop].decode())
return int(headers[start + 1 : stop])
# -------- Video Codecs --------
def get_type_av1(headers: bytes) -> str:
# Existing AV1 codec handler code here
...
def get_type_h264(headers: bytes) -> str:
# Existing H.264 codec handler code here
...
def get_type_hevc(headers: bytes) -> str:
# Based on the format hev1.X.Y.LLL.BV for HEVC
codec = ["hev1"]
# X - Profile (1 for Main, 2 for Main10)
profile = read_ffmpeg(headers, b"profile_idc")
if profile == 1: # Main
codec.append("1")
elif profile == 2: # Main10
codec.append("2")
else:
codec.append("1") # Default to Main if profile isn't clear
# Y - Level (add "4" as a placeholder if undefined)
tier = "2" # Default value for Chrome compatibility
level = read_ffmpeg(headers, b"level_idc")
if level:
codec.append("4") # Chrome compatibility
else:
codec.append(str_chk(level, "4"))
# Tier and Level - formatted as L120 or L93
tier_char = "H" if read_ffmpeg(headers, b"seq_tier") == 1 else "M"
codec.append(f"L{str_chk(level, '120').rjust(3, '0')}")
# Chroma Format and Bit Depth - for subsampling
bit_depth = read_ffmpeg(headers, b"bit_depth") or 8
return ".".join(codec)
# -------- Audio Codecs --------
def get_type_aac(headers: bytes) -> str:
return "mp4a.40.2"
# -------- Main --------
if __name__ == "__main__":
main()
@Steve-Tech
Copy link

Good work figuring h265 out! Although it looks to me that the if level on line 157 will always append "4", but I haven't actually tested it though so correct me if I'm wrong!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment